Skip to main content

tracing_microjson/
lib.rs

1//! A tracing JSON layer with zero serialization framework dependencies.
2//!
3//! Drop-in replacement for tracing-subscriber's `json` feature, producing
4//! identical output format without pulling in serde/serde_json/tracing-serde.
5//!
6//! # Example
7//!
8//! ```rust
9//! use tracing_microjson::JsonLayer;
10//! use tracing_subscriber::prelude::*;
11//!
12//! tracing_subscriber::registry()
13//!     .with(JsonLayer::new(std::io::stderr))
14//!     .init();
15//! ```
16
17use std::io::Write;
18use std::time::SystemTime;
19use tracing_core::{Event, Subscriber};
20use tracing_subscriber::Layer;
21use tracing_subscriber::layer::Context;
22use tracing_subscriber::registry::LookupSpan;
23
24mod visitor;
25
26#[cfg(feature = "_bench_internals")]
27pub mod writer;
28#[cfg(not(feature = "_bench_internals"))]
29mod writer;
30
31use visitor::JsonVisitor;
32use writer::JsonWriter;
33
34// Extension type stored in span data
35struct SpanFields(String);
36
37/// A [`tracing_subscriber::Layer`] that formats events as JSON lines.
38pub struct JsonLayer<W> {
39    make_writer: W,
40    display_target: bool,
41    display_filename: bool,
42    display_line_number: bool,
43    flatten_event: bool,
44}
45
46impl<W> JsonLayer<W>
47where
48    W: for<'w> tracing_subscriber::fmt::MakeWriter<'w> + 'static,
49{
50    /// Create a new `JsonLayer` writing to the given writer.
51    pub fn new(make_writer: W) -> Self {
52        Self {
53            make_writer,
54            display_target: true,
55            display_filename: false,
56            display_line_number: false,
57            flatten_event: false,
58        }
59    }
60
61    /// Whether to emit the `target` field. Default: `true`.
62    pub fn with_target(mut self, display_target: bool) -> Self {
63        self.display_target = display_target;
64        self
65    }
66
67    /// Whether to emit the `filename` field. Default: `false`.
68    pub fn with_file(mut self, display_filename: bool) -> Self {
69        self.display_filename = display_filename;
70        self
71    }
72
73    /// Whether to emit the `line_number` field. Default: `false`.
74    pub fn with_line_number(mut self, display_line: bool) -> Self {
75        self.display_line_number = display_line;
76        self
77    }
78
79    /// Whether to flatten event fields to the top level instead of nesting
80    /// them under `"fields"`. Default: `false`.
81    pub fn flatten_event(mut self, flatten: bool) -> Self {
82        self.flatten_event = flatten;
83        self
84    }
85}
86
87impl<S, W> Layer<S> for JsonLayer<W>
88where
89    S: Subscriber + for<'a> LookupSpan<'a>,
90    W: for<'w> tracing_subscriber::fmt::MakeWriter<'w> + 'static,
91{
92    fn on_new_span(
93        &self,
94        attrs: &tracing_core::span::Attributes<'_>,
95        id: &tracing_core::span::Id,
96        ctx: Context<'_, S>,
97    ) {
98        let span = match ctx.span(id) {
99            Some(s) => s,
100            None => return,
101        };
102        let mut jw = JsonWriter::new();
103        let mut visitor = JsonVisitor::new(&mut jw);
104        attrs.record(&mut visitor);
105        span.extensions_mut().insert(SpanFields(jw.into_string()));
106    }
107
108    fn on_record(
109        &self,
110        id: &tracing_core::span::Id,
111        values: &tracing_core::span::Record<'_>,
112        ctx: Context<'_, S>,
113    ) {
114        let span = match ctx.span(id) {
115            Some(s) => s,
116            None => return,
117        };
118        let mut ext = span.extensions_mut();
119        if let Some(fields) = ext.get_mut::<SpanFields>() {
120            let has_existing = !fields.0.is_empty();
121            let mut jw = JsonWriter::continuing(&fields.0);
122            let mut visitor = if has_existing {
123                JsonVisitor::continuing(&mut jw)
124            } else {
125                JsonVisitor::new(&mut jw)
126            };
127            values.record(&mut visitor);
128            fields.0 = jw.into_string();
129        }
130    }
131
132    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
133        let mut jw = JsonWriter::new();
134
135        // timestamp
136        jw.obj_start();
137        jw.key("timestamp");
138        jw.val_str(&format_timestamp(SystemTime::now()));
139
140        // level
141        jw.comma();
142        jw.key("level");
143        jw.val_str(&event.metadata().level().to_string());
144
145        if self.flatten_event {
146            // Event fields flattened to top level
147            let mut visitor = JsonVisitor::continuing(&mut jw);
148            event.record(&mut visitor);
149        } else {
150            // Event fields nested under "fields"
151            jw.comma();
152            jw.key("fields");
153            jw.obj_start();
154            let mut visitor = JsonVisitor::new(&mut jw);
155            event.record(&mut visitor);
156            jw.obj_end();
157        }
158
159        // target
160        if self.display_target {
161            jw.comma();
162            jw.key("target");
163            jw.val_str(event.metadata().target());
164        }
165
166        // filename
167        if self.display_filename
168            && let Some(file) = event.metadata().file()
169        {
170            jw.comma();
171            jw.key("filename");
172            jw.val_str(file);
173        }
174
175        // line_number
176        if self.display_line_number
177            && let Some(line) = event.metadata().line()
178        {
179            jw.comma();
180            jw.key("line_number");
181            jw.val_u64(line as u64);
182        }
183
184        // current span and spans list
185        if let Some(scope) = ctx.event_scope(event) {
186            let spans: Vec<_> = scope.collect();
187
188            // "span" = innermost (first in iterator = closest to current)
189            if let Some(leaf) = spans.first() {
190                jw.comma();
191                jw.key("span");
192                jw.obj_start();
193                jw.key("name");
194                jw.val_str(leaf.name());
195                let ext = leaf.extensions();
196                if let Some(fields) = ext.get::<SpanFields>()
197                    && !fields.0.is_empty()
198                {
199                    jw.comma();
200                    jw.raw(&fields.0);
201                }
202                jw.obj_end();
203            }
204
205            // "spans" = all spans from root to leaf
206            jw.comma();
207            jw.key("spans");
208            jw.arr_start();
209            for (i, span) in spans.iter().rev().enumerate() {
210                if i > 0 {
211                    jw.comma();
212                }
213                jw.obj_start();
214                jw.key("name");
215                jw.val_str(span.name());
216                let ext = span.extensions();
217                if let Some(fields) = ext.get::<SpanFields>()
218                    && !fields.0.is_empty()
219                {
220                    jw.comma();
221                    jw.raw(&fields.0);
222                }
223                jw.obj_end();
224            }
225            jw.arr_end();
226        }
227
228        jw.obj_end();
229        jw.finish_line();
230
231        let line = jw.into_string();
232        let mut writer = self.make_writer.make_writer();
233        let _ = writer.write_all(line.as_bytes());
234    }
235}
236
237/// Format a `SystemTime` as RFC 3339 with microsecond precision in UTC.
238/// e.g. "2026-02-20T12:00:00.000000Z"
239fn format_timestamp(t: SystemTime) -> String {
240    let dur = t.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
241    let secs = dur.as_secs();
242    let micros = dur.subsec_micros();
243
244    // Decompose Unix seconds into date/time components
245    let (year, month, day, hour, min, sec) = secs_to_datetime(secs);
246
247    format!(
248        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
249        year, month, day, hour, min, sec, micros
250    )
251}
252
253/// Convert Unix seconds to (year, month, day, hour, min, sec) in UTC.
254fn secs_to_datetime(secs: u64) -> (u64, u64, u64, u64, u64, u64) {
255    let sec = secs % 60;
256    let mins = secs / 60;
257    let min = mins % 60;
258    let hours = mins / 60;
259    let hour = hours % 24;
260    let days = hours / 24;
261
262    // Compute year, month, day from days since epoch (1970-01-01)
263    let (year, month, day) = days_to_ymd(days);
264
265    (year, month, day, hour, min, sec)
266}
267
268fn days_to_ymd(days: u64) -> (u64, u64, u64) {
269    // Using the algorithm from civil_from_days (Howard Hinnant's date algorithms)
270    let z = days + 719468;
271    let era = z / 146097;
272    let doe = z % 146097;
273    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
274    let y = yoe + era * 400;
275    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
276    let mp = (5 * doy + 2) / 153;
277    let d = doy - (153 * mp + 2) / 5 + 1;
278    let m = if mp < 10 { mp + 3 } else { mp - 9 };
279    let y = if m <= 2 { y + 1 } else { y };
280    (y, m, d)
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    /// Helper: write a string through val_str and return the raw buffer content.
288    fn val_str_output(s: &str) -> String {
289        let mut jw = JsonWriter::new();
290        jw.val_str(s);
291        jw.into_string()
292    }
293
294    #[test]
295    fn test_val_str_basic() {
296        assert_eq!(val_str_output("hello"), r#""hello""#);
297        assert_eq!(val_str_output("say \"hi\""), r#""say \"hi\"""#);
298        assert_eq!(val_str_output("back\\slash"), r#""back\\slash""#);
299        assert_eq!(val_str_output(""), r#""""#);
300    }
301
302    #[test]
303    fn test_val_str_control_chars() {
304        assert_eq!(val_str_output("\n"), r#""\n""#);
305        assert_eq!(val_str_output("\r"), r#""\r""#);
306        assert_eq!(val_str_output("\t"), r#""\t""#);
307        assert_eq!(val_str_output("\x08"), r#""\b""#);
308        assert_eq!(val_str_output("\x0C"), r#""\f""#);
309        // U+0001 → \u0001
310        assert_eq!(val_str_output("\x01"), r#""\u0001""#);
311        assert_eq!(val_str_output("\x1F"), r#""\u001f""#);
312    }
313
314    #[test]
315    fn test_val_str_unicode_passthrough() {
316        // Non-ASCII but above U+001F should pass through unescaped
317        assert_eq!(val_str_output("café"), "\"café\"");
318        assert_eq!(val_str_output("日本語"), "\"日本語\"");
319    }
320
321    #[test]
322    fn test_f64_edge_cases() {
323        let mut jw = JsonWriter::new();
324        jw.val_f64(f64::NAN);
325        assert_eq!(jw.into_string(), "null");
326
327        let mut jw = JsonWriter::new();
328        jw.val_f64(f64::INFINITY);
329        assert_eq!(jw.into_string(), "null");
330
331        let mut jw = JsonWriter::new();
332        jw.val_f64(f64::NEG_INFINITY);
333        assert_eq!(jw.into_string(), "null");
334
335        let mut jw = JsonWriter::new();
336        jw.val_f64(-0.0_f64);
337        let s = jw.into_string();
338        // -0.0 should be written as a number (not null)
339        assert!(
340            s == "-0" || s == "0" || s == "-0.0" || s == "0.0",
341            "got: {s}"
342        );
343
344        let mut jw = JsonWriter::new();
345        jw.val_f64(2.78);
346        let s = jw.into_string();
347        assert!(s.contains("2.78"), "got: {s}");
348    }
349
350    #[test]
351    fn test_timestamp_format() {
352        // Test known SystemTime value: Unix epoch
353        let epoch = SystemTime::UNIX_EPOCH;
354        let s = format_timestamp(epoch);
355        assert_eq!(s, "1970-01-01T00:00:00.000000Z");
356
357        // Test another known value: 2026-02-20T12:00:00Z = 1771588800 seconds
358        let t = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1771588800);
359        let s = format_timestamp(t);
360        assert_eq!(s, "2026-02-20T12:00:00.000000Z");
361    }
362
363    #[test]
364    fn test_timestamp_microsecond_precision() {
365        // 2026-02-20T12:00:00Z + 123456 µs → .123456
366        let t = SystemTime::UNIX_EPOCH
367            + std::time::Duration::from_micros(1_771_588_800 * 1_000_000 + 123_456);
368        let s = format_timestamp(t);
369        assert_eq!(s, "2026-02-20T12:00:00.123456Z");
370
371        // Exactly 1 µs past epoch
372        let t = SystemTime::UNIX_EPOCH + std::time::Duration::from_micros(1);
373        let s = format_timestamp(t);
374        assert_eq!(s, "1970-01-01T00:00:00.000001Z");
375
376        // 999999 µs (all six digits occupied)
377        let t = SystemTime::UNIX_EPOCH + std::time::Duration::from_micros(999_999);
378        let s = format_timestamp(t);
379        assert_eq!(s, "1970-01-01T00:00:00.999999Z");
380    }
381}