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
13 changes: 0 additions & 13 deletions bottlecap/Cargo.lock

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

2 changes: 1 addition & 1 deletion bottlecap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
1 change: 0 additions & 1 deletion bottlecap/LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@ tracing,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman <eliza@buoyant.io
tracing-attributes,https://github.com/tokio-rs/tracing,MIT,"Tokio Contributors <team@tokio.rs>, Eliza Weisman <eliza@buoyant.io>, David Barsky <dbarsky@amazon.com>"
tracing-core,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors <team@tokio.rs>
tracing-log,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors <team@tokio.rs>
tracing-serde,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors <team@tokio.rs>
tracing-subscriber,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman <eliza@buoyant.io>, David Barsky <me@davidbarsky.com>, Tokio Contributors <team@tokio.rs>"
try-lock,https://github.com/seanmonstar/try-lock,MIT,Sean McArthur <sean@seanmonstar.com>
twoway,https://github.com/bluss/twoway,MIT OR Apache-2.0,bluss
Expand Down
76 changes: 69 additions & 7 deletions bottlecap/src/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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, "\"}}")
}
}

Expand Down Expand Up @@ -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(|| {
Expand Down
Loading