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