diff --git a/crates/trusted-server-adapter-axum/src/main.rs b/crates/trusted-server-adapter-axum/src/main.rs index 960982176..66d592e0b 100644 --- a/crates/trusted-server-adapter-axum/src/main.rs +++ b/crates/trusted-server-adapter-axum/src/main.rs @@ -1,10 +1,28 @@ use edgezero_adapter_axum::dev_server::{AxumDevServer, AxumDevServerConfig}; use edgezero_core::app::Hooks as _; +use log::LevelFilter; use trusted_server_adapter_axum::app::TrustedServerApp; +/// Environment variable that sets the maximum log level. +const LOG_LEVEL_ENV: &str = "RUST_LOG"; + +/// Log level used when `RUST_LOG` is unset or unrecognized. +const DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::Info; + #[allow(clippy::print_stderr)] fn main() { - if let Err(e) = simple_logger::SimpleLogger::new().init() { + // `var_os` keeps a set but non-UTF-8 value visible, so it is rejected + // with a warning instead of silently falling back. + let configured_level = + std::env::var_os(LOG_LEVEL_ENV).map(|value| value.to_string_lossy().into_owned()); + let level = parse_log_level(configured_level.as_deref()).unwrap_or_else(|value| { + eprintln!( + "warning: {LOG_LEVEL_ENV}={value:?} is not a single log level; using {}", + DEFAULT_LOG_LEVEL.as_str().to_ascii_lowercase() + ); + DEFAULT_LOG_LEVEL + }); + if let Err(e) = simple_logger::SimpleLogger::new().with_level(level).init() { eprintln!("warning: logger init failed: {e}"); } @@ -27,6 +45,26 @@ fn main() { } } +/// Parses an optional `RUST_LOG` value into the logger's maximum level. +/// +/// Absent and blank values resolve to [`DEFAULT_LOG_LEVEL`]. Any other value +/// must be a single level name (`trace`, `debug`, `info`, `warn`, `error`, or +/// `off`), matched case-insensitively after trimming whitespace. The quiet +/// default keeps trace-level payload and consent-string logging out of +/// standard output unless an operator explicitly asks for it. +/// +/// # Errors +/// +/// Returns the trimmed value when it is not a single level name, such as the +/// per-module filter `trusted_server=trace`, so the caller can warn before +/// falling back to [`DEFAULT_LOG_LEVEL`]. +fn parse_log_level(configured: Option<&str>) -> Result { + match configured.map(str::trim) { + None | Some("") => Ok(DEFAULT_LOG_LEVEL), + Some(trimmed) => trimmed.parse().map_err(|_| trimmed), + } +} + /// Read a port number from the `PORT` environment variable. /// /// Returns `None` when the variable is unset. Exits non-zero if the value @@ -46,6 +84,65 @@ fn port_from_env() -> Option { #[cfg(test)] mod tests { + use super::*; + #[test] fn crate_compiles() {} + + #[test] + fn parse_log_level_defaults_to_info_when_unset_or_blank() { + assert_eq!( + parse_log_level(None), + Ok(LevelFilter::Info), + "should default to info when RUST_LOG is unset" + ); + assert_eq!( + parse_log_level(Some("")), + Ok(LevelFilter::Info), + "should treat an empty value as unset" + ); + assert_eq!( + parse_log_level(Some(" ")), + Ok(LevelFilter::Info), + "should treat a whitespace-only value as unset" + ); + } + + #[test] + fn parse_log_level_honors_explicit_levels() { + assert_eq!( + parse_log_level(Some("info")), + Ok(LevelFilter::Info), + "should suppress trace when RUST_LOG is info" + ); + assert_eq!( + parse_log_level(Some("trace")), + Ok(LevelFilter::Trace), + "should enable trace when RUST_LOG is trace" + ); + assert_eq!( + parse_log_level(Some(" DEBUG ")), + Ok(LevelFilter::Debug), + "should accept case-insensitive levels with surrounding whitespace" + ); + assert_eq!( + parse_log_level(Some("off")), + Ok(LevelFilter::Off), + "should allow disabling logging" + ); + } + + #[test] + fn parse_log_level_rejects_unrecognized_values() { + assert_eq!( + parse_log_level(Some("trace,hyper=info")), + Err("trace,hyper=info"), + "should reject module-filter syntax it does not support" + ); + assert_eq!( + parse_log_level(Some(" verbose ")), + Err("verbose"), + "should reject an unknown level name and return it trimmed" + ); + } } diff --git a/docs/guide/axum-dev.md b/docs/guide/axum-dev.md index a98acbb06..82c18cd5d 100644 --- a/docs/guide/axum-dev.md +++ b/docs/guide/axum-dev.md @@ -45,6 +45,21 @@ rewrite its origin URL to the Axum listener, and remove the original URL. The exit trap stops the Axum process and stub origin, then removes the temporary config, local-push output, bodies, headers, and logs. +## Logging + +Axum logs to standard output at `info` by default. Set `RUST_LOG` to a single +level (`trace`, `debug`, `info`, `warn`, `error`, or `off`) to change it, for +example `RUST_LOG=debug cargo run -p trusted-server-adapter-axum`. +Unrecognized values, including per-module filters such as +`trusted_server=debug`, fall back to `info` and print a warning to standard +error at startup. + +The `trace` level can log auction payloads and TCF consent strings, for example +the `adserver_mock` mediation request, and the full Prebid Server response +(including its `ext.debug` echo of the resolved request) when a provider sets +`profile_config.debug = true`. Enable it only for short local debugging +sessions. + ## Runtime boundaries Axum implements the read-only `/_ts/admin/eids` diagnostic. EC record lookup diff --git a/docs/guide/testing.md b/docs/guide/testing.md index bff414f7c..86c025908 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -123,7 +123,7 @@ Show captured output and serialize a focused test when ordering or shared test state matters: ```bash -RUST_LOG=debug cargo test-axum test_name -- --nocapture --test-threads=1 +cargo test-axum test_name -- --nocapture --test-threads=1 cargo test-fastly test_name -- --nocapture --test-threads=1 ```