Skip to main content

tracing_ndjson/
lib.rs

1//! # tracing-ndjson
2//!
3//! [![Rust](https://github.com/cmackenzie1/tracing-ndjson/actions/workflows/rust.yml/badge.svg)](https://github.com/cmackenzie1/tracing-ndjson/actions/workflows/rust.yml)
4//!
5//! A simple library for tracing in new-line delimited JSON format. This library is meant to be used with [tracing](https://github.com/tokio-rs/tracing) as an alternative to the `tracing_subscriber::fmt::json` formatter.
6//!
7//! The goal of this crate is to provide a flattend JSON event, comprising of fields from the span attributes and event fields, with customizeable field names and timestamp formats.
8//!
9//! ## Features
10//!
11//! - Configurable field names for `target`, `message`, `level`, and `timestamp`.
12//! - Configurable timestamp formats
13//!   - RFC3339 (`2023-10-08T03:30:52Z`),
14//!   - RFC339Nanos (`2023-10-08T03:30:52.123456789Z`)
15//!   - Unix timestamp (`1672535452`)
16//!   - UnixMills (`1672535452123`)
17//! - Captures all span attributes and event fields in the root of the JSON object. Collisions will result in overwriting the existing field.
18//!
19//! ## Limitations
20//!
21//! - When flattening span attributes and event fields, the library will overwrite any existing fields with the same name, including the built-in fields such as `target`, `message`, `level`, `timestamp`, `file`, and `line`.
22//! - Non-determistic ordering of fields in the JSON object. ([JSON objects are unordered](https://www.json.org/json-en.html))
23//! - Currently only logs to stdout. (PRs welcome!)
24//!
25//! ## Usage
26//!
27//! Add this to your `Cargo.toml`:
28//!
29//! ```toml
30//! [dependencies]
31//! tracing = "0.1"
32//! tracing-ndjson = "0.2"
33//! ```
34//!
35//! ```rust
36//! use tracing_subscriber::prelude::*;
37//!
38//! let subscriber = tracing_subscriber::registry().with(tracing_ndjson::layer());
39//!
40//! tracing::subscriber::set_global_default(subscriber).unwrap();
41//!
42//! tracing::info!(life = 42, "Hello, world!");
43//! // {"level":"info","target":"default","life":42,"timestamp":"2023-10-20T21:17:49Z","message":"Hello, world!"}
44//!
45//! let span = tracing::info_span!("hello", "request.uri" = "https://example.com");
46//! span.in_scope(|| {
47//!     tracing::info!("Hello, world!");
48//!     // {"message":"Hello, world!","request.uri":"https://example.com","level":"info","target":"default","timestamp":"2023-10-20T21:17:49Z"}
49//! });
50//! ```
51//!
52//! ### Examples
53//!
54//! See the [examples](./examples) directory for more examples.
55//!
56//! ## License
57//!
58//! Licensed under [MIT license](./LICENSE)
59
60mod layer;
61mod storage;
62
63pub use layer::*;
64use tracing_core::Subscriber;
65use tracing_subscriber::registry::LookupSpan;
66
67/// A timestamp format for the JSON formatter.
68/// This is used to format the timestamp field in the JSON output.
69/// The default is RFC3339.
70#[derive(Debug, Default)]
71pub enum TimestampFormat {
72    /// Seconds since UNIX_EPOCH
73    Unix,
74    /// Milliseconds since UNIX_EPOCH
75    UnixMillis,
76    /// RFC3339
77    #[default]
78    Rfc3339,
79    /// RFC3339 with nanoseconds
80    Rfc3339Nanos,
81    /// Custom format string. This should be a valid format string for chrono.
82    Custom(String),
83}
84
85impl TimestampFormat {
86    fn format_string(&self, now: &chrono::DateTime<chrono::Utc>) -> String {
87        match self {
88            TimestampFormat::Unix => now.timestamp().to_string(),
89            TimestampFormat::UnixMillis => now.timestamp_millis().to_string(),
90            TimestampFormat::Rfc3339 => now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
91            TimestampFormat::Rfc3339Nanos => {
92                now.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true)
93            }
94            TimestampFormat::Custom(format) => now.format(format).to_string(),
95        }
96    }
97
98    fn format_number(&self, now: &chrono::DateTime<chrono::Utc>) -> u64 {
99        match self {
100            TimestampFormat::Unix => now.timestamp() as u64,
101            TimestampFormat::UnixMillis => now.timestamp_millis() as u64,
102            TimestampFormat::Rfc3339 => unreachable!("rfc3339 is not a number"),
103            TimestampFormat::Rfc3339Nanos => unreachable!("rfc3339_nanos is not a number"),
104            TimestampFormat::Custom(_) => unreachable!("custom is not a number"),
105        }
106    }
107}
108
109#[derive(Debug, Default)]
110pub enum Casing {
111    #[default]
112    Lowercase,
113    Uppercase,
114}
115
116/// A builder for the JSON formatter.
117/// This is used to configure the JSON formatter.
118/// The default configuration is:
119/// * level_name: "level"
120/// * level_value_casing: Casing::Lowercase
121/// * message_name: "message"
122/// * target_name: "target"
123/// * timestamp_name: "timestamp"
124/// * timestamp_format: TimestampFormat::Rfc3339
125/// * line_numbers: false
126/// * file_names: false
127/// * flatten_fields: true
128/// * flatten_spans: true
129///
130/// # Examples
131///
132/// ```rust
133/// use tracing_subscriber::prelude::*;
134///
135/// tracing_subscriber::registry()
136///     .with(
137///         tracing_ndjson::Builder::default()
138///             .with_level_name("severity")
139///             .with_level_value_casing(tracing_ndjson::Casing::Uppercase)
140///             .with_message_name("msg")
141///             .with_timestamp_name("ts")
142///             .with_timestamp_format(tracing_ndjson::TimestampFormat::Unix)
143///             .layer(),
144///     ).init();
145///
146/// tracing::info!(life = 42, "Hello, world!");
147pub struct Builder {
148    layer: crate::JsonFormattingLayer,
149}
150
151impl Builder {
152    pub fn new() -> Self {
153        Self {
154            layer: crate::JsonFormattingLayer::default(),
155        }
156    }
157}
158
159impl Default for Builder {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165/// Alias for `Builder::default()`.
166/// This is used to configure the JSON formatter.
167pub fn builder() -> Builder {
168    Builder::default()
169}
170
171impl Builder {
172    /// Set the field name for the level field.
173    /// The default is "level".
174    pub fn with_level_name(mut self, level_name: &'static str) -> Self {
175        self.layer.level_name = level_name;
176        self
177    }
178
179    /// Set the casing for the level field value.
180    /// The default is Casing::Lowercase.
181    pub fn with_level_value_casing(mut self, casing: Casing) -> Self {
182        self.layer.level_value_casing = casing;
183        self
184    }
185
186    /// Set the field name for the message field.
187    /// The default is "message".
188    pub fn with_message_name(mut self, message_name: &'static str) -> Self {
189        self.layer.message_name = message_name;
190        self
191    }
192
193    /// Set the field name for the target field.
194    /// The default is "target".
195    pub fn with_target_name(mut self, target_name: &'static str) -> Self {
196        self.layer.target_name = target_name;
197        self
198    }
199
200    /// Set the field name for the timestamp field.
201    /// The default is "timestamp".
202    pub fn with_timestamp_name(mut self, timestamp_name: &'static str) -> Self {
203        self.layer.timestamp_name = timestamp_name;
204        self
205    }
206
207    /// Set the timestamp format for the timestamp field.
208    /// The default is TimestampFormat::Rfc3339.
209    pub fn with_timestamp_format(mut self, timestamp_format: TimestampFormat) -> Self {
210        self.layer.timestamp_format = timestamp_format;
211        self
212    }
213
214    /// Set whether to flatten fields.
215    /// The default is true. If false, fields will be nested under a "fields" object.
216    pub fn with_flatten_fields(mut self, flatten_fields: bool) -> Self {
217        self.layer.flatten_fields = flatten_fields;
218        self
219    }
220
221    /// Set whether to flatten spans.
222    pub fn with_flatten_spans(mut self, flatten_spans: bool) -> Self {
223        self.layer.flatten_spans = flatten_spans;
224        self
225    }
226
227    /// Set whether to include line numbers.
228    pub fn with_line_numbers(mut self, line_numbers: bool) -> Self {
229        self.layer.line_numbers = line_numbers;
230        self
231    }
232
233    /// Set whether to include file names.
234    pub fn with_file_names(mut self, file_names: bool) -> Self {
235        self.layer.file_names = file_names;
236        self
237    }
238
239    /// Set a predicate that controls which fields appear in the JSON output.
240    /// Fields for which the predicate returns `false` are omitted from output
241    /// but are still recorded in span storage and visible to other layers.
242    pub fn with_field_filter(
243        mut self,
244        filter: impl Fn(&str) -> bool + Send + Sync + 'static,
245    ) -> Self {
246        self.layer.field_filter = Some(Box::new(filter));
247        self
248    }
249
250    pub fn layer<S>(self) -> impl tracing_subscriber::Layer<S>
251    where
252        S: Subscriber + for<'a> LookupSpan<'a>,
253    {
254        self.layer
255    }
256}
257
258/// Returns a `Layer` that subscribes to all spans and events using a JSON formatter.
259/// This is used to configure the JSON formatter.
260pub fn layer<S>() -> impl tracing_subscriber::Layer<S>
261where
262    S: Subscriber + for<'a> LookupSpan<'a>,
263{
264    crate::builder().layer
265}
266
267#[cfg(test)]
268mod tests {
269
270    use tracing::{debug, error, info, info_span, instrument, trace, warn};
271    use tracing_subscriber::prelude::*;
272
273    use super::*;
274
275    #[instrument]
276    fn some_function(a: u32, b: u32) {
277        let span = info_span!("some_span", a = a, b = b);
278        span.in_scope(|| {
279            info!("some message from inside a span");
280        });
281    }
282
283    #[test]
284    fn test_json_event_formatter() {
285        let subscriber = tracing_subscriber::registry().with(builder().layer());
286
287        tracing::subscriber::with_default(subscriber, || {
288            trace!(a = "b", "hello world from trace");
289            debug!("hello world from debug");
290            info!("hello world from info");
291            warn!("hello world from warn");
292            error!("hello world from error");
293            let span = info_span!(
294                "test_span",
295                person.firstname = "cole",
296                person.lastname = "mackenzie",
297                later = tracing::field::Empty,
298            );
299            span.in_scope(|| {
300                info!("some message from inside a info_span");
301                let inner = info_span!("inner_span", a = "b", c = "d", inner_span = true);
302                inner.in_scope(|| {
303                    info!(
304                        inner_span_field = true,
305                        later = "populated from inside a span",
306                        "some message from inside a info_span",
307                    );
308                });
309            });
310        });
311
312        let subscriber = tracing_subscriber::registry().with(
313            builder()
314                .with_level_name("severity")
315                .with_level_value_casing(Casing::Uppercase)
316                .with_message_name("msg")
317                .with_timestamp_name("ts")
318                .with_timestamp_format(TimestampFormat::Unix)
319                .with_flatten_fields(false)
320                .layer(),
321        );
322
323        tracing::subscriber::with_default(subscriber, || {
324            trace!(a = "b", "hello world from trace");
325            debug!("hello world from debug");
326            info!("hello world from info");
327            warn!("hello world from warn");
328            error!("hello world from error");
329            let span = info_span!(
330                "test_span",
331                person.firstname = "cole",
332                person.lastname = "mackenzie",
333                later = tracing::field::Empty,
334            );
335            span.in_scope(|| {
336                info!("some message from inside a info_span");
337                let inner = info_span!("inner_span", a = "b", c = "d", inner_span = true);
338                inner.in_scope(|| {
339                    info!(
340                        inner_span_field = true,
341                        later = "populated from inside a span",
342                        "some message from inside a info_span",
343                    );
344                });
345            });
346        });
347    }
348
349    #[test]
350    fn test_nested_spans() {
351        let subscriber = tracing_subscriber::registry().with(builder().layer());
352
353        tracing::subscriber::with_default(subscriber, || {
354            let span = info_span!(
355                "test_span",
356                person.firstname = "cole",
357                person.lastname = "mackenzie",
358                later = tracing::field::Empty,
359            );
360            span.in_scope(|| {
361                info!("some message from inside a info_span");
362                let inner = info_span!("inner_span", a = "b", c = "d", inner_span = true);
363                inner.in_scope(|| {
364                    info!(
365                        inner_span_field = true,
366                        later = "populated from inside a span",
367                        "some message from inside a info_span",
368                    );
369                });
370            });
371
372            some_function(1, 2);
373        });
374    }
375
376    #[test]
377    fn test_field_filter() {
378        let subscriber = tracing_subscriber::registry().with(
379            builder()
380                .with_field_filter(|name| !name.starts_with("__"))
381                .layer(),
382        );
383
384        tracing::subscriber::with_default(subscriber, || {
385            info!(
386                __sentry_fn = "MyService::handle",
387                __sentry_label = "request failed",
388                correlation_id = "abc-123",
389                "something went wrong"
390            );
391
392            let span = info_span!("request", __internal = "hidden", request_id = "req-456",);
393            span.in_scope(|| {
394                info!("processing request");
395            });
396        });
397    }
398}