Skip to main content

log_io/
format.rs

1//! Output formats.
2//!
3//! A [`Format`] turns a [`crate::Record`] into text on a
4//! [`core::fmt::Write`] destination. Three formats ship in the crate,
5//! each behind its own feature flag: [`JsonFormat`], [`LogfmtFormat`]
6//! and [`HumanFormat`]. Custom formats implement the trait, or wrap a
7//! closure with [`from_fn`].
8//!
9//! # Output safety
10//!
11//! The built-in formats share one escaping policy for untrusted text:
12//! C0 and C1 control characters, DEL, the Unicode line and paragraph
13//! separators (U+2028, U+2029) and the bidirectional override and
14//! isolate controls are always escaped. Every record therefore renders
15//! as exactly one line (except [`JsonFormat::pretty`]) and cannot
16//! inject terminal escape sequences, whatever the message, target, key
17//! or value contains.
18//!
19//! JSON and logfmt reserve the keys `timestamp`, `level`, `target`,
20//! `message`, `file` and `line`. A user field with one of those names
21//! is written as `fields.<name>` so it can never be mistaken for, or
22//! override, the record's real metadata.
23//!
24//! # Dynamic and static dispatch
25//!
26//! [`Format::write_record`] takes `&mut dyn fmt::Write`, which makes
27//! the trait object-safe: `Box<dyn Format>`, `&dyn Format` and
28//! `Arc<dyn Format>` all work, so a format can be chosen at run time.
29//! [`Format::write_record_generic`] is the statically dispatched
30//! variant the built-in sinks call. The built-in formats override it
31//! with a monomorphized implementation; measured on the reference
32//! machine, the `dyn` path costs roughly 40-60% more per record for
33//! the built-in formats because every small write becomes an indirect
34//! call.
35
36use core::fmt;
37
38use crate::record::Record;
39
40/// A record-to-text serializer.
41///
42/// Implementors write exactly one logical record per call, including
43/// any trailing newline: the built-in sinks add no separators between
44/// records. Formatters write to [`core::fmt::Write`] (not
45/// `std::io::Write`) so they work under `no_std` and can format into a
46/// `String` or a fixed buffer.
47///
48/// Only [`Format::write_record`] is required. Override
49/// [`Format::write_record_generic`] as well if your format is on a hot
50/// path and you want the built-in sinks to call it without dynamic
51/// dispatch.
52///
53/// # Examples
54///
55/// ```
56/// use core::fmt;
57/// use log_io::format::Format;
58/// use log_io::{Level, Metadata, Record};
59///
60/// struct Minimal;
61///
62/// impl Format for Minimal {
63///     fn write_record(&self, record: &Record<'_>, w: &mut dyn fmt::Write) -> fmt::Result {
64///         writeln!(w, "{} {}", record.metadata.level, record.message)
65///     }
66/// }
67///
68/// let record = Record::new(Metadata::new(Level::Info, "app"), "ready", &[]);
69/// let mut out = String::new();
70/// Minimal.write_record(&record, &mut out)?;
71/// assert_eq!(out, "info ready\n");
72///
73/// // The trait is object-safe, so formats can be picked at run time.
74/// let chosen: Box<dyn Format> = Box::new(Minimal);
75/// out.clear();
76/// chosen.write_record(&record, &mut out)?;
77/// assert_eq!(out, "info ready\n");
78/// # Ok::<(), fmt::Error>(())
79/// ```
80pub trait Format: Send + Sync {
81    /// Write `record` to `writer`.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`core::fmt::Error`] if the writer rejects a write (or
86    /// if a lazily formatted value's `Display` / `Debug` impl fails).
87    fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result;
88
89    /// Statically dispatched variant of [`Format::write_record`].
90    ///
91    /// The built-in sinks call this method. The default implementation
92    /// forwards to `write_record`; formats that care about the cost of
93    /// dynamic dispatch override it with a generic implementation.
94    ///
95    /// # Errors
96    ///
97    /// Same as [`Format::write_record`].
98    #[inline]
99    fn write_record_generic<W: fmt::Write>(
100        &self,
101        record: &Record<'_>,
102        writer: &mut W,
103    ) -> fmt::Result
104    where
105        Self: Sized,
106    {
107        self.write_record(record, writer)
108    }
109}
110
111impl<T: Format> Format for &T {
112    #[inline]
113    fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
114        (**self).write_record(record, writer)
115    }
116
117    #[inline]
118    fn write_record_generic<W: fmt::Write>(
119        &self,
120        record: &Record<'_>,
121        writer: &mut W,
122    ) -> fmt::Result {
123        (**self).write_record_generic(record, writer)
124    }
125}
126
127impl Format for &dyn Format {
128    #[inline]
129    fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
130        (**self).write_record(record, writer)
131    }
132}
133
134#[cfg(feature = "std")]
135impl<T: Format> Format for std::sync::Arc<T> {
136    #[inline]
137    fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
138        (**self).write_record(record, writer)
139    }
140
141    #[inline]
142    fn write_record_generic<W: fmt::Write>(
143        &self,
144        record: &Record<'_>,
145        writer: &mut W,
146    ) -> fmt::Result {
147        (**self).write_record_generic(record, writer)
148    }
149}
150
151#[cfg(feature = "std")]
152impl Format for std::sync::Arc<dyn Format> {
153    #[inline]
154    fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
155        (**self).write_record(record, writer)
156    }
157}
158
159#[cfg(feature = "std")]
160impl<T: Format> Format for Box<T> {
161    #[inline]
162    fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
163        (**self).write_record(record, writer)
164    }
165
166    #[inline]
167    fn write_record_generic<W: fmt::Write>(
168        &self,
169        record: &Record<'_>,
170        writer: &mut W,
171    ) -> fmt::Result {
172        (**self).write_record_generic(record, writer)
173    }
174}
175
176#[cfg(feature = "std")]
177impl Format for Box<dyn Format> {
178    #[inline]
179    fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
180        (**self).write_record(record, writer)
181    }
182}
183
184/// A [`Format`] built from a closure. Create one with [`from_fn`].
185#[derive(Clone, Copy)]
186pub struct FnFormat<F> {
187    f: F,
188}
189
190impl<F> fmt::Debug for FnFormat<F> {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.debug_struct("FnFormat").finish_non_exhaustive()
193    }
194}
195
196impl<F> Format for FnFormat<F>
197where
198    F: Fn(&Record<'_>, &mut dyn fmt::Write) -> fmt::Result + Send + Sync,
199{
200    #[inline]
201    fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
202        (self.f)(record, writer)
203    }
204}
205
206/// Use a closure as a [`Format`].
207///
208/// The closure receives the record and the destination writer and must
209/// write one complete record, including its trailing newline.
210///
211/// # Examples
212///
213/// ```
214/// use log_io::format::{self, Format};
215/// use log_io::{Level, Metadata, Record};
216///
217/// let csv = format::from_fn(|r: &Record<'_>, w: &mut dyn core::fmt::Write| {
218///     writeln!(w, "{},{},{}", r.metadata.level, r.metadata.target, r.message)
219/// });
220///
221/// let record = Record::new(Metadata::new(Level::Warn, "db"), "slow", &[]);
222/// let mut out = String::new();
223/// csv.write_record(&record, &mut out)?;
224/// assert_eq!(out, "warn,db,slow\n");
225/// # Ok::<(), core::fmt::Error>(())
226/// ```
227pub const fn from_fn<F>(f: F) -> FnFormat<F>
228where
229    F: Fn(&Record<'_>, &mut dyn fmt::Write) -> fmt::Result + Send + Sync,
230{
231    FnFormat { f }
232}
233
234#[cfg(any(feature = "json", feature = "logfmt", feature = "human"))]
235pub(crate) mod escape;
236
237#[cfg(any(feature = "json", feature = "human", feature = "logfmt"))]
238pub(crate) mod rfc3339;
239
240#[cfg(feature = "json")]
241mod json;
242#[cfg(feature = "json")]
243pub use self::json::JsonFormat;
244
245#[cfg(feature = "logfmt")]
246mod logfmt;
247#[cfg(feature = "logfmt")]
248pub use self::logfmt::LogfmtFormat;
249
250#[cfg(feature = "human")]
251mod human;
252#[cfg(all(feature = "human", feature = "std"))]
253pub use self::human::ColorChoice;
254#[cfg(feature = "human")]
255pub use self::human::HumanFormat;
256
257#[cfg(all(test, feature = "std"))]
258mod tests {
259    use super::*;
260    use crate::{Level, Metadata};
261
262    struct Upper;
263    impl Format for Upper {
264        fn write_record(&self, record: &Record<'_>, w: &mut dyn fmt::Write) -> fmt::Result {
265            writeln!(w, "{}", record.metadata.level.as_str_upper())
266        }
267    }
268
269    fn record() -> Record<'static> {
270        Record::new(Metadata::new(Level::Info, "t"), "m", &[])
271    }
272
273    fn render<F: Format>(f: &F) -> String {
274        let mut out = String::new();
275        f.write_record_generic(&record(), &mut out).unwrap();
276        let mut dyn_out = String::new();
277        f.write_record(&record(), &mut dyn_out).unwrap();
278        assert_eq!(out, dyn_out, "static and dynamic paths must agree");
279        out
280    }
281
282    #[test]
283    fn test_format_adapters_forward_to_inner() {
284        assert_eq!(render(&Upper), "INFO\n");
285        assert_eq!(render(&&Upper), "INFO\n");
286        assert_eq!(render(&Box::new(Upper)), "INFO\n");
287        assert_eq!(render(&std::sync::Arc::new(Upper)), "INFO\n");
288        let boxed: Box<dyn Format> = Box::new(Upper);
289        assert_eq!(render(&boxed), "INFO\n");
290        let arced: std::sync::Arc<dyn Format> = std::sync::Arc::new(Upper);
291        assert_eq!(render(&arced), "INFO\n");
292        let borrowed: &dyn Format = &Upper;
293        assert_eq!(render(&borrowed), "INFO\n");
294    }
295
296    #[test]
297    fn test_from_fn_closure_is_a_format() {
298        let f = from_fn(|r: &Record<'_>, w: &mut dyn fmt::Write| writeln!(w, "<{}>", r.message));
299        assert_eq!(render(&f), "<m>\n");
300        assert!(format!("{f:?}").starts_with("FnFormat"));
301    }
302
303    #[test]
304    fn test_format_errors_propagate() {
305        struct Failing;
306        impl fmt::Write for Failing {
307            fn write_str(&mut self, _: &str) -> fmt::Result {
308                Err(fmt::Error)
309            }
310        }
311        assert!(Upper.write_record_generic(&record(), &mut Failing).is_err());
312    }
313}