Skip to content

Commit bfade6e

Browse files
perf(logger): replace serde_json::json! with zero-allocation write! in logger (#1042)
## Summary Root cause: commit 3b533a0 introduced `serde_json::json!()` for JSON formatting, adding per-call heap pressure and a new crate dependency that contributes to Lambda init duration regression JIRA: https://datadoghq.atlassian.net/browse/SVLS-8619 - Replaces `serde_json::json!()` in `logger.rs` with direct `write!`/`writeln!` calls to the tracing `Writer`, eliminating two heap allocations (a `Map` + `Value`) per log call - Adds a `write_json_escaped` helper that handles all 6 mandatory JSON escape sequences inline (`"`, `\`, `\n`, `\r`, `\t`, U+0000–U+001F) - Removes the `"json"` feature from `tracing-subscriber`, dropping `tracing-serde` as a transitive dependency and reducing binary size - The JSON output format is **identical** — no observable behavior change ## Test plan - unit tests - self-monitoring tests
1 parent 6c7ed88 commit bfade6e

File tree

4 files changed

+70
-22
lines changed

4 files changed

+70
-22
lines changed

bottlecap/Cargo.lock

Lines changed: 0 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bottlecap/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ tokio = { version = "1.47", default-features = false, features = ["macros", "rt-
3434
tokio-util = { version = "0.7", default-features = false }
3535
tracing = { version = "0.1", default-features = false }
3636
tracing-core = { version = "0.1", default-features = false }
37-
tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "registry", "fmt", "env-filter", "tracing-log", "json"] }
37+
tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "registry", "fmt", "env-filter", "tracing-log"] }
3838
hmac = { version = "0.12", default-features = false }
3939
sha2 = { version = "0.10", default-features = false }
4040
hex = { version = "0.4", default-features = false, features = ["std"] }

bottlecap/LICENSE-3rdparty.csv

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ tracing,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman <eliza@buoyant.io
235235
tracing-attributes,https://github.com/tokio-rs/tracing,MIT,"Tokio Contributors <team@tokio.rs>, Eliza Weisman <eliza@buoyant.io>, David Barsky <dbarsky@amazon.com>"
236236
tracing-core,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors <team@tokio.rs>
237237
tracing-log,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors <team@tokio.rs>
238-
tracing-serde,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors <team@tokio.rs>
239238
tracing-subscriber,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman <eliza@buoyant.io>, David Barsky <me@davidbarsky.com>, Tokio Contributors <team@tokio.rs>"
240239
try-lock,https://github.com/seanmonstar/try-lock,MIT,Sean McArthur <sean@seanmonstar.com>
241240
twoway,https://github.com/bluss/twoway,MIT OR Apache-2.0,bluss

bottlecap/src/logger.rs

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ use tracing_subscriber::fmt::{
66
};
77
use tracing_subscriber::registry::LookupSpan;
88

9+
/// Writes `s` to `w` with the 6 mandatory JSON string escape sequences applied.
10+
/// Handles: `"`, `\`, `\n`, `\r`, `\t`, and U+0000–U+001F control characters.
11+
fn write_json_escaped(w: &mut impl fmt::Write, s: &str) -> fmt::Result {
12+
for c in s.chars() {
13+
match c {
14+
'"' => w.write_str("\\\"")?,
15+
'\\' => w.write_str("\\\\")?,
16+
'\n' => w.write_str("\\n")?,
17+
'\r' => w.write_str("\\r")?,
18+
'\t' => w.write_str("\\t")?,
19+
c if (c as u32) < 0x20 => write!(w, "\\u{:04X}", c as u32)?,
20+
c => w.write_char(c)?,
21+
}
22+
}
23+
Ok(())
24+
}
25+
926
#[derive(Debug, Clone, Copy)]
1027
pub struct Formatter;
1128

@@ -62,13 +79,9 @@ where
6279

6380
let message = format!("DD_EXTENSION | {level} | {span_prefix}{}", visitor.0);
6481

65-
// Use serde_json for safe serialization (handles escaping automatically)
66-
let output = serde_json::json!({
67-
"level": level.to_string(),
68-
"message": message,
69-
});
70-
71-
writeln!(writer, "{output}")
82+
write!(writer, "{{\"level\":\"{level}\",\"message\":\"")?;
83+
write_json_escaped(&mut writer, &message)?;
84+
writeln!(writer, "\"}}")
7285
}
7386
}
7487

@@ -116,6 +129,55 @@ mod tests {
116129
}
117130
}
118131

132+
fn escaped(s: &str) -> String {
133+
let mut out = String::new();
134+
write_json_escaped(&mut out, s).expect("write_json_escaped failed");
135+
out
136+
}
137+
138+
#[test]
139+
fn test_escape_plain_text_is_unchanged() {
140+
assert_eq!(escaped("hello world"), "hello world");
141+
}
142+
143+
#[test]
144+
fn test_escape_double_quote() {
145+
assert_eq!(escaped(r#"say "hi""#), r#"say \"hi\""#);
146+
}
147+
148+
#[test]
149+
fn test_escape_backslash() {
150+
assert_eq!(escaped(r"C:\path"), r"C:\\path");
151+
}
152+
153+
#[test]
154+
fn test_escape_newline() {
155+
assert_eq!(escaped("line1\nline2"), r"line1\nline2");
156+
}
157+
158+
#[test]
159+
fn test_escape_carriage_return() {
160+
assert_eq!(escaped("a\rb"), r"a\rb");
161+
}
162+
163+
#[test]
164+
fn test_escape_tab() {
165+
assert_eq!(escaped("a\tb"), r"a\tb");
166+
}
167+
168+
#[test]
169+
fn test_escape_control_characters() {
170+
// U+0001 (SOH) and U+001F (US) must be \uXXXX-escaped
171+
assert_eq!(escaped("\x01"), r"\u0001");
172+
assert_eq!(escaped("\x1F"), r"\u001F");
173+
}
174+
175+
#[test]
176+
fn test_escape_unicode_above_control_range_passes_through() {
177+
// U+0020 (space) and above are not escaped
178+
assert_eq!(escaped("€ ñ 中"), "€ ñ 中");
179+
}
180+
119181
#[test]
120182
fn test_formatter_outputs_valid_json_with_level() {
121183
let output = capture_log(|| {

0 commit comments

Comments
 (0)