Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions crates/datadog-serverless-logging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,19 +38,20 @@ 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`.

## 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
Expand Down
47 changes: 39 additions & 8 deletions crates/datadog-serverless-logging/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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<String>), ParseError> {
let mut env_filter = EnvFilter::try_new(base)?;
let mut invalid_entries = Vec::new();
Expand All @@ -38,16 +43,22 @@ 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))
}

/// Parses one `<target>=<level>` 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<Directive> {
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 =
Expand All @@ -59,16 +70,21 @@ fn parse_target_level(entry: &str) -> Option<Directive> {
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<String>, Vec<String>) {
fn filter_entries(
base: &str,
levels_by_target: &str,
locked_targets: &[&str],
) -> (Vec<String>, Vec<String>) {
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)
}
Expand All @@ -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()));
Expand All @@ -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()));
Expand All @@ -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());
}
}
Loading