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#[derive(Debug, thiserror::Error)]
117enum Error {
118    #[error("fmt error: {0}")]
119    Format(#[from] std::fmt::Error),
120    #[error("json error: {0}")]
121    Serde(#[from] serde_json::Error),
122    #[error("utf8 error: {0}")]
123    Utf8(#[from] std::str::Utf8Error),
124}
125
126impl From<Error> for std::fmt::Error {
127    fn from(_: Error) -> Self {
128        Self
129    }
130}
131
132/// A builder for the JSON formatter.
133/// This is used to configure the JSON formatter.
134/// The default configuration is:
135/// * level_name: "level"
136/// * level_value_casing: Casing::Lowercase
137/// * message_name: "message"
138/// * target_name: "target"
139/// * timestamp_name: "timestamp"
140/// * timestamp_format: TimestampFormat::Rfc3339
141/// * line_numbers: false
142/// * file_names: false
143/// * flatten_fields: true
144/// * flatten_spans: true
145///
146/// # Examples
147///
148/// ```rust
149/// use tracing_subscriber::prelude::*;
150///
151/// tracing_subscriber::registry()
152///     .with(
153///         tracing_ndjson::Builder::default()
154///             .with_level_name("severity")
155///             .with_level_value_casing(tracing_ndjson::Casing::Uppercase)
156///             .with_message_name("msg")
157///             .with_timestamp_name("ts")
158///             .with_timestamp_format(tracing_ndjson::TimestampFormat::Unix)
159///             .layer(),
160///     ).init();
161///
162/// tracing::info!(life = 42, "Hello, world!");
163pub struct Builder {
164    layer: crate::JsonFormattingLayer,
165}
166
167impl Builder {
168    pub fn new() -> Self {
169        Self {
170            layer: crate::JsonFormattingLayer::default(),
171        }
172    }
173}
174
175impl Default for Builder {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181/// Alias for `Builder::default()`.
182/// This is used to configure the JSON formatter.
183pub fn builder() -> Builder {
184    Builder::default()
185}
186
187impl Builder {
188    /// Set the field name for the level field.
189    /// The default is "level".
190    pub fn with_level_name(mut self, level_name: &'static str) -> Self {
191        self.layer.level_name = level_name;
192        self
193    }
194
195    /// Set the casing for the level field value.
196    /// The default is Casing::Lowercase.
197    pub fn with_level_value_casing(mut self, casing: Casing) -> Self {
198        self.layer.level_value_casing = casing;
199        self
200    }
201
202    /// Set the field name for the message field.
203    /// The default is "message".
204    pub fn with_message_name(mut self, message_name: &'static str) -> Self {
205        self.layer.message_name = message_name;
206        self
207    }
208
209    /// Set the field name for the target field.
210    /// The default is "target".
211    pub fn with_target_name(mut self, target_name: &'static str) -> Self {
212        self.layer.target_name = target_name;
213        self
214    }
215
216    /// Set the field name for the timestamp field.
217    /// The default is "timestamp".
218    pub fn with_timestamp_name(mut self, timestamp_name: &'static str) -> Self {
219        self.layer.timestamp_name = timestamp_name;
220        self
221    }
222
223    /// Set the timestamp format for the timestamp field.
224    /// The default is TimestampFormat::Rfc3339.
225    pub fn with_timestamp_format(mut self, timestamp_format: TimestampFormat) -> Self {
226        self.layer.timestamp_format = timestamp_format;
227        self
228    }
229
230    /// Set whether to flatten fields.
231    /// The default is true. If false, fields will be nested under a "fields" object.
232    pub fn with_flatten_fields(mut self, flatten_fields: bool) -> Self {
233        self.layer.flatten_fields = flatten_fields;
234        self
235    }
236
237    /// Set whether to flatten spans.
238    pub fn with_flatten_spans(mut self, flatten_spans: bool) -> Self {
239        self.layer.flatten_spans = flatten_spans;
240        self
241    }
242
243    /// Set whether to include line numbers.
244    pub fn with_line_numbers(mut self, line_numbers: bool) -> Self {
245        self.layer.line_numbers = line_numbers;
246        self
247    }
248
249    /// Set whether to include file names.
250    pub fn with_file_names(mut self, file_names: bool) -> Self {
251        self.layer.file_names = file_names;
252        self
253    }
254
255    pub fn layer<S>(self) -> impl tracing_subscriber::Layer<S>
256    where
257        S: Subscriber + for<'a> LookupSpan<'a>,
258    {
259        self.layer
260    }
261}
262
263/// Returns a `Layer` that subscribes to all spans and events using a JSON formatter.
264/// This is used to configure the JSON formatter.
265pub fn layer<S>() -> impl tracing_subscriber::Layer<S>
266where
267    S: Subscriber + for<'a> LookupSpan<'a>,
268{
269    crate::builder().layer
270}
271
272#[cfg(test)]
273mod tests {
274
275    use tracing::{debug, error, info, info_span, instrument, trace, warn};
276    use tracing_subscriber::prelude::*;
277
278    use super::*;
279
280    #[instrument]
281    fn some_function(a: u32, b: u32) {
282        let span = info_span!("some_span", a = a, b = b);
283        span.in_scope(|| {
284            info!("some message from inside a span");
285        });
286    }
287
288    #[test]
289    fn test_json_event_formatter() {
290        let subscriber = tracing_subscriber::registry().with(builder().layer());
291
292        tracing::subscriber::with_default(subscriber, || {
293            trace!(a = "b", "hello world from trace");
294            debug!("hello world from debug");
295            info!("hello world from info");
296            warn!("hello world from warn");
297            error!("hello world from error");
298            let span = info_span!(
299                "test_span",
300                person.firstname = "cole",
301                person.lastname = "mackenzie",
302                later = tracing::field::Empty,
303            );
304            span.in_scope(|| {
305                info!("some message from inside a info_span");
306                let inner = info_span!("inner_span", a = "b", c = "d", inner_span = true);
307                inner.in_scope(|| {
308                    info!(
309                        inner_span_field = true,
310                        later = "populated from inside a span",
311                        "some message from inside a info_span",
312                    );
313                });
314            });
315        });
316
317        let subscriber = tracing_subscriber::registry().with(
318            builder()
319                .with_level_name("severity")
320                .with_level_value_casing(Casing::Uppercase)
321                .with_message_name("msg")
322                .with_timestamp_name("ts")
323                .with_timestamp_format(TimestampFormat::Unix)
324                .with_flatten_fields(false)
325                .layer(),
326        );
327
328        tracing::subscriber::with_default(subscriber, || {
329            trace!(a = "b", "hello world from trace");
330            debug!("hello world from debug");
331            info!("hello world from info");
332            warn!("hello world from warn");
333            error!("hello world from error");
334            let span = info_span!(
335                "test_span",
336                person.firstname = "cole",
337                person.lastname = "mackenzie",
338                later = tracing::field::Empty,
339            );
340            span.in_scope(|| {
341                info!("some message from inside a info_span");
342                let inner = info_span!("inner_span", a = "b", c = "d", inner_span = true);
343                inner.in_scope(|| {
344                    info!(
345                        inner_span_field = true,
346                        later = "populated from inside a span",
347                        "some message from inside a info_span",
348                    );
349                });
350            });
351        });
352    }
353
354    #[test]
355    fn test_nested_spans() {
356        let subscriber = tracing_subscriber::registry().with(builder().layer());
357
358        tracing::subscriber::with_default(subscriber, || {
359            let span = info_span!(
360                "test_span",
361                person.firstname = "cole",
362                person.lastname = "mackenzie",
363                later = tracing::field::Empty,
364            );
365            span.in_scope(|| {
366                info!("some message from inside a info_span");
367                let inner = info_span!("inner_span", a = "b", c = "d", inner_span = true);
368                inner.in_scope(|| {
369                    info!(
370                        inner_span_field = true,
371                        later = "populated from inside a span",
372                        "some message from inside a info_span",
373                    );
374                });
375            });
376
377            some_function(1, 2);
378        });
379    }
380}