env_logger/fmt/
mod.rs

1//! Formatting for log records.
2//!
3//! This module contains a [`Formatter`] that can be used to format log records
4//! into without needing temporary allocations. Usually you won't need to worry
5//! about the contents of this module and can use the `Formatter` like an ordinary
6//! [`Write`].
7//!
8//! # Formatting log records
9//!
10//! The format used to print log records can be customised using the [`Builder::format`]
11//! method.
12//!
13//! Terminal styling is done through ANSI escape codes and will be adapted to the capabilities of
14//! the target stream.s
15//!
16//! For example, you could use one of:
17//! - [anstyle](https://docs.rs/anstyle) is a minimal, runtime string styling API and is re-exported as [`style`]
18//! - [owo-colors](https://docs.rs/owo-colors) is a feature rich runtime string styling API
19//! - [color-print](https://docs.rs/color-print) for feature-rich compile-time styling API
20//!
21//! See also [`Formatter::default_level_style`]
22//!
23//! ```
24//! use std::io::Write;
25//!
26//! let mut builder = env_logger::Builder::new();
27//!
28//! builder.format(|buf, record| {
29//!     writeln!(buf, "{}: {}",
30//!         record.level(),
31//!         record.args())
32//! });
33//! ```
34//!
35//! # Key Value arguments
36//!
37//! If the `unstable-kv` feature is enabled, then the default format will include key values from
38//! the log by default, but this can be disabled by calling [`Builder::format_key_values`]
39//! with [`hidden_kv_format`] as the format function.
40//!
41//! The way these keys and values are formatted can also be customized with a separate format
42//! function that is called by the default format with [`Builder::format_key_values`].
43//!
44//! ```
45//! # #[cfg(feature= "unstable-kv")]
46//! # {
47//! use log::info;
48//! env_logger::init();
49//! info!(x="45"; "Some message");
50//! info!(x="12"; "Another message {x}", x="12");
51//! # }
52//! ```
53//!
54//! See <https://docs.rs/log/latest/log/#structured-logging>.
55//!
56//! [`Builder::format`]: crate::Builder::format
57//! [`Write`]: std::io::Write
58//! [`Builder::format_key_values`]: crate::Builder::format_key_values
59
60use std::cell::RefCell;
61use std::fmt::Display;
62use std::io::prelude::Write;
63use std::rc::Rc;
64use std::{fmt, io, mem};
65
66#[cfg(feature = "color")]
67use log::Level;
68use log::Record;
69
70#[cfg(feature = "humantime")]
71mod humantime;
72#[cfg(feature = "unstable-kv")]
73mod kv;
74pub(crate) mod writer;
75
76#[cfg(feature = "color")]
77pub use anstyle as style;
78
79#[cfg(feature = "humantime")]
80pub use self::humantime::Timestamp;
81#[cfg(feature = "unstable-kv")]
82pub use self::kv::*;
83pub use self::writer::Target;
84pub use self::writer::WriteStyle;
85
86use self::writer::{Buffer, Writer};
87
88/// Formatting precision of timestamps.
89///
90/// Seconds give precision of full seconds, milliseconds give thousands of a
91/// second (3 decimal digits), microseconds are millionth of a second (6 decimal
92/// digits) and nanoseconds are billionth of a second (9 decimal digits).
93#[allow(clippy::exhaustive_enums)] // compatibility
94#[derive(Copy, Clone, Debug)]
95pub enum TimestampPrecision {
96    /// Full second precision (0 decimal digits)
97    Seconds,
98    /// Millisecond precision (3 decimal digits)
99    Millis,
100    /// Microsecond precision (6 decimal digits)
101    Micros,
102    /// Nanosecond precision (9 decimal digits)
103    Nanos,
104}
105
106/// The default timestamp precision is seconds.
107impl Default for TimestampPrecision {
108    fn default() -> Self {
109        TimestampPrecision::Seconds
110    }
111}
112
113/// A formatter to write logs into.
114///
115/// `Formatter` implements the standard [`Write`] trait for writing log records.
116/// It also supports terminal styling using ANSI escape codes.
117///
118/// # Examples
119///
120/// Use the [`writeln`] macro to format a log record.
121/// An instance of a `Formatter` is passed to an `env_logger` format as `buf`:
122///
123/// ```
124/// use std::io::Write;
125///
126/// let mut builder = env_logger::Builder::new();
127///
128/// builder.format(|buf, record| writeln!(buf, "{}: {}", record.level(), record.args()));
129/// ```
130///
131/// [`Write`]: std::io::Write
132/// [`writeln`]: std::writeln
133pub struct Formatter {
134    buf: Rc<RefCell<Buffer>>,
135    write_style: WriteStyle,
136}
137
138impl Formatter {
139    pub(crate) fn new(writer: &Writer) -> Self {
140        Formatter {
141            buf: Rc::new(RefCell::new(writer.buffer())),
142            write_style: writer.write_style(),
143        }
144    }
145
146    pub(crate) fn write_style(&self) -> WriteStyle {
147        self.write_style
148    }
149
150    pub(crate) fn print(&self, writer: &Writer) -> io::Result<()> {
151        writer.print(&self.buf.borrow())
152    }
153
154    pub(crate) fn clear(&mut self) {
155        self.buf.borrow_mut().clear();
156    }
157}
158
159#[cfg(feature = "color")]
160impl Formatter {
161    /// Get the default [`style::Style`] for the given level.
162    ///
163    /// The style can be used to print other values besides the level.
164    ///
165    /// See [`style`] for how to adapt it to the styling crate of your choice
166    pub fn default_level_style(&self, level: Level) -> style::Style {
167        if self.write_style == WriteStyle::Never {
168            style::Style::new()
169        } else {
170            match level {
171                Level::Trace => style::AnsiColor::Cyan.on_default(),
172                Level::Debug => style::AnsiColor::Blue.on_default(),
173                Level::Info => style::AnsiColor::Green.on_default(),
174                Level::Warn => style::AnsiColor::Yellow.on_default(),
175                Level::Error => style::AnsiColor::Red
176                    .on_default()
177                    .effects(style::Effects::BOLD),
178            }
179        }
180    }
181}
182
183impl Write for Formatter {
184    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
185        self.buf.borrow_mut().write(buf)
186    }
187
188    fn flush(&mut self) -> io::Result<()> {
189        self.buf.borrow_mut().flush()
190    }
191}
192
193impl fmt::Debug for Formatter {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        let buf = self.buf.borrow();
196        f.debug_struct("Formatter")
197            .field("buf", &buf)
198            .field("write_style", &self.write_style)
199            .finish()
200    }
201}
202
203pub(crate) type FormatFn = Box<dyn Fn(&mut Formatter, &Record<'_>) -> io::Result<()> + Sync + Send>;
204
205pub(crate) struct Builder {
206    pub(crate) format_timestamp: Option<TimestampPrecision>,
207    pub(crate) format_module_path: bool,
208    pub(crate) format_target: bool,
209    pub(crate) format_level: bool,
210    pub(crate) format_indent: Option<usize>,
211    pub(crate) custom_format: Option<FormatFn>,
212    pub(crate) format_suffix: &'static str,
213    pub(crate) format_file: bool,
214    pub(crate) format_line_number: bool,
215    #[cfg(feature = "unstable-kv")]
216    pub(crate) kv_format: Option<Box<KvFormatFn>>,
217    built: bool,
218}
219
220impl Builder {
221    /// Convert the format into a callable function.
222    ///
223    /// If the `custom_format` is `Some`, then any `default_format` switches are ignored.
224    /// If the `custom_format` is `None`, then a default format is returned.
225    /// Any `default_format` switches set to `false` won't be written by the format.
226    pub(crate) fn build(&mut self) -> FormatFn {
227        assert!(!self.built, "attempt to re-use consumed builder");
228
229        let built = mem::replace(
230            self,
231            Builder {
232                built: true,
233                ..Default::default()
234            },
235        );
236
237        if let Some(fmt) = built.custom_format {
238            fmt
239        } else {
240            Box::new(move |buf, record| {
241                let fmt = DefaultFormat {
242                    timestamp: built.format_timestamp,
243                    module_path: built.format_module_path,
244                    target: built.format_target,
245                    level: built.format_level,
246                    written_header_value: false,
247                    indent: built.format_indent,
248                    suffix: built.format_suffix,
249                    source_file: built.format_file,
250                    source_line_number: built.format_line_number,
251                    #[cfg(feature = "unstable-kv")]
252                    kv_format: built.kv_format.as_deref().unwrap_or(&default_kv_format),
253                    buf,
254                };
255
256                fmt.write(record)
257            })
258        }
259    }
260}
261
262impl Default for Builder {
263    fn default() -> Self {
264        Builder {
265            format_timestamp: Some(Default::default()),
266            format_module_path: false,
267            format_target: true,
268            format_level: true,
269            format_file: false,
270            format_line_number: false,
271            format_indent: Some(4),
272            custom_format: None,
273            format_suffix: "\n",
274            #[cfg(feature = "unstable-kv")]
275            kv_format: None,
276            built: false,
277        }
278    }
279}
280
281#[cfg(feature = "color")]
282type SubtleStyle = StyledValue<&'static str>;
283#[cfg(not(feature = "color"))]
284type SubtleStyle = &'static str;
285
286/// A value that can be printed using the given styles.
287#[cfg(feature = "color")]
288struct StyledValue<T> {
289    style: style::Style,
290    value: T,
291}
292
293#[cfg(feature = "color")]
294impl<T: Display> Display for StyledValue<T> {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        let style = self.style;
297
298        // We need to make sure `f`s settings don't get passed onto the styling but do get passed
299        // to the value
300        write!(f, "{style}")?;
301        self.value.fmt(f)?;
302        write!(f, "{style:#}")?;
303        Ok(())
304    }
305}
306
307#[cfg(not(feature = "color"))]
308type StyledValue<T> = T;
309
310/// The default format.
311///
312/// This format needs to work with any combination of crate features.
313struct DefaultFormat<'a> {
314    timestamp: Option<TimestampPrecision>,
315    module_path: bool,
316    target: bool,
317    level: bool,
318    source_file: bool,
319    source_line_number: bool,
320    written_header_value: bool,
321    indent: Option<usize>,
322    buf: &'a mut Formatter,
323    suffix: &'a str,
324    #[cfg(feature = "unstable-kv")]
325    kv_format: &'a KvFormatFn,
326}
327
328impl DefaultFormat<'_> {
329    fn write(mut self, record: &Record<'_>) -> io::Result<()> {
330        self.write_timestamp()?;
331        self.write_level(record)?;
332        self.write_module_path(record)?;
333        self.write_source_location(record)?;
334        self.write_target(record)?;
335        self.finish_header()?;
336
337        self.write_args(record)?;
338        #[cfg(feature = "unstable-kv")]
339        self.write_kv(record)?;
340        write!(self.buf, "{}", self.suffix)
341    }
342
343    fn subtle_style(&self, text: &'static str) -> SubtleStyle {
344        #[cfg(feature = "color")]
345        {
346            StyledValue {
347                style: if self.buf.write_style == WriteStyle::Never {
348                    style::Style::new()
349                } else {
350                    style::AnsiColor::BrightBlack.on_default()
351                },
352                value: text,
353            }
354        }
355        #[cfg(not(feature = "color"))]
356        {
357            text
358        }
359    }
360
361    fn write_header_value<T>(&mut self, value: T) -> io::Result<()>
362    where
363        T: Display,
364    {
365        if !self.written_header_value {
366            self.written_header_value = true;
367
368            let open_brace = self.subtle_style("[");
369            write!(self.buf, "{open_brace}{value}")
370        } else {
371            write!(self.buf, " {value}")
372        }
373    }
374
375    fn write_level(&mut self, record: &Record<'_>) -> io::Result<()> {
376        if !self.level {
377            return Ok(());
378        }
379
380        let level = {
381            let level = record.level();
382            #[cfg(feature = "color")]
383            {
384                StyledValue {
385                    style: self.buf.default_level_style(level),
386                    value: level,
387                }
388            }
389            #[cfg(not(feature = "color"))]
390            {
391                level
392            }
393        };
394
395        self.write_header_value(format_args!("{level:<5}"))
396    }
397
398    fn write_timestamp(&mut self) -> io::Result<()> {
399        #[cfg(feature = "humantime")]
400        {
401            use self::TimestampPrecision::{Micros, Millis, Nanos, Seconds};
402            let ts = match self.timestamp {
403                None => return Ok(()),
404                Some(Seconds) => self.buf.timestamp_seconds(),
405                Some(Millis) => self.buf.timestamp_millis(),
406                Some(Micros) => self.buf.timestamp_micros(),
407                Some(Nanos) => self.buf.timestamp_nanos(),
408            };
409
410            self.write_header_value(ts)
411        }
412        #[cfg(not(feature = "humantime"))]
413        {
414            // Trick the compiler to think we have used self.timestamp
415            // Workaround for "field is never used: `timestamp`" compiler nag.
416            let _ = self.timestamp;
417            Ok(())
418        }
419    }
420
421    fn write_module_path(&mut self, record: &Record<'_>) -> io::Result<()> {
422        if !self.module_path {
423            return Ok(());
424        }
425
426        if let Some(module_path) = record.module_path() {
427            self.write_header_value(module_path)
428        } else {
429            Ok(())
430        }
431    }
432
433    fn write_source_location(&mut self, record: &Record<'_>) -> io::Result<()> {
434        if !self.source_file {
435            return Ok(());
436        }
437
438        if let Some(file_path) = record.file() {
439            let line = self.source_line_number.then(|| record.line()).flatten();
440            match line {
441                Some(line) => self.write_header_value(format_args!("{file_path}:{line}")),
442                None => self.write_header_value(file_path),
443            }
444        } else {
445            Ok(())
446        }
447    }
448
449    fn write_target(&mut self, record: &Record<'_>) -> io::Result<()> {
450        if !self.target {
451            return Ok(());
452        }
453
454        match record.target() {
455            "" => Ok(()),
456            target => self.write_header_value(target),
457        }
458    }
459
460    fn finish_header(&mut self) -> io::Result<()> {
461        if self.written_header_value {
462            let close_brace = self.subtle_style("]");
463            write!(self.buf, "{close_brace} ")
464        } else {
465            Ok(())
466        }
467    }
468
469    fn write_args(&mut self, record: &Record<'_>) -> io::Result<()> {
470        match self.indent {
471            // Fast path for no indentation
472            None => write!(self.buf, "{}", record.args()),
473
474            Some(indent_count) => {
475                // Create a wrapper around the buffer only if we have to actually indent the message
476
477                struct IndentWrapper<'a, 'b> {
478                    fmt: &'a mut DefaultFormat<'b>,
479                    indent_count: usize,
480                }
481
482                impl Write for IndentWrapper<'_, '_> {
483                    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
484                        let mut first = true;
485                        for chunk in buf.split(|&x| x == b'\n') {
486                            if !first {
487                                write!(
488                                    self.fmt.buf,
489                                    "{}{:width$}",
490                                    self.fmt.suffix,
491                                    "",
492                                    width = self.indent_count
493                                )?;
494                            }
495                            self.fmt.buf.write_all(chunk)?;
496                            first = false;
497                        }
498
499                        Ok(buf.len())
500                    }
501
502                    fn flush(&mut self) -> io::Result<()> {
503                        self.fmt.buf.flush()
504                    }
505                }
506
507                // The explicit scope here is just to make older versions of Rust happy
508                {
509                    let mut wrapper = IndentWrapper {
510                        fmt: self,
511                        indent_count,
512                    };
513                    write!(wrapper, "{}", record.args())?;
514                }
515
516                Ok(())
517            }
518        }
519    }
520
521    #[cfg(feature = "unstable-kv")]
522    fn write_kv(&mut self, record: &Record<'_>) -> io::Result<()> {
523        let format = self.kv_format;
524        format(self.buf, record.key_values())
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    use log::{Level, Record};
533
534    fn write_record(record: Record<'_>, fmt: DefaultFormat<'_>) -> String {
535        let buf = fmt.buf.buf.clone();
536
537        fmt.write(&record).expect("failed to write record");
538
539        let buf = buf.borrow();
540        String::from_utf8(buf.as_bytes().to_vec()).expect("failed to read record")
541    }
542
543    fn write_target(target: &str, fmt: DefaultFormat<'_>) -> String {
544        write_record(
545            Record::builder()
546                .args(format_args!("log\nmessage"))
547                .level(Level::Info)
548                .file(Some("test.rs"))
549                .line(Some(144))
550                .module_path(Some("test::path"))
551                .target(target)
552                .build(),
553            fmt,
554        )
555    }
556
557    fn write(fmt: DefaultFormat<'_>) -> String {
558        write_target("", fmt)
559    }
560
561    fn formatter() -> Formatter {
562        let writer = writer::Builder::new()
563            .write_style(WriteStyle::Never)
564            .build();
565
566        Formatter::new(&writer)
567    }
568
569    #[test]
570    fn format_with_header() {
571        let mut f = formatter();
572
573        let written = write(DefaultFormat {
574            timestamp: None,
575            module_path: true,
576            target: false,
577            level: true,
578            source_file: false,
579            source_line_number: false,
580            #[cfg(feature = "unstable-kv")]
581            kv_format: &hidden_kv_format,
582            written_header_value: false,
583            indent: None,
584            suffix: "\n",
585            buf: &mut f,
586        });
587
588        assert_eq!("[INFO  test::path] log\nmessage\n", written);
589    }
590
591    #[test]
592    fn format_no_header() {
593        let mut f = formatter();
594
595        let written = write(DefaultFormat {
596            timestamp: None,
597            module_path: false,
598            target: false,
599            level: false,
600            source_file: false,
601            source_line_number: false,
602            #[cfg(feature = "unstable-kv")]
603            kv_format: &hidden_kv_format,
604            written_header_value: false,
605            indent: None,
606            suffix: "\n",
607            buf: &mut f,
608        });
609
610        assert_eq!("log\nmessage\n", written);
611    }
612
613    #[test]
614    fn format_indent_spaces() {
615        let mut f = formatter();
616
617        let written = write(DefaultFormat {
618            timestamp: None,
619            module_path: true,
620            target: false,
621            level: true,
622            source_file: false,
623            source_line_number: false,
624            #[cfg(feature = "unstable-kv")]
625            kv_format: &hidden_kv_format,
626            written_header_value: false,
627            indent: Some(4),
628            suffix: "\n",
629            buf: &mut f,
630        });
631
632        assert_eq!("[INFO  test::path] log\n    message\n", written);
633    }
634
635    #[test]
636    fn format_indent_zero_spaces() {
637        let mut f = formatter();
638
639        let written = write(DefaultFormat {
640            timestamp: None,
641            module_path: true,
642            target: false,
643            level: true,
644            source_file: false,
645            source_line_number: false,
646            #[cfg(feature = "unstable-kv")]
647            kv_format: &hidden_kv_format,
648            written_header_value: false,
649            indent: Some(0),
650            suffix: "\n",
651            buf: &mut f,
652        });
653
654        assert_eq!("[INFO  test::path] log\nmessage\n", written);
655    }
656
657    #[test]
658    fn format_indent_spaces_no_header() {
659        let mut f = formatter();
660
661        let written = write(DefaultFormat {
662            timestamp: None,
663            module_path: false,
664            target: false,
665            level: false,
666            source_file: false,
667            source_line_number: false,
668            #[cfg(feature = "unstable-kv")]
669            kv_format: &hidden_kv_format,
670            written_header_value: false,
671            indent: Some(4),
672            suffix: "\n",
673            buf: &mut f,
674        });
675
676        assert_eq!("log\n    message\n", written);
677    }
678
679    #[test]
680    fn format_suffix() {
681        let mut f = formatter();
682
683        let written = write(DefaultFormat {
684            timestamp: None,
685            module_path: false,
686            target: false,
687            level: false,
688            source_file: false,
689            source_line_number: false,
690            #[cfg(feature = "unstable-kv")]
691            kv_format: &hidden_kv_format,
692            written_header_value: false,
693            indent: None,
694            suffix: "\n\n",
695            buf: &mut f,
696        });
697
698        assert_eq!("log\nmessage\n\n", written);
699    }
700
701    #[test]
702    fn format_suffix_with_indent() {
703        let mut f = formatter();
704
705        let written = write(DefaultFormat {
706            timestamp: None,
707            module_path: false,
708            target: false,
709            level: false,
710            source_file: false,
711            source_line_number: false,
712            #[cfg(feature = "unstable-kv")]
713            kv_format: &hidden_kv_format,
714            written_header_value: false,
715            indent: Some(4),
716            suffix: "\n\n",
717            buf: &mut f,
718        });
719
720        assert_eq!("log\n\n    message\n\n", written);
721    }
722
723    #[test]
724    fn format_target() {
725        let mut f = formatter();
726
727        let written = write_target(
728            "target",
729            DefaultFormat {
730                timestamp: None,
731                module_path: true,
732                target: true,
733                level: true,
734                source_file: false,
735                source_line_number: false,
736                #[cfg(feature = "unstable-kv")]
737                kv_format: &hidden_kv_format,
738                written_header_value: false,
739                indent: None,
740                suffix: "\n",
741                buf: &mut f,
742            },
743        );
744
745        assert_eq!("[INFO  test::path target] log\nmessage\n", written);
746    }
747
748    #[test]
749    fn format_empty_target() {
750        let mut f = formatter();
751
752        let written = write(DefaultFormat {
753            timestamp: None,
754            module_path: true,
755            target: true,
756            level: true,
757            source_file: false,
758            source_line_number: false,
759            #[cfg(feature = "unstable-kv")]
760            kv_format: &hidden_kv_format,
761            written_header_value: false,
762            indent: None,
763            suffix: "\n",
764            buf: &mut f,
765        });
766
767        assert_eq!("[INFO  test::path] log\nmessage\n", written);
768    }
769
770    #[test]
771    fn format_no_target() {
772        let mut f = formatter();
773
774        let written = write_target(
775            "target",
776            DefaultFormat {
777                timestamp: None,
778                module_path: true,
779                target: false,
780                level: true,
781                source_file: false,
782                source_line_number: false,
783                #[cfg(feature = "unstable-kv")]
784                kv_format: &hidden_kv_format,
785                written_header_value: false,
786                indent: None,
787                suffix: "\n",
788                buf: &mut f,
789            },
790        );
791
792        assert_eq!("[INFO  test::path] log\nmessage\n", written);
793    }
794
795    #[test]
796    fn format_with_source_file_and_line_number() {
797        let mut f = formatter();
798
799        let written = write(DefaultFormat {
800            timestamp: None,
801            module_path: false,
802            target: false,
803            level: true,
804            source_file: true,
805            source_line_number: true,
806            #[cfg(feature = "unstable-kv")]
807            kv_format: &hidden_kv_format,
808            written_header_value: false,
809            indent: None,
810            suffix: "\n",
811            buf: &mut f,
812        });
813
814        assert_eq!("[INFO  test.rs:144] log\nmessage\n", written);
815    }
816
817    #[cfg(feature = "unstable-kv")]
818    #[test]
819    fn format_kv_default() {
820        let kvs = &[("a", 1u32), ("b", 2u32)][..];
821        let mut f = formatter();
822        let record = Record::builder()
823            .args(format_args!("log message"))
824            .level(Level::Info)
825            .module_path(Some("test::path"))
826            .key_values(&kvs)
827            .build();
828
829        let written = write_record(
830            record,
831            DefaultFormat {
832                timestamp: None,
833                module_path: false,
834                target: false,
835                level: true,
836                source_file: false,
837                source_line_number: false,
838                kv_format: &default_kv_format,
839                written_header_value: false,
840                indent: None,
841                suffix: "\n",
842                buf: &mut f,
843            },
844        );
845
846        assert_eq!("[INFO ] log message a=1 b=2\n", written);
847    }
848
849    #[cfg(feature = "unstable-kv")]
850    #[test]
851    fn format_kv_default_full() {
852        let kvs = &[("a", 1u32), ("b", 2u32)][..];
853        let mut f = formatter();
854        let record = Record::builder()
855            .args(format_args!("log\nmessage"))
856            .level(Level::Info)
857            .module_path(Some("test::path"))
858            .target("target")
859            .file(Some("test.rs"))
860            .line(Some(42))
861            .key_values(&kvs)
862            .build();
863
864        let written = write_record(
865            record,
866            DefaultFormat {
867                timestamp: None,
868                module_path: true,
869                target: true,
870                level: true,
871                source_file: true,
872                source_line_number: true,
873                kv_format: &default_kv_format,
874                written_header_value: false,
875                indent: None,
876                suffix: "\n",
877                buf: &mut f,
878            },
879        );
880
881        assert_eq!(
882            "[INFO  test::path test.rs:42 target] log\nmessage a=1 b=2\n",
883            written
884        );
885    }
886}