-
Notifications
You must be signed in to change notification settings - Fork 9
Native API - Option to dump Rust tracing to file #458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cwshugg
wants to merge
10
commits into
Azure:main
Choose a base branch
from
cwshugg:user/connorshugg/trace_capturing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
9abf221
initial implementation for file-based tracing output capturing
cwshugg 11512dc
file and function renames for clarity
cwshugg 9e38aca
added ability to toggle between truncation and appending for trace fi…
cwshugg a129e75
minor comment updates
cwshugg 0f3960b
env var rename (again) and docs formatting/minor updates
cwshugg 872b794
cargo fmt
cwshugg 2cff195
PR feedback updates; file rename
cwshugg 441de26
fixed naming conflict pt. 2, other updates to code based on PR feedba…
cwshugg 105afff
removed same-file-name conflict in trace_file.rs test
cwshugg 212bfd2
Merge branch 'main' of https://github.com/cwshugg/azihsm-sdk into use…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # Diagnostics | ||
|
|
||
| ## File-Based Tracing | ||
|
|
||
| The native API library uses Rust's `tracing` framework internally for structured logging across the entire SDK stack, from the API layer down through the DDI. | ||
| By default no trace output is emitted, but it can be directed to a file by setting environment variables before loading the library. | ||
|
|
||
| This is useful for diagnosing failures in any host process that loads `azihsm_api_native` (e.g., the OpenSSL provider, C/C++ test binaries, or custom applications). | ||
|
|
||
| ### Environment Variables | ||
|
|
||
| | Variable | Required | Description | | ||
| |----------|----------|-------------| | ||
| | `AZIHSM_NATIVEAPI_TRACE_FILE` | Yes | File path for trace output. When set, a tracing subscriber is installed on the first API call. | | ||
| | `AZIHSM_NATIVEAPI_TRACE_FILE_APPEND` | No | Set to `1` to append to an existing file. If unset or any other value, the file is truncated on each run. | | ||
| | `RUST_LOG` | No | Controls the trace filter level. Defaults to `info`. Accepts standard `tracing` filter syntax (see below). | | ||
|
cwshugg marked this conversation as resolved.
cwshugg marked this conversation as resolved.
cwshugg marked this conversation as resolved.
|
||
|
|
||
| ### Trace Filter Syntax | ||
|
|
||
| The `RUST_LOG` variable accepts directives in the format used by [`tracing-subscriber`'s `EnvFilter`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html). | ||
| A few examples: | ||
|
|
||
| | Value | Effect | | ||
| |-------|--------| | ||
| | `debug` | All crates at DEBUG level and above | | ||
| | `trace` | All crates at TRACE level (most verbose) | | ||
| | `info` | All crates at INFO level (the default) | | ||
| | `azihsm_api=debug,azihsm_ddi_mock=info` | DEBUG for the API, INFO for the mock DDI, WARN for others | | ||
|
|
||
| ### Usage Example | ||
|
|
||
| ```bash | ||
| # Linux | ||
| export AZIHSM_NATIVEAPI_TRACE_FILE=/tmp/azihsm_trace.log | ||
| export RUST_LOG=debug | ||
| ./my_application | ||
|
|
||
| # Inspect the trace | ||
| head -50 /tmp/azihsm_trace.log | ||
| ``` | ||
|
|
||
| ```powershell | ||
| # Windows (PowerShell) | ||
| $env:AZIHSM_NATIVEAPI_TRACE_FILE = "$env:TEMP\azihsm_trace.log" | ||
| $env:RUST_LOG = "debug" | ||
| .\my_application.exe | ||
|
|
||
| # Inspect the trace | ||
| Get-Content $env:AZIHSM_NATIVEAPI_TRACE_FILE | Select-Object -First 50 | ||
| ``` | ||
|
|
||
| ### Output Format | ||
|
|
||
| Each line in the trace file contains a structured event with the following fields: | ||
|
|
||
| * **Timestamp** — UTC wall-clock time (e.g., `2026-06-12T17:13:42.223204Z`) | ||
|
cwshugg marked this conversation as resolved.
|
||
| * **Level** — `TRACE`, `DEBUG`, `INFO`, `WARN`, or `ERROR` | ||
| * **Thread ID** — Identifies the originating thread (e.g., `ThreadId(01)`) | ||
| * **Span context** — Nested call chain showing the path through the SDK | ||
| * **Target** — The Rust module that emitted the event (e.g., `azihsm_api::partition`) | ||
| * **Message** — The log message and any structured fields | ||
|
|
||
| Example output: | ||
|
|
||
| ```text | ||
| 2026-06-12T17:13:42.223204Z INFO ThreadId(01) partition_info_list: azihsm_api::partition: enter | ||
| 2026-06-12T17:13:42.224544Z DEBUG ThreadId(01) partition_info_list:dev_paths:dev_info_list{self=DdiMock}: azihsm_ddi_mock::ddi: Got DdiMock device info list size=1 | ||
| ``` | ||
|
|
||
| ### Behavior Notes | ||
|
|
||
| * Tracing initialization occurs exactly once, on the first API call. | ||
| Subsequent calls incur no overhead. | ||
| * If `AZIHSM_NATIVEAPI_TRACE_FILE` is not set, no subscriber is installed and there is no performance impact. | ||
| * If the trace file cannot be opened (e.g., invalid path or permission denied), the library silently continues without tracing. | ||
| * The trace subscriber is global to the process. | ||
| If another subscriber has already been installed (e.g., by the host application), the library's subscriber will not replace it. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| //! File-based tracing initialization for the native API. | ||
| //! | ||
| //! When the environment variable `AZIHSM_NATIVEAPI_TRACE_FILE` is set to a file | ||
| //! path, this module installs a `tracing_subscriber` that writes all trace | ||
| //! output to that file. Initialization is idempotent and thread-safe thanks | ||
| //! to [`std::sync::Once`]. | ||
| //! | ||
| //! By default, the file is truncated on each run. | ||
| //! Set `AZIHSM_NATIVEAPI_TRACE_FILE_APPEND=1` to append instead. | ||
| //! | ||
| //! If the environment variable is not set, or if any step of the | ||
| //! initialization fails (file open, filter parse, subscriber install), the | ||
| //! function silently returns without installing a subscriber. | ||
|
|
||
| use std::io::Write; | ||
| use std::sync::Once; | ||
|
|
||
| use tracing_subscriber::EnvFilter; | ||
| use tracing_subscriber::fmt; | ||
| use tracing_subscriber::fmt::MakeWriter; | ||
| use tracing_subscriber::prelude::*; | ||
|
cwshugg marked this conversation as resolved.
|
||
|
|
||
| /// Name of the environment variable that controls file-based tracing. | ||
| const TRACE_FILE_ENV_VAR: &str = "AZIHSM_NATIVEAPI_TRACE_FILE"; | ||
|
|
||
| /// Name of the environment variable that controls append mode. | ||
| /// When set to `"1"`, the trace file is opened in append mode so that | ||
| /// output from successive runs accumulates. Any other value (or unset) | ||
| /// causes the file to be truncated on each run. | ||
| const TRACE_FILE_APPEND_ENV_VAR: &str = "AZIHSM_NATIVEAPI_TRACE_FILE_APPEND"; | ||
|
|
||
| /// Thread-safe, non-poisoning file writer for the tracing subscriber. | ||
| /// | ||
| /// Uses [`parking_lot::Mutex`] instead of [`std::sync::Mutex`] so that a | ||
| /// panic while holding the lock does not poison the mutex and silently | ||
| /// break all subsequent trace writes. | ||
| struct TraceFileWriter(parking_lot::Mutex<std::fs::File>); | ||
|
|
||
| /// RAII guard returned by [`TraceFileWriter`] that implements [`Write`]. | ||
| struct TraceFileWriterGuard<'a>(parking_lot::MutexGuard<'a, std::fs::File>); | ||
|
|
||
| impl Write for TraceFileWriterGuard<'_> { | ||
| fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { | ||
| self.0.write(buf) | ||
| } | ||
|
|
||
| fn flush(&mut self) -> std::io::Result<()> { | ||
| self.0.flush() | ||
| } | ||
| } | ||
|
|
||
| impl<'a> MakeWriter<'a> for TraceFileWriter { | ||
| type Writer = TraceFileWriterGuard<'a>; | ||
|
|
||
| fn make_writer(&'a self) -> Self::Writer { | ||
| TraceFileWriterGuard(self.0.lock()) | ||
| } | ||
| } | ||
|
|
||
| /// Ensures file-based tracing is initialized exactly once. | ||
| /// | ||
| /// This function is safe to call from any thread and any number of times. | ||
| /// On the first call it checks `AZIHSM_NATIVEAPI_TRACE_FILE`: | ||
| /// | ||
| /// * If the variable is **not set**, no subscriber is installed. | ||
| /// * If it **is set**, the file is opened and a `tracing_subscriber::fmt` | ||
| /// subscriber is installed that writes timestamped, structured trace events | ||
| /// to the file. Timestamps are always in UTC. | ||
| /// | ||
| /// All errors are silently ignored so that tracing failures never affect | ||
| /// normal library operation. | ||
| pub(crate) fn init_trace_file() { | ||
| static ONCE: Once = Once::new(); | ||
|
|
||
| ONCE.call_once(|| { | ||
| // If the env var is not set, do nothing. | ||
| let trace_path = match std::env::var(TRACE_FILE_ENV_VAR) { | ||
| Ok(p) if !p.is_empty() => p, | ||
| _ => return, | ||
| }; | ||
|
|
||
| // Check whether append mode is requested. | ||
| let append = matches!(std::env::var(TRACE_FILE_APPEND_ENV_VAR).as_deref(), Ok("1")); | ||
|
|
||
| // Attempt to open/create the trace file. | ||
| let mut opts = std::fs::OpenOptions::new(); | ||
| opts.create(true).write(true); | ||
| if append { | ||
| opts.append(true); | ||
| } else { | ||
| opts.truncate(true); | ||
| } | ||
| let file = match opts.open(&trace_path) { | ||
| Ok(f) => f, | ||
| Err(_) => return, | ||
| }; | ||
|
Comment on lines
+88
to
+99
|
||
| let writer = TraceFileWriter(parking_lot::Mutex::new(file)); | ||
|
|
||
| // Build an EnvFilter from RUST_LOG, defaulting to `info`. | ||
| let filter = match EnvFilter::try_from_default_env() { | ||
| Ok(f) => f, | ||
| Err(_) => match EnvFilter::try_new("info") { | ||
| Ok(f) => f, | ||
| Err(_) => return, | ||
| }, | ||
| }; | ||
|
|
||
| // Build and install the subscriber. If `set_global_default` fails | ||
| // (e.g. another subscriber was already installed), silently ignore. | ||
| let subscriber = tracing_subscriber::registry().with(filter).with( | ||
|
cwshugg marked this conversation as resolved.
|
||
| fmt::layer() | ||
| .with_writer(writer) | ||
| .with_ansi(false) | ||
| .with_timer(fmt::time::SystemTime) | ||
| .with_thread_ids(true) | ||
| .with_target(true) | ||
| .with_span_events(fmt::format::FmtSpan::FULL), | ||
| ); | ||
|
|
||
| let _ = tracing::subscriber::set_global_default(subscriber); | ||
| }); | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| /// Calling `init_trace_file` multiple times must never panic, regardless | ||
| /// of whether the env var is set. | ||
| #[test] | ||
| fn init_trace_file_is_idempotent() { | ||
| // Without the env var set, these are all no-ops. | ||
| init_trace_file(); | ||
| init_trace_file(); | ||
| init_trace_file(); | ||
| } | ||
|
|
||
| /// Verifies that trace output is written to the file when the | ||
| /// environment variable is set. Runs as a subprocess so that the | ||
| /// `Once` guard and global subscriber don't interfere with other tests. | ||
| #[test] | ||
| fn trace_output_written_to_file() { | ||
| // Use a unique filename to avoid collisions when tests run in | ||
| // parallel or multiple jobs share the same temp directory. | ||
| let filename = format!( | ||
| "azihsm_nativeapi_trace_test_{}_{}.log", | ||
| std::process::id(), | ||
| std::time::SystemTime::now() | ||
| .duration_since(std::time::UNIX_EPOCH) | ||
| .map_or(0, |d| d.as_nanos()), | ||
| ); | ||
| let trace_path = std::env::temp_dir().join(filename); | ||
| let _ = std::fs::remove_file(&trace_path); | ||
|
|
||
| // Re-invoke *this* test binary running only the helper test, with | ||
| // the trace env vars set. The helper emits a known marker event. | ||
| // The helper is #[ignore]d so it doesn't run as a no-op during | ||
| // normal test execution; --include-ignored allows us to invoke it. | ||
| let exe = std::env::current_exe().expect("current_exe should be available"); | ||
| let status = std::process::Command::new(&exe) | ||
| .arg("--exact") | ||
| .arg("trace_file::tests::trace_output_helper") | ||
| .arg("--nocapture") | ||
| .arg("--include-ignored") | ||
| .env(TRACE_FILE_ENV_VAR, &trace_path) | ||
| .env("RUST_LOG", "trace") | ||
| .status() | ||
| .expect("failed to spawn subprocess"); | ||
|
|
||
| assert!(status.success(), "helper subprocess failed: {status}"); | ||
|
|
||
| let contents = std::fs::read_to_string(&trace_path) | ||
| .expect("trace file should exist after the helper ran"); | ||
|
|
||
| assert!( | ||
| contents.contains("trace_init_marker_event"), | ||
| "trace file should contain the marker event, but got:\n{contents}" | ||
| ); | ||
|
|
||
| // Verify the first line starts with an RFC 3339 UTC timestamp | ||
| // (e.g. "2026-06-12T17:13:42.223204Z"). | ||
| let first_line = contents.lines().next().unwrap_or(""); | ||
| assert!( | ||
| first_line.len() > 30 | ||
| && first_line.as_bytes()[4] == b'-' | ||
| && first_line.as_bytes()[10] == b'T' | ||
| && first_line.contains("Z "), | ||
| "first line should start with an RFC 3339 UTC timestamp, but was:\n{first_line}" | ||
| ); | ||
|
|
||
| let _ = std::fs::remove_file(&trace_path); | ||
| } | ||
|
|
||
| /// Helper test invoked as a subprocess by `trace_output_written_to_file`. | ||
| /// Not meant to be run directly — it requires the trace env vars to be | ||
| /// set by the parent process. | ||
| #[ignore] | ||
| #[test] | ||
| fn trace_output_helper() { | ||
| init_trace_file(); | ||
| tracing::info!("trace_init_marker_event"); | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not seeing issues with compilation locally - but I expect the
cargo xtask ...checks done on this PR should catch anything I missed.