serif/
lib.rs

1// Copyright 2022-2025 Allen Wild
2// SPDX-License-Identifier: Apache-2.0
3//! Serif is an opinionated Rust tracing-subscriber configuration with a focus on readability.
4//! ## About
5//!
6//! Serif is my take on the best way to configure [`tracing-subscriber`] for use in command-line
7//! applications, with an emphasis on readability of the main log messages. The tracing span scope,
8//! event target, and additional metadata is all rendered with dimmed colors, making the main
9//! message stand out quickly. Or at least it does on the Solarized Dark colorscheme that I prefer.
10//!
11//! Serif uses [`EnvFilter`] for filtering using the `RUST_LOG` environment variable, with a default
12//! level of `INFO` if not otherwise configured.
13//!
14//! Serif sets up [`FmtSubscriber`] and [`EnvFilter`] in a unified configuration. Basically this is
15//! all to make my life easier migrating from [`env_logger`].
16//!
17//! ## Usage
18//!
19//! All you need is a single dependency in `Cargo.toml` and a single builder chain to set up the
20//! global default tracing subscriber.  For convenience, `serif` re-exports `tracing` and provides
21//! the common log macros in `serif::macros`.
22//!
23//! ```
24//! use serif::macros::*;
25//! use serif::tracing::Level;
26//!
27//! # fn do_stuff() {}
28//! fn main() {
29//!     serif::Config::new()            // create config builder
30//!         .with_default(Level::DEBUG) // the default otherwise is INFO
31//!         .init();                    // finalize and register with tracing
32//!     info!("Hello World!");
33//!     do_stuff();
34//!     debug!("Finished doing stuff");
35//! }
36//! ```
37//!
38//! For more advanced use-cases, Serif provides [`EventFormatter`] which implements
39//! [`FormatEvent`], and [`FieldFormatter`] which implements [`FormatFields`]. These objects can be
40//! passed to a [`SubscriberBuilder`] along with whatever other options are desired.
41//!
42//! ## ANSI Terminal Colors
43//!
44//! By default, Serif enables ANSI coloring when the output file descriptor (stdout or stderr) is
45//! a TTY and the environment variable `NO_COLOR` is either unset or empty. At the moment, the
46//! specific color styles are not customizable.
47//!
48//! A note to advanced users configuring a [`SubscriberBuilder`] manually: `EventFormatter` and
49//! `FieldFormatter` do not track whether ANSI colors are enabled directly, instead they obtain
50//! this from the [`Writer`] that's passed to various methods. Call
51//! [`SubscriberBuilder::with_ansi`] to configure coloring in custom usage.
52//!
53//! [`tracing-subscriber`]: https://lib.rs/crates/tracing-subscriber
54//! [`FmtSubscriber`]: tracing_subscriber::fmt::Subscriber
55//! [`EnvFilter`]: tracing_subscriber::EnvFilter
56//! [`env_logger`]: https://lib.rs/crates/env_logger
57//! [`SubscriberBuilder`]: tracing_subscriber::fmt::SubscriberBuilder
58//! [`SubscriberBuilder::with_ansi`]: tracing_subscriber::fmt::SubscriberBuilder::with_ansi
59
60#![warn(missing_docs)]
61#![warn(clippy::all)]
62
63use std::fmt;
64
65use jiff::{Timestamp, Zoned, tz::TimeZone};
66use nu_ansi_term::{Color, Style};
67use tracing_core::{Event, Level, Subscriber, field::Field};
68use tracing_log::NormalizeEvent;
69use tracing_subscriber::{
70    field::{MakeVisitor, Visit, VisitFmt, VisitOutput},
71    fmt::{FmtContext, FormatEvent, FormatFields, FormattedFields, format::Writer},
72    registry::LookupSpan,
73};
74
75#[cfg(feature = "re-exports")]
76pub use tracing;
77
78/// Convenient re-exports of macros from the [`tracing`] crate. This module is intended to be
79/// glob-imported like a prelude.
80#[cfg(feature = "re-exports")]
81pub mod macros {
82    #[doc(no_inline)]
83    pub use tracing::{
84        debug, debug_span, enabled, error, error_span, event, event_enabled, info, info_span, span,
85        span_enabled, trace, trace_span, warn, warn_span,
86    };
87}
88
89mod config;
90pub use config::{ColorMode, Config, Output};
91
92/// Extension trait for writing ANSI-styled messages.
93trait WriterExt: fmt::Write {
94    /// Whether or not ANSI formatting should be enabled.
95    ///
96    /// When this method returns `false`, calls to [`write_style`] will ignore the given style and
97    /// write plain output instead.
98    fn enable_ansi(&self) -> bool;
99
100    /// Write any `Display`-able type to this Writer, using the given `Style` if and only if
101    /// `enable_ansi` returns `true`.
102    fn write_style<S, T>(&mut self, style: S, value: T) -> fmt::Result
103    where
104        S: Into<Style>,
105        T: fmt::Display,
106    {
107        if self.enable_ansi() {
108            let style = style.into();
109            write!(self, "{}{}{}", style.prefix(), value, style.suffix())
110        } else {
111            write!(self, "{}", value)
112        }
113    }
114}
115
116impl WriterExt for Writer<'_> {
117    #[inline]
118    fn enable_ansi(&self) -> bool {
119        self.has_ansi_escapes()
120    }
121}
122
123/// Macro to call [`WriterExt::write_style`] with arbitrary format arguments.
124macro_rules! write_style {
125    ($writer:expr, $style:expr, $($arg:tt)*) => {
126        $writer.write_style($style, format_args!($($arg)*))
127    };
128}
129
130/// Serif's formatter for event and span metadata fields.
131///
132/// `FieldFormatter` is intended to be used with [`SubscriberBuilder::fmt_fields`] and is designed
133/// to work with [`EventFormatter`]'s output format.
134///
135/// `FieldFormatter` implements [`FormatFields`], though this isn't immediately clear in Rustdoc.
136/// Specifically, `FieldFormatter` implements [`MakeVisitor`], and [`FieldVisitor`] implements
137/// [`Visit`], [`VisitOutput`], and [`VisitFmt`]. Thanks to blanket impls in the
138/// [`tracing_subscriber`] crate, this means that `FieldFormatter` implements [`FormatFields`].
139///
140/// # Field Format
141/// If a field is named `message`, then it's printed in the default text style. All other fields
142/// are formatted in square brackets and dimmed text style like `[name=value]`. Padding is added on
143/// either side of the `message` field, but not around other fields.
144///
145/// [`SubscriberBuilder::fmt_fields`]: tracing_subscriber::fmt::SubscriberBuilder::fmt_fields
146#[derive(Clone)]
147pub struct FieldFormatter {
148    // reserve the right to add options in the future
149    _private: (),
150}
151
152impl FieldFormatter {
153    /// Create a new `FieldFormatter` with the default configuration.
154    pub fn new() -> Self {
155        Self { _private: () }
156    }
157}
158
159impl Default for FieldFormatter {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165impl fmt::Debug for FieldFormatter {
166    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
167        f.pad("FieldFormatter")
168    }
169}
170
171impl<'a> MakeVisitor<Writer<'a>> for FieldFormatter {
172    type Visitor = FieldVisitor<'a>;
173
174    fn make_visitor(&self, target: Writer<'a>) -> Self::Visitor {
175        FieldVisitor::new(target)
176    }
177}
178
179/// A type of field that's been visited. Implementation detail of [`FieldVisitor`].
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181enum FieldType {
182    None,
183    Message,
184    Other,
185}
186
187/// The visitor type used by [`FieldFormatter`]
188///
189/// If a field is named `message`, then it's printed in the default text style. All other fields
190/// are formatted in square brackets and dimmed text style like `[name=value]`. Padding is added on
191/// either side of the `message` field, but not around other fields. [`Error`] typed fields are
192/// rendered in dimmed red text.
193///
194/// [`Error`]: std::error::Error
195#[derive(Debug)]
196pub struct FieldVisitor<'a> {
197    writer: Writer<'a>,
198    result: fmt::Result,
199    last: FieldType,
200}
201
202impl<'a> FieldVisitor<'a> {
203    /// Create a new `FieldVisitor` with the given writer.
204    pub fn new(writer: Writer<'a>) -> Self {
205        Self { writer, result: Ok(()), last: FieldType::None }
206    }
207
208    /// Get the padding that should be prepended when visiting the message field
209    fn pad_for_message(&self) -> &'static str {
210        match self.last {
211            FieldType::None => "",
212            FieldType::Message | FieldType::Other => " ",
213        }
214    }
215
216    /// Get the padding that should be prepended when visiting a non-message field
217    fn pad_for_other(&self) -> &'static str {
218        match self.last {
219            FieldType::Message => " ",
220            FieldType::None | FieldType::Other => "",
221        }
222    }
223}
224
225impl Visit for FieldVisitor<'_> {
226    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
227        if self.result.is_err() {
228            return;
229        }
230
231        let name = field.name();
232        if name.starts_with("log.") {
233            // skip log metadata
234            return;
235        }
236
237        self.result = if name == "message" {
238            let pad = self.pad_for_message();
239            self.last = FieldType::Message;
240            write!(self.writer, "{pad}{value:?}")
241        } else {
242            let pad = self.pad_for_other();
243            self.last = FieldType::Other;
244            write_style!(self.writer, Style::default().dimmed(), "{pad}[{name}={value:?}]")
245        };
246    }
247
248    fn record_str(&mut self, field: &Field, value: &str) {
249        if field.name() == "message" {
250            // Usually the message gets visited by record_debug, presumably becuase it's
251            // a fmt::Arguments object from a format_args! macro, but just in case the message
252            // field ends up here, force using the Display impl to render without quotes.
253            self.record_debug(field, &format_args!("{value}"));
254        } else {
255            // Otherwise, delegate to record_debug as usual.
256            self.record_debug(field, &value);
257        }
258    }
259
260    fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
261        if self.result.is_err() {
262            return;
263        }
264
265        let name = field.name();
266        if name.starts_with("log.") {
267            // skip log metadata
268            return;
269        }
270
271        // Treat Errors like a non-message field, and make them red.
272        let pad = self.pad_for_other();
273        self.last = FieldType::Other;
274        self.result = write_style!(self.writer, Color::Red.dimmed(), "{pad}[{name}={value}]");
275    }
276}
277
278impl VisitOutput<fmt::Result> for FieldVisitor<'_> {
279    fn finish(self) -> fmt::Result {
280        self.result
281    }
282}
283
284impl VisitFmt for FieldVisitor<'_> {
285    fn writer(&mut self) -> &mut dyn fmt::Write {
286        &mut self.writer
287    }
288}
289
290/// The style of timestamp to be formatted for tracing events.
291///
292/// Format strings are used by [`jiff::fmt::strtime`], and local timezone handling is
293/// provided by the [`jiff`] crate.
294#[derive(Clone)]
295pub struct TimeFormat {
296    inner: InnerTimeFormat,
297}
298
299/// Private implementation for TimeFormat
300#[derive(Clone)]
301enum InnerTimeFormat {
302    None,
303    Local(Option<Box<str>>),
304    Utc(Option<Box<str>>),
305}
306
307impl Default for TimeFormat {
308    fn default() -> Self {
309        Self::local()
310    }
311}
312
313impl fmt::Debug for TimeFormat {
314    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
315        match &self.inner {
316            InnerTimeFormat::None => f.write_str("TimeFormat::None"),
317            InnerTimeFormat::Local(format) => write!(f, "TimeFormat::Local({format:?})"),
318            InnerTimeFormat::Utc(format) => write!(f, "TimeFormat::Utc({format:?})"),
319        }
320    }
321}
322
323impl TimeFormat {
324    /// RFC 3339 timestamp enclosed in square brackets, with offset.
325    pub const LOCAL_FORMAT: &'static str = "[%Y-%m-%dT%H:%M:%S%z]";
326
327    /// RFC 3339 timestamp enclosed in square brackets, with UTC (using 'Z' for the timezone
328    /// instead of '+0000')
329    pub const UTC_FORMAT: &'static str = "[%Y-%m-%dT%H:%M:%SZ]";
330
331    /// Do not render a timestamp.
332    pub const fn none() -> Self {
333        Self { inner: InnerTimeFormat::None }
334    }
335
336    /// Render a timestamp in the local timezone using the default format.
337    pub const fn local() -> Self {
338        Self { inner: InnerTimeFormat::Local(None) }
339    }
340
341    /// Render a timestamp in UTC using the default format.
342    pub const fn utc() -> Self {
343        Self { inner: InnerTimeFormat::Utc(None) }
344    }
345
346    /// Render a timestamp in the local timezone using a custom format.
347    ///
348    /// **Panics:** When `debug_assertions` are enabled, the format string is validated to ensure
349    /// that no unknown `%` fields are present. In release mode, formatting the timestamp fails and
350    /// tracing-subscriber will emit "Unable to format the following event" messages.
351    pub fn local_custom(format: impl Into<String>) -> Self {
352        let format = format.into();
353
354        #[cfg(debug_assertions)]
355        {
356            let zoned = Zoned::new(Timestamp::UNIX_EPOCH, TimeZone::UTC);
357            let res = jiff::fmt::strtime::format(format.as_bytes(), &zoned);
358            if let Err(err) = res {
359                panic!("Unable to use custom TimeFormat '{format}': {err}");
360            }
361        }
362
363        Self { inner: InnerTimeFormat::Local(Some(format.into_boxed_str())) }
364    }
365
366    /// Render a timestamp in UTC using a custom format.
367    ///
368    /// **Panics:** When `debug_assertions` are enabled, the format string is validated to ensure
369    /// that no unknown `%` fields are present. In release mode, formatting the timestamp fails and
370    /// tracing-subscriber will emit "Unable to format the following event" messages.
371    pub fn utc_custom(format: impl Into<String>) -> Self {
372        let format = format.into();
373
374        #[cfg(debug_assertions)]
375        {
376            let res = jiff::fmt::strtime::format(format.as_bytes(), Timestamp::UNIX_EPOCH);
377            if let Err(err) = res {
378                panic!("Unable to use custom TimeFormat '{format}': {err}");
379            }
380        }
381
382        Self { inner: InnerTimeFormat::Utc(Some(format.into_boxed_str())) }
383    }
384
385    /// Get a [`Display`]-able object of this format applied to a `Timestamp`.
386    ///
387    /// [`Display`]: std::fmt::Display
388    pub fn render(&self, ts: Timestamp) -> impl fmt::Display + '_ {
389        TimeDisplay(self, ts)
390    }
391
392    /// Render the current system time in this format
393    pub fn render_now(&self) -> impl fmt::Display + '_ {
394        self.render(Timestamp::now())
395    }
396
397    fn is_none(&self) -> bool {
398        matches!(self.inner, InnerTimeFormat::None)
399    }
400}
401
402/// Helper to format a timestamp easily using Display
403struct TimeDisplay<'a>(&'a TimeFormat, Timestamp);
404
405impl fmt::Display for TimeDisplay<'_> {
406    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
407        match &self.0.inner {
408            InnerTimeFormat::None => Ok(()),
409            InnerTimeFormat::Local(format) => {
410                let format = format.as_deref().unwrap_or(TimeFormat::LOCAL_FORMAT);
411                let zoned = Zoned::new(self.1, TimeZone::system());
412                let disp = zoned.strftime(format.as_bytes());
413                fmt::Display::fmt(&disp, f)
414            }
415            InnerTimeFormat::Utc(format) => {
416                let format = format.as_deref().unwrap_or(TimeFormat::UTC_FORMAT);
417                let disp = self.1.strftime(format.as_bytes());
418                fmt::Display::fmt(&disp, f)
419            }
420        }
421    }
422}
423
424/// Serif's tracing event formatter.
425///
426/// # Event Format
427/// Events are rendered similarly to [`tracing_subscriber::fmt::format::Full`], but with everything
428/// besides the main log message in dimmed ANSI text colors to increase readability of the main log
429/// message.
430#[derive(Debug, Clone)]
431pub struct EventFormatter {
432    time_format: TimeFormat,
433    display_target: bool,
434    display_scope: bool,
435}
436
437impl EventFormatter {
438    /// Create a new `EventFormatter` with the default options.
439    pub fn new() -> Self {
440        Self { time_format: Default::default(), display_target: true, display_scope: true }
441    }
442
443    /// Set the timestamp format for this event formatter.
444    pub fn with_timestamp(self, time_format: TimeFormat) -> Self {
445        Self { time_format, ..self }
446    }
447
448    /// Set whether or not an event's target is displayed.
449    pub fn with_target(self, display_target: bool) -> Self {
450        Self { display_target, ..self }
451    }
452
453    /// Set whether or not an event's span scope is displayed.
454    pub fn with_scope(self, display_scope: bool) -> Self {
455        Self { display_scope, ..self }
456    }
457}
458
459impl Default for EventFormatter {
460    fn default() -> Self {
461        Self::new()
462    }
463}
464
465impl<S, N> FormatEvent<S, N> for EventFormatter
466where
467    S: Subscriber + for<'a> LookupSpan<'a>,
468    N: for<'a> FormatFields<'a> + 'static,
469{
470    fn format_event(
471        &self,
472        ctx: &FmtContext<'_, S, N>,
473        mut writer: Writer<'_>,
474        event: &Event<'_>,
475    ) -> fmt::Result {
476        // normalize event metadata in case this even was a log message
477        let norm_meta = event.normalized_metadata();
478        let meta = norm_meta.as_ref().unwrap_or_else(|| event.metadata());
479
480        // display the timestamp
481        if !self.time_format.is_none() {
482            write_style!(writer, Style::default().dimmed(), "{} ", self.time_format.render_now(),)?;
483        }
484
485        // display the level
486        let level = *meta.level();
487        let level_style = match level {
488            Level::TRACE => Color::Purple,
489            Level::DEBUG => Color::Blue,
490            Level::INFO => Color::Green,
491            Level::WARN => Color::Yellow,
492            Level::ERROR => Color::Red,
493        };
494        write_style!(writer, level_style, "{level:>5} ")?;
495
496        // display the span's scope
497        let maybe_scope = if self.display_scope { ctx.event_scope() } else { None };
498        if let Some(scope) = maybe_scope {
499            let mut seen = false;
500
501            for span in scope.from_root() {
502                writer.write_style(Color::Cyan.dimmed(), span.metadata().name())?;
503                seen = true;
504
505                if let Some(fields) = span.extensions().get::<FormattedFields<N>>() {
506                    if !fields.is_empty() {
507                        write!(writer, "{}:", fields)?;
508                    }
509                }
510            }
511
512            if seen {
513                writer.write_char(' ')?;
514            }
515        }
516
517        // display the target (which is the rust module path by default, but can be overridden)
518        if self.display_target {
519            write_style!(writer, Color::Blue.dimmed(), "{}", meta.target())?;
520            writer.write_str(": ")?;
521        }
522
523        // display the event message and fields
524        ctx.format_fields(writer.by_ref(), event)?;
525        writeln!(writer)
526    }
527}