From d3ffd988d1d9f0444cbac22eafe91ac4030e0073 Mon Sep 17 00:00:00 2001 From: lym953 <10097700+lym953@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:22:18 -0400 Subject: [PATCH] [SVLS-9883] feat: let agents lock targets in DD_LOG_LEVEL_BY_TARGET build_env_filter takes a list of targets whose level no entry can change. An entry is not valid if its target starts with a locked target, or if a locked target starts with its target, because an entry also matches every target that starts with its text. The Lambda extension will lock h2, hyper, reqwest and rustls: it sends its own logs to Datadog, so the logs these libraries write for each request would add lines to each flush. Co-Authored-By: Claude Opus 5.5 --- crates/datadog-serverless-logging/README.md | 11 ++++- crates/datadog-serverless-logging/src/lib.rs | 47 ++++++++++++++++---- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/crates/datadog-serverless-logging/README.md b/crates/datadog-serverless-logging/README.md index b514793b..3609cc3d 100644 --- a/crates/datadog-serverless-logging/README.md +++ b/crates/datadog-serverless-logging/README.md @@ -21,6 +21,12 @@ The level is one of `off`, `error`, `warn`, `info`, `debug` and `trace`, in any logs a warning for an entry that is not valid, and ignores that entry. For example, `debug` with no target is not valid. +An agent can lock some targets, so that no entry can change their level. For example, the Lambda +extension locks the HTTP libraries `h2`, `hyper`, `reqwest` and `rustls`. The extension sends its +own logs to Datadog, so their logs for each request would add lines to each flush. An entry for a +locked target is not valid. Because of the prefix match, an entry such as `h=debug` that starts +the name of a locked target is also not valid. + ## Targets The agents log with [`tracing`](https://docs.rs/tracing), a Rust logging library. Every log @@ -32,7 +38,7 @@ statement has a target. An entry matches each target that starts with the entry' | Module of the agent | A statement with no `target:` gets its module path, such as `bottlecap::traces::trace_flusher` in the Lambda extension. The entry also matches the modules under it. | `bottlecap::traces=debug` | | All of the agent's own code | The crate name matches all its module paths. The Lambda extension's crate is `bottlecap`. It does not match custom targets. | `bottlecap=debug` | | Crate from this repository | Its module paths start with the crate name, with underscores in place of hyphens | `dogstatsd=debug`, `datadog_agent_config=debug` | -| Third-party crate | The same as for a crate from this repository | `hyper=debug` | +| Third-party crate | The same as for a crate from this repository | `libdd_trace_utils=debug` | Because of the prefix match, do not give a custom target a name that starts with the name of another target. For example, an entry for `trace_flush` also matches `trace_flush_duration`. @@ -40,11 +46,12 @@ another target. For example, an entry for `trace_flush` also matches `trace_flus ## Use in an agent The agent calls the function `build_env_filter` once, when it sets up logging. The function -takes two inputs: +takes three inputs: - The agent's base filter, in the same format. It turns off some targets and sets the default level. For example, `h2=off,info` turns off the `h2` logs and sets the default level to info. - The value of `DD_LOG_LEVEL_BY_TARGET`. +- The targets that the agent locks, such as `["h2", "hyper"]`. It returns the filter, and the entries that are not valid. The agent gives the filter to [`tracing-subscriber`](https://docs.rs/tracing-subscriber), the library that writes the agent's diff --git a/crates/datadog-serverless-logging/src/lib.rs b/crates/datadog-serverless-logging/src/lib.rs index 991904f8..d0545dfd 100644 --- a/crates/datadog-serverless-logging/src/lib.rs +++ b/crates/datadog-serverless-logging/src/lib.rs @@ -20,6 +20,10 @@ pub const LEVEL_BY_TARGET_ENV_VAR: &str = "DD_LOG_LEVEL_BY_TARGET"; /// module path, such as `bottlecap::traces` or the crate name `dogstatsd`. An entry matches /// every target that starts with its text. The crate README lists the kinds of targets. /// +/// `locked_targets` lists targets whose level an entry must not change, such as `h2`. Because of +/// the prefix match, an entry is not valid if its target starts with a locked target, or if a +/// locked target starts with its target. For example, `h=debug` would also turn on `h2`. +/// /// Returns the entries that are not valid, so that the caller can log them after it sets up the /// subscriber. A typo then gives a warning instead of a failed startup. /// @@ -29,6 +33,7 @@ pub const LEVEL_BY_TARGET_ENV_VAR: &str = "DD_LOG_LEVEL_BY_TARGET"; pub fn build_env_filter( base: &str, levels_by_target: &str, + locked_targets: &[&str], ) -> Result<(EnvFilter, Vec), ParseError> { let mut env_filter = EnvFilter::try_new(base)?; let mut invalid_entries = Vec::new(); @@ -38,8 +43,14 @@ pub fn build_env_filter( .filter(|e| !e.is_empty()) { match parse_target_level(entry) { - Some(directive) => env_filter = env_filter.add_directive(directive), - None => invalid_entries.push(entry.to_string()), + Some((target, directive)) + if !locked_targets + .iter() + .any(|locked| target.starts_with(locked) || locked.starts_with(target)) => + { + env_filter = env_filter.add_directive(directive); + } + _ => invalid_entries.push(entry.to_string()), } } Ok((env_filter, invalid_entries)) @@ -47,7 +58,7 @@ pub fn build_env_filter( /// Parses one `=` entry. Rejects the other forms that `EnvFilter` accepts, such as a /// span filter like `[span]`, or a level with no target, which would replace the default level. -fn parse_target_level(entry: &str) -> Option { +fn parse_target_level(entry: &str) -> Option<(&str, Directive)> { let (target, level) = entry.split_once('=')?; let (target, level) = (target.trim(), level.trim()); let valid_target = @@ -59,16 +70,21 @@ fn parse_target_level(entry: &str) -> Option { if !(valid_target && valid_level) { return None; } - format!("{target}={level}").parse().ok() + let directive = format!("{target}={level}").parse().ok()?; + Some((target, directive)) } #[cfg(test)] mod tests { use super::build_env_filter; - fn filter_entries(base: &str, levels_by_target: &str) -> (Vec, Vec) { + fn filter_entries( + base: &str, + levels_by_target: &str, + locked_targets: &[&str], + ) -> (Vec, Vec) { let (filter, invalid) = - build_env_filter(base, levels_by_target).expect("base filter is valid"); + build_env_filter(base, levels_by_target, locked_targets).expect("base filter is valid"); let entries = filter.to_string().split(',').map(String::from).collect(); (entries, invalid) } @@ -78,6 +94,7 @@ mod tests { let (entries, invalid) = filter_entries( "h2=off,info", " cold_start_duration=debug, trace_flush_duration = TRACE ,", + &[], ); assert!(entries.contains(&"cold_start_duration=debug".to_string())); assert!(entries.contains(&"trace_flush_duration=trace".to_string())); @@ -90,6 +107,7 @@ mod tests { let (entries, invalid) = filter_entries( "h2=off,info", "debug,cold_start_duration,cold_start_duration=verbose,=debug,[span]=debug,a b=debug,trace_flush_duration=debug", + &[], ); assert!(entries.contains(&"trace_flush_duration=debug".to_string())); assert!(entries.contains(&"info".to_string())); @@ -109,14 +127,27 @@ mod tests { #[test] fn overrides_base_filter() { - let (entries, _) = filter_entries("h2=off,debug", "bottlecap::logs=warn,h2=debug"); + let (entries, _) = filter_entries("h2=off,debug", "bottlecap::logs=warn,h2=debug", &[]); assert!(entries.contains(&"bottlecap::logs=warn".to_string())); assert!(entries.contains(&"h2=debug".to_string())); assert!(!entries.contains(&"h2=off".to_string())); } + #[test] + fn rejects_entries_for_locked_targets() { + let (entries, invalid) = filter_entries( + "h2=off,hyper=off,info", + "h2=debug,hyper::proto=debug,h=debug,dogstatsd=debug", + &["h2", "hyper"], + ); + assert_eq!(invalid, vec!["h2=debug", "hyper::proto=debug", "h=debug"]); + assert!(entries.contains(&"dogstatsd=debug".to_string())); + assert!(entries.contains(&"h2=off".to_string())); + assert!(entries.contains(&"hyper=off".to_string())); + } + #[test] fn rejects_invalid_base() { - assert!(build_env_filter("info,cold_start_duration=verbose", "").is_err()); + assert!(build_env_filter("info,cold_start_duration=verbose", "", &[]).is_err()); } }