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//! # Quick start
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//!
17//! # Configuration
18//!
19//! [`JsonLayer`] uses a builder pattern. All options have sensible defaults —
20//! only override what you need.
21//!
22//! ```rust
23//! # use tracing_microjson::JsonLayer;
24//! # use tracing_subscriber::prelude::*;
25//! tracing_subscriber::registry()
26//!     .with(
27//!         JsonLayer::new(std::io::stderr)
28//!             .with_target(false)
29//!             .with_file(true)
30//!             .with_line_number(true)
31//!             .flatten_event(true)
32//!             .without_time(),
33//!     )
34//!     .init();
35//! ```
36//!
37//! | Method | Default | Effect |
38//! |---|---|---|
39//! | [`JsonLayer::with_target`] | `true` | Include the event target (module path) |
40//! | [`JsonLayer::with_file`] | `false` | Include the source filename |
41//! | [`JsonLayer::with_line_number`] | `false` | Include the source line number |
42//! | [`JsonLayer::with_thread_ids`] | `false` | Include the thread ID |
43//! | [`JsonLayer::with_thread_names`] | `false` | Include the thread name |
44//! | [`JsonLayer::flatten_event`] | `false` | Flatten event fields to the top level instead of nesting under `"fields"` |
45//! | [`JsonLayer::with_timer`] | [`SystemTimestamp`] | Use a custom [`FormatTime`] implementation for timestamps |
46//! | [`JsonLayer::without_time`] | — | Disable timestamps entirely |
47//!
48//! # Output format
49//!
50//! Every event is written as a single JSON line. The fields present depend on
51//! the configuration above and whether the event occurs inside a span:
52//!
53//! ```text
54//! {"timestamp":"…","level":"INFO","fields":{"message":"hello"},"target":"my_app","span":{"name":"req"},"spans":[{"name":"req"}]}
55//! ```
56//!
57//! - `timestamp` — RFC 3339 with microsecond precision in UTC by default.
58//!   Customisable via [`with_timer`](JsonLayer::with_timer) or disabled with
59//!   [`without_time`](JsonLayer::without_time).
60//! - `level` — always present (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`).
61//! - `fields` — event fields, nested under `"fields"` by default. With
62//!   [`flatten_event(true)`](JsonLayer::flatten_event) they appear at the top
63//!   level instead.
64//! - `target` — module path, present when [`with_target`](JsonLayer::with_target)
65//!   is `true`.
66//! - `filename` / `line_number` — source location, present when enabled via
67//!   [`with_file`](JsonLayer::with_file) / [`with_line_number`](JsonLayer::with_line_number).
68//! - `threadId` / `threadName` — thread info, present when enabled via
69//!   [`with_thread_ids`](JsonLayer::with_thread_ids) / [`with_thread_names`](JsonLayer::with_thread_names).
70//! - `span` — the innermost active span (if any).
71//! - `spans` — all active spans from root to leaf (if any).
72
73use std::io::Write;
74use std::time::SystemTime;
75use tracing_core::{Event, Subscriber};
76use tracing_subscriber::Layer;
77use tracing_subscriber::fmt::format::Writer as FmtWriter;
78use tracing_subscriber::layer::Context;
79use tracing_subscriber::registry::LookupSpan;
80
81pub use tracing_subscriber::fmt::time::FormatTime;
82
83mod visitor;
84
85#[cfg(feature = "_bench_internals")]
86pub mod writer;
87#[cfg(not(feature = "_bench_internals"))]
88mod writer;
89
90use visitor::JsonVisitor;
91use writer::JsonWriter;
92
93/// A timestamp formatter that produces RFC 3339 timestamps with microsecond
94/// precision in UTC (e.g. `2026-02-20T12:00:00.000000Z`).
95///
96/// This is the default timer used by [`JsonLayer`]. It uses a hand-written
97/// formatter for minimal overhead — no chrono or time crate required.
98pub struct SystemTimestamp;
99
100impl FormatTime for SystemTimestamp {
101    fn format_time(&self, w: &mut FmtWriter<'_>) -> std::fmt::Result {
102        w.write_str(&format_timestamp(SystemTime::now()))
103    }
104}
105
106// Extension type stored in span data
107struct SpanFields(String);
108
109/// A [`tracing_subscriber::Layer`] that formats events as JSON lines.
110///
111/// See the [crate-level docs](crate) for configuration options and output
112/// format details.
113pub struct JsonLayer<W, T = SystemTimestamp> {
114    make_writer: W,
115    timer: T,
116    display_target: bool,
117    display_filename: bool,
118    display_line_number: bool,
119    display_thread_id: bool,
120    display_thread_name: bool,
121    flatten_event: bool,
122}
123
124impl<W> JsonLayer<W>
125where
126    W: for<'w> tracing_subscriber::fmt::MakeWriter<'w> + 'static,
127{
128    /// Create a new `JsonLayer` that writes JSON lines to `make_writer`.
129    ///
130    /// Accepts anything implementing [`tracing_subscriber::fmt::MakeWriter`],
131    /// e.g. `std::io::stderr` or `std::io::stdout`.
132    pub fn new(make_writer: W) -> Self {
133        Self {
134            make_writer,
135            timer: SystemTimestamp,
136            display_target: true,
137            display_filename: false,
138            display_line_number: false,
139            display_thread_id: false,
140            display_thread_name: false,
141            flatten_event: false,
142        }
143    }
144}
145
146impl<W, T> JsonLayer<W, T>
147where
148    W: for<'w> tracing_subscriber::fmt::MakeWriter<'w> + 'static,
149{
150    /// Set whether the `target` field (module path) is included in output.
151    ///
152    /// Default: **`true`**.
153    pub fn with_target(mut self, display_target: bool) -> Self {
154        self.display_target = display_target;
155        self
156    }
157
158    /// Set whether the `filename` field is included in output.
159    ///
160    /// Default: **`false`**.
161    pub fn with_file(mut self, display_filename: bool) -> Self {
162        self.display_filename = display_filename;
163        self
164    }
165
166    /// Set whether the `line_number` field is included in output.
167    ///
168    /// Default: **`false`**.
169    pub fn with_line_number(mut self, display_line: bool) -> Self {
170        self.display_line_number = display_line;
171        self
172    }
173
174    /// Set whether the `threadId` field is included in output.
175    ///
176    /// Default: **`false`**.
177    pub fn with_thread_ids(mut self, display_thread_id: bool) -> Self {
178        self.display_thread_id = display_thread_id;
179        self
180    }
181
182    /// Set whether the `threadName` field is included in output.
183    ///
184    /// Default: **`false`**.
185    pub fn with_thread_names(mut self, display_thread_name: bool) -> Self {
186        self.display_thread_name = display_thread_name;
187        self
188    }
189
190    /// Set whether event fields are flattened to the top level of the JSON
191    /// object instead of being nested under a `"fields"` key.
192    ///
193    /// Default: **`false`** (fields are nested).
194    pub fn flatten_event(mut self, flatten: bool) -> Self {
195        self.flatten_event = flatten;
196        self
197    }
198
199    /// Use a custom [`FormatTime`] implementation for timestamps.
200    ///
201    /// This replaces the default [`SystemTimestamp`] formatter. Any type
202    /// implementing [`FormatTime`] can be used, including those from
203    /// `tracing-subscriber` such as `Uptime` and `ChronoUtc`.
204    ///
205    /// Pass `()` to disable timestamps entirely (equivalent to
206    /// [`without_time`](Self::without_time)).
207    pub fn with_timer<T2: FormatTime>(self, timer: T2) -> JsonLayer<W, T2> {
208        JsonLayer {
209            make_writer: self.make_writer,
210            timer,
211            display_target: self.display_target,
212            display_filename: self.display_filename,
213            display_line_number: self.display_line_number,
214            display_thread_id: self.display_thread_id,
215            display_thread_name: self.display_thread_name,
216            flatten_event: self.flatten_event,
217        }
218    }
219
220    /// Disable timestamps in the output.
221    ///
222    /// This is a convenience for `self.with_timer(())`.
223    pub fn without_time(self) -> JsonLayer<W, ()> {
224        self.with_timer(())
225    }
226}
227
228impl<S, W, T> Layer<S> for JsonLayer<W, T>
229where
230    S: Subscriber + for<'a> LookupSpan<'a>,
231    W: for<'w> tracing_subscriber::fmt::MakeWriter<'w> + 'static,
232    T: FormatTime + 'static,
233{
234    fn on_new_span(
235        &self,
236        attrs: &tracing_core::span::Attributes<'_>,
237        id: &tracing_core::span::Id,
238        ctx: Context<'_, S>,
239    ) {
240        let span = match ctx.span(id) {
241            Some(s) => s,
242            None => return,
243        };
244        let mut jw = JsonWriter::new();
245        let mut visitor = JsonVisitor::new(&mut jw);
246        attrs.record(&mut visitor);
247        span.extensions_mut().insert(SpanFields(jw.into_string()));
248    }
249
250    fn on_record(
251        &self,
252        id: &tracing_core::span::Id,
253        values: &tracing_core::span::Record<'_>,
254        ctx: Context<'_, S>,
255    ) {
256        let span = match ctx.span(id) {
257            Some(s) => s,
258            None => return,
259        };
260        let mut ext = span.extensions_mut();
261        if let Some(fields) = ext.get_mut::<SpanFields>() {
262            let has_existing = !fields.0.is_empty();
263            let mut jw = JsonWriter::continuing(&fields.0);
264            let mut visitor = if has_existing {
265                JsonVisitor::continuing(&mut jw)
266            } else {
267                JsonVisitor::new(&mut jw)
268            };
269            values.record(&mut visitor);
270            fields.0 = jw.into_string();
271        }
272    }
273
274    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
275        let mut jw = JsonWriter::new();
276
277        jw.obj_start();
278
279        // timestamp (absent when timer is `()` / `without_time()`)
280        let mut ts_buf = String::new();
281        {
282            let mut fmt_writer = FmtWriter::new(&mut ts_buf);
283            let _ = self.timer.format_time(&mut fmt_writer);
284        }
285        let wrote_timestamp = !ts_buf.is_empty();
286        if wrote_timestamp {
287            jw.key("timestamp");
288            jw.val_str(&ts_buf);
289        }
290
291        // level
292        if wrote_timestamp {
293            jw.comma();
294        }
295        jw.key("level");
296        jw.val_str(&event.metadata().level().to_string());
297
298        if self.flatten_event {
299            // Event fields flattened to top level
300            let mut visitor = JsonVisitor::continuing(&mut jw);
301            event.record(&mut visitor);
302        } else {
303            // Event fields nested under "fields"
304            jw.comma();
305            jw.key("fields");
306            jw.obj_start();
307            let mut visitor = JsonVisitor::new(&mut jw);
308            event.record(&mut visitor);
309            jw.obj_end();
310        }
311
312        // target
313        if self.display_target {
314            jw.comma();
315            jw.key("target");
316            jw.val_str(event.metadata().target());
317        }
318
319        // filename
320        if self.display_filename
321            && let Some(file) = event.metadata().file()
322        {
323            jw.comma();
324            jw.key("filename");
325            jw.val_str(file);
326        }
327
328        // line_number
329        if self.display_line_number
330            && let Some(line) = event.metadata().line()
331        {
332            jw.comma();
333            jw.key("line_number");
334            jw.val_u64(line as u64);
335        }
336
337        // thread ID
338        if self.display_thread_id {
339            jw.comma();
340            jw.key("threadId");
341            jw.val_str(&format!("{:?}", std::thread::current().id()));
342        }
343
344        // thread name
345        if self.display_thread_name {
346            jw.comma();
347            jw.key("threadName");
348            if let Some(name) = std::thread::current().name() {
349                jw.val_str(name);
350            } else {
351                jw.val_str("");
352            }
353        }
354
355        // current span and spans list
356        if let Some(scope) = ctx.event_scope(event) {
357            let spans: Vec<_> = scope.collect();
358
359            // "span" = innermost (first in iterator = closest to current)
360            if let Some(leaf) = spans.first() {
361                jw.comma();
362                jw.key("span");
363                jw.obj_start();
364                jw.key("name");
365                jw.val_str(leaf.name());
366                let ext = leaf.extensions();
367                if let Some(fields) = ext.get::<SpanFields>()
368                    && !fields.0.is_empty()
369                {
370                    jw.comma();
371                    jw.raw(&fields.0);
372                }
373                jw.obj_end();
374            }
375
376            // "spans" = all spans from root to leaf
377            jw.comma();
378            jw.key("spans");
379            jw.arr_start();
380            for (i, span) in spans.iter().rev().enumerate() {
381                if i > 0 {
382                    jw.comma();
383                }
384                jw.obj_start();
385                jw.key("name");
386                jw.val_str(span.name());
387                let ext = span.extensions();
388                if let Some(fields) = ext.get::<SpanFields>()
389                    && !fields.0.is_empty()
390                {
391                    jw.comma();
392                    jw.raw(&fields.0);
393                }
394                jw.obj_end();
395            }
396            jw.arr_end();
397        }
398
399        jw.obj_end();
400        jw.finish_line();
401
402        let line = jw.into_string();
403        let mut writer = self.make_writer.make_writer();
404        let _ = writer.write_all(line.as_bytes());
405    }
406}
407
408/// Format a `SystemTime` as RFC 3339 with microsecond precision in UTC.
409/// e.g. "2026-02-20T12:00:00.000000Z"
410fn format_timestamp(t: SystemTime) -> String {
411    let dur = t.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
412    let secs = dur.as_secs();
413    let micros = dur.subsec_micros();
414
415    // Decompose Unix seconds into date/time components
416    let (year, month, day, hour, min, sec) = secs_to_datetime(secs);
417
418    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}.{micros:06}Z")
419}
420
421/// Convert Unix seconds to (year, month, day, hour, min, sec) in UTC.
422fn secs_to_datetime(secs: u64) -> (u64, u64, u64, u64, u64, u64) {
423    let sec = secs % 60;
424    let mins = secs / 60;
425    let min = mins % 60;
426    let hours = mins / 60;
427    let hour = hours % 24;
428    let days = hours / 24;
429
430    // Compute year, month, day from days since epoch (1970-01-01)
431    let (year, month, day) = days_to_ymd(days);
432
433    (year, month, day, hour, min, sec)
434}
435
436fn days_to_ymd(days: u64) -> (u64, u64, u64) {
437    // Using the algorithm from civil_from_days (Howard Hinnant's date algorithms)
438    let z = days + 719468;
439    let era = z / 146097;
440    let doe = z % 146097;
441    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
442    let y = yoe + era * 400;
443    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
444    let mp = (5 * doy + 2) / 153;
445    let d = doy - (153 * mp + 2) / 5 + 1;
446    let m = if mp < 10 { mp + 3 } else { mp - 9 };
447    let y = if m <= 2 { y + 1 } else { y };
448    (y, m, d)
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    /// Helper: write a string through val_str and return the raw buffer content.
456    fn val_str_output(s: &str) -> String {
457        let mut jw = JsonWriter::new();
458        jw.val_str(s);
459        jw.into_string()
460    }
461
462    #[test]
463    fn test_val_str_basic() {
464        assert_eq!(val_str_output("hello"), r#""hello""#);
465        assert_eq!(val_str_output("say \"hi\""), r#""say \"hi\"""#);
466        assert_eq!(val_str_output("back\\slash"), r#""back\\slash""#);
467        assert_eq!(val_str_output(""), r#""""#);
468    }
469
470    #[test]
471    fn test_val_str_control_chars() {
472        assert_eq!(val_str_output("\n"), r#""\n""#);
473        assert_eq!(val_str_output("\r"), r#""\r""#);
474        assert_eq!(val_str_output("\t"), r#""\t""#);
475        assert_eq!(val_str_output("\x08"), r#""\b""#);
476        assert_eq!(val_str_output("\x0C"), r#""\f""#);
477        // U+0001 → \u0001
478        assert_eq!(val_str_output("\x01"), r#""\u0001""#);
479        assert_eq!(val_str_output("\x1F"), r#""\u001f""#);
480    }
481
482    #[test]
483    fn test_val_str_unicode_passthrough() {
484        // Non-ASCII but above U+001F should pass through unescaped
485        assert_eq!(val_str_output("café"), "\"café\"");
486        assert_eq!(val_str_output("日本語"), "\"日本語\"");
487    }
488
489    #[test]
490    fn test_f64_edge_cases() {
491        let mut jw = JsonWriter::new();
492        jw.val_f64(f64::NAN);
493        assert_eq!(jw.into_string(), "null");
494
495        let mut jw = JsonWriter::new();
496        jw.val_f64(f64::INFINITY);
497        assert_eq!(jw.into_string(), "null");
498
499        let mut jw = JsonWriter::new();
500        jw.val_f64(f64::NEG_INFINITY);
501        assert_eq!(jw.into_string(), "null");
502
503        let mut jw = JsonWriter::new();
504        jw.val_f64(-0.0_f64);
505        let s = jw.into_string();
506        // -0.0 should be written as a number (not null)
507        assert!(
508            s == "-0" || s == "0" || s == "-0.0" || s == "0.0",
509            "got: {s}"
510        );
511
512        let mut jw = JsonWriter::new();
513        jw.val_f64(2.78);
514        let s = jw.into_string();
515        assert!(s.contains("2.78"), "got: {s}");
516    }
517
518    #[test]
519    fn test_timestamp_format() {
520        // Test known SystemTime value: Unix epoch
521        let epoch = SystemTime::UNIX_EPOCH;
522        let s = format_timestamp(epoch);
523        assert_eq!(s, "1970-01-01T00:00:00.000000Z");
524
525        // Test another known value: 2026-02-20T12:00:00Z = 1771588800 seconds
526        let t = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1771588800);
527        let s = format_timestamp(t);
528        assert_eq!(s, "2026-02-20T12:00:00.000000Z");
529    }
530
531    #[test]
532    fn test_timestamp_microsecond_precision() {
533        // 2026-02-20T12:00:00Z + 123456 µs → .123456
534        let t = SystemTime::UNIX_EPOCH
535            + std::time::Duration::from_micros(1_771_588_800 * 1_000_000 + 123_456);
536        let s = format_timestamp(t);
537        assert_eq!(s, "2026-02-20T12:00:00.123456Z");
538
539        // Exactly 1 µs past epoch
540        let t = SystemTime::UNIX_EPOCH + std::time::Duration::from_micros(1);
541        let s = format_timestamp(t);
542        assert_eq!(s, "1970-01-01T00:00:00.000001Z");
543
544        // 999999 µs (all six digits occupied)
545        let t = SystemTime::UNIX_EPOCH + std::time::Duration::from_micros(999_999);
546        let s = format_timestamp(t);
547        assert_eq!(s, "1970-01-01T00:00:00.999999Z");
548    }
549}