Skip to content
Merged
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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions crates/datadog-serverless-logging/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "datadog-serverless-logging"
version = "0.1.0"
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true

[dependencies]
tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "env-filter"] }
56 changes: 56 additions & 0 deletions crates/datadog-serverless-logging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Datadog Serverless Logging

This crate lets a user set the log level of one part of a serverless agent, such as the Lambda
extension. Then the user can see the debug logs of that part without the debug logs of the
whole agent.

## Usage

Set the environment variable `DD_LOG_LEVEL_BY_TARGET` on the function, such as a Lambda
function. Its value is a comma-separated list of `<target>=<level>` entries. Each entry sets the
log level of one target, and the other targets keep the agent's default level. For example:

| Environment variable | Value |
| --- | --- |
| `DD_LOG_LEVEL_BY_TARGET` | `cold_start_duration=debug,dogstatsd=debug` |

With this value, the agent logs debug messages with the target `cold_start_duration`, and debug
messages from the `dogstatsd` crate.

The level is one of `off`, `error`, `warn`, `info`, `debug` and `trace`, in any case. The agent
logs a warning for an entry that is not valid, and ignores that entry. For example, `debug` with
no target is not valid.

## Targets

The agents log with [`tracing`](https://docs.rs/tracing), a Rust logging library. Every log
statement has a target. An entry matches each target that starts with the entry's text.

| Kind of target | Where the target comes from | Example entry |
| --- | --- | --- |
| Custom target | The statement sets it: `debug!(target: "cold_start_duration", ...)` | `cold_start_duration=debug` |
| 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` |

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:

- 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`.

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
logs. Then it logs a warning for the entries that are not valid. Each agent keeps its own log
format.

Only `tracing` events go through the filter. If a crate logs with the
[`log`](https://docs.rs/log) crate, the agent must install a bridge from `log` to `tracing`, for
example `tracing_log::LogTracer`.
122 changes: 122 additions & 0 deletions crates/datadog-serverless-logging/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! Log filter setup shared by the serverless agents.

use tracing_subscriber::{
EnvFilter,
filter::{Directive, ParseError},
};

/// Comma-separated `<target>=<level>` entries, each of which sets the log level of one tracing
/// target. The other targets keep the agent's default level.
pub const LEVEL_BY_TARGET_ENV_VAR: &str = "DD_LOG_LEVEL_BY_TARGET";

/// Builds a log filter from the agent's base filter and a [`LEVEL_BY_TARGET_ENV_VAR`] value.
/// The base filter uses the same comma-separated syntax, and it also sets the default level. For
/// example, `h2=off,info` turns off the `h2` logs and sets the default level to info.
///
/// A target is a custom target that a statement sets, such as `cold_start_duration`, or a
/// 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.
///
/// 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.
///
/// # Errors
///
/// Returns an error if `base` is not a valid filter.
pub fn build_env_filter(
base: &str,
levels_by_target: &str,
) -> Result<(EnvFilter, Vec<String>), ParseError> {
let mut env_filter = EnvFilter::try_new(base)?;
let mut invalid_entries = Vec::new();
for entry in levels_by_target
.split(',')
.map(str::trim)
.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()),
}
}
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> {
let (target, level) = entry.split_once('=')?;
let (target, level) = (target.trim(), level.trim());
let valid_target =
!target.is_empty() && !target.contains(|c: char| c.is_whitespace() || "[]{}".contains(c));
let valid_level = matches!(
level.to_ascii_lowercase().as_str(),
"off" | "error" | "warn" | "info" | "debug" | "trace"
);
if !(valid_target && valid_level) {
return None;
}
format!("{target}={level}").parse().ok()
}

#[cfg(test)]
mod tests {
use super::build_env_filter;

fn filter_entries(base: &str, levels_by_target: &str) -> (Vec<String>, Vec<String>) {
let (filter, invalid) =
build_env_filter(base, levels_by_target).expect("base filter is valid");
let entries = filter.to_string().split(',').map(String::from).collect();
(entries, invalid)
}

#[test]
fn adds_level_per_entry() {
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()));
assert!(entries.contains(&"info".to_string()));
assert!(invalid.is_empty());
}

#[test]
fn skips_invalid_entries() {
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()));
assert!(!entries.contains(&"debug".to_string()));
assert_eq!(
invalid,
vec![
"debug",
"cold_start_duration",
"cold_start_duration=verbose",
"=debug",
"[span]=debug",
"a b=debug",
]
);
}

#[test]
fn overrides_base_filter() {
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_invalid_base() {
assert!(build_env_filter("info,cold_start_duration=verbose", "").is_err());
}
}
Loading