From a783c82cacfbe6b38854e11af16c80e03ee5fb5f Mon Sep 17 00:00:00 2001 From: Tianning Li Date: Tue, 24 Feb 2026 10:33:43 -0500 Subject: [PATCH] perf(logger): replace serde_json::json! with zero-allocation write! in logger Commit 3b533a0 introduced serde_json::json!() for JSON log formatting, which allocates a heap Map+Value on every log call and pulled in the tracing-serde crate via the "json" feature of tracing-subscriber, increasing binary size and Lambda init duration. Replace with direct write!/writeln! calls to the tracing Writer plus a small write_json_escaped helper that handles all 6 mandatory JSON escape sequences inline. The output format is identical. Also remove the "json" feature from tracing-subscriber, dropping tracing-serde as a transitive dependency. JIRA: https://datadoghq.atlassian.net/browse/SVLS-8619 --- bottlecap/Cargo.lock | 13 ------ bottlecap/Cargo.toml | 2 +- bottlecap/LICENSE-3rdparty.csv | 1 - bottlecap/src/logger.rs | 76 ++++++++++++++++++++++++++++++---- 4 files changed, 70 insertions(+), 22 deletions(-) diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index 298487f95..5d7392397 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -3784,16 +3784,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.3.22" @@ -3803,14 +3793,11 @@ dependencies = [ "matchers", "once_cell", "regex-automata", - "serde", - "serde_json", "sharded-slab", "thread_local", "tracing", "tracing-core", "tracing-log", - "tracing-serde", ] [[package]] diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index abc4ea306..b783045b8 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -34,7 +34,7 @@ tokio = { version = "1.47", default-features = false, features = ["macros", "rt- tokio-util = { version = "0.7", default-features = false } tracing = { version = "0.1", default-features = false } tracing-core = { version = "0.1", default-features = false } -tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "registry", "fmt", "env-filter", "tracing-log", "json"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "registry", "fmt", "env-filter", "tracing-log"] } hmac = { version = "0.12", default-features = false } sha2 = { version = "0.10", default-features = false } hex = { version = "0.4", default-features = false, features = ["std"] } diff --git a/bottlecap/LICENSE-3rdparty.csv b/bottlecap/LICENSE-3rdparty.csv index 7eb8750fd..85e333834 100644 --- a/bottlecap/LICENSE-3rdparty.csv +++ b/bottlecap/LICENSE-3rdparty.csv @@ -235,7 +235,6 @@ tracing,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman , Eliza Weisman , David Barsky " tracing-core,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors tracing-log,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors -tracing-serde,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors tracing-subscriber,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman , David Barsky , Tokio Contributors " try-lock,https://github.com/seanmonstar/try-lock,MIT,Sean McArthur twoway,https://github.com/bluss/twoway,MIT OR Apache-2.0,bluss diff --git a/bottlecap/src/logger.rs b/bottlecap/src/logger.rs index 72d7c111f..56079b90b 100644 --- a/bottlecap/src/logger.rs +++ b/bottlecap/src/logger.rs @@ -6,6 +6,23 @@ use tracing_subscriber::fmt::{ }; use tracing_subscriber::registry::LookupSpan; +/// Writes `s` to `w` with the 6 mandatory JSON string escape sequences applied. +/// Handles: `"`, `\`, `\n`, `\r`, `\t`, and U+0000–U+001F control characters. +fn write_json_escaped(w: &mut impl fmt::Write, s: &str) -> fmt::Result { + for c in s.chars() { + match c { + '"' => w.write_str("\\\"")?, + '\\' => w.write_str("\\\\")?, + '\n' => w.write_str("\\n")?, + '\r' => w.write_str("\\r")?, + '\t' => w.write_str("\\t")?, + c if (c as u32) < 0x20 => write!(w, "\\u{:04X}", c as u32)?, + c => w.write_char(c)?, + } + } + Ok(()) +} + #[derive(Debug, Clone, Copy)] pub struct Formatter; @@ -62,13 +79,9 @@ where let message = format!("DD_EXTENSION | {level} | {span_prefix}{}", visitor.0); - // Use serde_json for safe serialization (handles escaping automatically) - let output = serde_json::json!({ - "level": level.to_string(), - "message": message, - }); - - writeln!(writer, "{output}") + write!(writer, "{{\"level\":\"{level}\",\"message\":\"")?; + write_json_escaped(&mut writer, &message)?; + writeln!(writer, "\"}}") } } @@ -116,6 +129,55 @@ mod tests { } } + fn escaped(s: &str) -> String { + let mut out = String::new(); + write_json_escaped(&mut out, s).expect("write_json_escaped failed"); + out + } + + #[test] + fn test_escape_plain_text_is_unchanged() { + assert_eq!(escaped("hello world"), "hello world"); + } + + #[test] + fn test_escape_double_quote() { + assert_eq!(escaped(r#"say "hi""#), r#"say \"hi\""#); + } + + #[test] + fn test_escape_backslash() { + assert_eq!(escaped(r"C:\path"), r"C:\\path"); + } + + #[test] + fn test_escape_newline() { + assert_eq!(escaped("line1\nline2"), r"line1\nline2"); + } + + #[test] + fn test_escape_carriage_return() { + assert_eq!(escaped("a\rb"), r"a\rb"); + } + + #[test] + fn test_escape_tab() { + assert_eq!(escaped("a\tb"), r"a\tb"); + } + + #[test] + fn test_escape_control_characters() { + // U+0001 (SOH) and U+001F (US) must be \uXXXX-escaped + assert_eq!(escaped("\x01"), r"\u0001"); + assert_eq!(escaped("\x1F"), r"\u001F"); + } + + #[test] + fn test_escape_unicode_above_control_range_passes_through() { + // U+0020 (space) and above are not escaped + assert_eq!(escaped("€ ñ 中"), "€ ñ 中"); + } + #[test] fn test_formatter_outputs_valid_json_with_level() { let output = capture_log(|| {