tracing_ndjson/lib.rs
1//! # tracing-ndjson
2//!
3//! [](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}