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}