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#[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}