Skip to main content

tracing_logfmt/
formatter.rs

1pub(crate) mod builder;
2
3use std::fmt::{self, Write};
4
5use tracing::field::Visit;
6use tracing_core::{Event, Field, Subscriber};
7use tracing_subscriber::field::RecordFields;
8use tracing_subscriber::fmt::format::{self, FormatEvent, FormatFields};
9use tracing_subscriber::fmt::{FmtContext, FormattedFields};
10use tracing_subscriber::registry::LookupSpan;
11use tracing_subscriber::Layer;
12
13use crate::serializer::{Serializer, SerializerError};
14
15/// Creates a formatting layer
16///
17/// Example:
18/// ```rust
19/// use tracing::dispatcher::{self, Dispatch};
20/// use tracing_subscriber::Registry;
21/// use tracing_subscriber::layer::SubscriberExt;
22///
23/// let subscriber = Registry::default()
24///     .with(tracing_logfmt::layer());
25///
26/// dispatcher::set_global_default(Dispatch::new(subscriber))
27///     .expect("Global logger has already been set!");
28/// ```
29pub fn layer<S>() -> impl Layer<S>
30where
31    S: Subscriber + for<'a> LookupSpan<'a>,
32{
33    crate::builder().layer()
34}
35
36/// A formatter that formats tracing-subscriber events into logfmt formatted log rows.
37pub struct EventsFormatter {
38    pub(crate) with_level: bool,
39    pub(crate) with_target: bool,
40    pub(crate) with_span_name: bool,
41    pub(crate) with_span_path: bool,
42    pub(crate) with_location: bool,
43    pub(crate) with_module_path: bool,
44    pub(crate) with_timestamp: bool,
45    pub(crate) with_thread_names: bool,
46    pub(crate) with_thread_ids: bool,
47    #[cfg(feature = "ansi_logs")]
48    pub(crate) with_ansi_color: bool,
49}
50
51impl Default for EventsFormatter {
52    fn default() -> Self {
53        Self {
54            with_level: true,
55            with_target: true,
56            with_span_name: true,
57            with_span_path: true,
58            with_location: false,
59            with_module_path: false,
60            with_timestamp: true,
61            with_thread_names: false,
62            with_thread_ids: false,
63            #[cfg(feature = "ansi_logs")]
64            with_ansi_color: default_enable_ansi_color(),
65        }
66    }
67}
68
69#[cfg(feature = "ansi_logs")]
70fn default_enable_ansi_color() -> bool {
71    use std::io::IsTerminal;
72    std::io::stdout().is_terminal()
73}
74
75/// Gets the current thread id as an integer
76///
77/// This only supports the normal platforms here, I doubt this crate has wide usage,
78/// we just hard compile fail if targeting a platform we don't implement so
79/// that it is obvious
80///
81/// Note that a Rust `ThreadId` is actually just a process specific atomic,
82/// completely unrelated to the thread id assigned by the operating system, but
83/// we use the operating system one, at least until they stabilize
84/// <https://doc.rust-lang.org/std/thread/struct.ThreadId.html#method.as_u64>
85#[inline]
86#[allow(unsafe_code)]
87fn current_thread_id() -> u64 {
88    #[cfg(any(target_os = "linux", target_os = "android"))]
89    {
90        #[link(name = "c")]
91        unsafe extern "C" {
92            safe fn gettid() -> i32;
93        }
94
95        gettid() as _
96    }
97
98    #[cfg(target_os = "windows")]
99    {
100        #[link(name = "kernel32", kind = "raw-dylib")]
101        unsafe extern "system" {
102            safe fn GetCurrentThreadId() -> u32;
103        }
104
105        GetCurrentThreadId() as _
106    }
107
108    #[cfg(target_vendor = "apple")]
109    {
110        #[link(name = "c")]
111        unsafe extern "C" {
112            // technically isn't safe since it accepts a pointer, but we only
113            // have a single use that keeps the pointee on the stack
114            safe fn pthread_threadid_np(thread: usize, id: *mut u64) -> i32;
115        }
116
117        let mut id = 0;
118        if pthread_threadid_np(0, &mut id) != 0 {
119            id = 0;
120        }
121
122        id
123    }
124}
125
126impl<S, N> FormatEvent<S, N> for EventsFormatter
127where
128    S: Subscriber + for<'a> LookupSpan<'a>,
129    N: for<'a> FormatFields<'a> + 'static,
130{
131    fn format_event(
132        &self,
133        ctx: &FmtContext<'_, S, N>,
134        mut writer: format::Writer<'_>,
135        event: &Event<'_>,
136    ) -> fmt::Result {
137        let mut serializer = Serializer::new(
138            &mut writer,
139            #[cfg(feature = "ansi_logs")]
140            self.with_ansi_color,
141        );
142
143        let mut visit = || {
144            let metadata = event.metadata();
145
146            if self.with_timestamp {
147                serializer.serialize_key("ts")?;
148                serializer.writer.write_char('=')?;
149                time::OffsetDateTime::now_utc()
150                    .format_into(
151                        &mut serializer,
152                        &time::format_description::well_known::Rfc3339,
153                    )
154                    .map_err(|_e| fmt::Error)?;
155            }
156
157            if self.with_level {
158                let level = match *metadata.level() {
159                    tracing::Level::ERROR => "error",
160                    tracing::Level::WARN => "warn",
161                    tracing::Level::INFO => "info",
162                    tracing::Level::DEBUG => "debug",
163                    tracing::Level::TRACE => "trace",
164                };
165
166                #[cfg(feature = "ansi_logs")]
167                {
168                    if self.with_ansi_color {
169                        let level_str = match *metadata.level() {
170                            tracing::Level::ERROR => nu_ansi_term::Color::Red,
171                            tracing::Level::WARN => nu_ansi_term::Color::Yellow,
172                            tracing::Level::INFO => nu_ansi_term::Color::Green,
173                            tracing::Level::DEBUG => nu_ansi_term::Color::Blue,
174                            tracing::Level::TRACE => nu_ansi_term::Color::Purple,
175                        }
176                        .bold()
177                        .paint(level);
178
179                        serializer.serialize_entry("level", &level_str.to_string())?;
180                    } else {
181                        serializer.serialize_entry("level", level)?;
182                    }
183                }
184
185                #[cfg(not(feature = "ansi_logs"))]
186                serializer.serialize_entry("level", level)?;
187            }
188
189            if self.with_target {
190                serializer.serialize_entry("target", metadata.target())?;
191            }
192
193            // Use same logic as tracing-subscriber for thread names and ids
194            // https://github.com/tokio-rs/tracing/blob/efc690fa6bd1d9c3a57528b9bc8ac80504a7a6ed/tracing-subscriber/src/fmt/format/json.rs#L306
195            if self.with_thread_names {
196                if let Some(name) = std::thread::current().name() {
197                    serializer.serialize_entry("thread_name", name)?;
198                } else if !self.with_thread_ids {
199                    // fall-back to thread id when name is absent and ids are not enabled
200                    serializer
201                        .serialize_entry("thread_name", &format!("{}", current_thread_id()))?;
202                }
203            }
204
205            if self.with_thread_ids {
206                serializer.serialize_entry_no_quote("thread_id", current_thread_id())?;
207            }
208
209            let span = if self.with_span_name || self.with_span_path {
210                event
211                    .parent()
212                    .and_then(|id| ctx.span(id))
213                    .or_else(|| ctx.lookup_current())
214            } else {
215                None
216            };
217
218            if self.with_location {
219                if let (Some(file), Some(line)) = (metadata.file(), metadata.line()) {
220                    serializer.serialize_entry("location", &format!("{}:{}", file, line))?;
221                }
222            }
223            if self.with_module_path {
224                if let Some(module) = metadata.module_path() {
225                    serializer.serialize_entry("module_path", module)?;
226                }
227            }
228
229            if let Some(span) = span {
230                if self.with_span_name {
231                    serializer.serialize_entry("span", span.name())?;
232                }
233
234                if self.with_span_path {
235                    serializer.serialize_key("span_path")?;
236                    serializer.writer.write_char('=')?;
237
238                    let needs_quote = span
239                        .scope()
240                        .from_root()
241                        .any(|span| span.name().chars().any(crate::serializer::need_quote));
242
243                    // if none of the span names need to be quoted we can do things a bit faster
244                    if needs_quote {
245                        let mut required_capacity = 0;
246                        let mut insert_sep = false;
247                        for span in span.scope().from_root() {
248                            if insert_sep {
249                                required_capacity += 1;
250                            }
251                            required_capacity += span.name().len();
252                            insert_sep = true;
253                        }
254
255                        let mut span_path = String::with_capacity(required_capacity);
256                        let s = Serializer::new(
257                            &mut span_path,
258                            #[cfg(feature = "ansi_logs")]
259                            self.with_ansi_color,
260                        );
261                        let mut insert_sep = false;
262                        for span in span.scope().from_root() {
263                            if insert_sep {
264                                s.writer.write_char('>')?;
265                            }
266                            s.writer.write_str(span.name())?;
267                            insert_sep = true;
268                        }
269                        serializer.serialize_value(&span_path)?;
270                    } else {
271                        let mut insert_sep = false;
272                        for span in span.scope().from_root() {
273                            if insert_sep {
274                                serializer.writer.write_char('>')?;
275                            }
276                            serializer.writer.write_str(span.name())?;
277                            insert_sep = true;
278                        }
279                    }
280                }
281            }
282
283            let mut visitor = Visitor::new(&mut serializer);
284            event.record(&mut visitor);
285            visitor.state?;
286
287            Ok(())
288        };
289
290        visit().map_err(|_e: SerializerError| fmt::Error)?;
291
292        // Write all fields from spans
293        if let Some(leaf_span) = ctx.lookup_current() {
294            for span in leaf_span.scope().from_root() {
295                let ext = span.extensions();
296                let data = ext
297                    .get::<FormattedFields<N>>()
298                    .expect("Unable to find FormattedFields in extensions; this is a bug");
299
300                if !data.is_empty() {
301                    write!(writer, " ")?;
302                    write!(writer, "{}", data)?;
303                }
304            }
305        }
306
307        writeln!(writer)
308    }
309}
310
311/// A formatter that formats span fields into logfmt.
312#[derive(Default)]
313#[non_exhaustive]
314pub struct FieldsFormatter {}
315
316impl<'writer> FormatFields<'writer> for FieldsFormatter {
317    fn format_fields<R: RecordFields>(
318        &self,
319        mut writer: format::Writer<'writer>,
320        fields: R,
321    ) -> fmt::Result {
322        let mut serializer = Serializer::new(
323            &mut writer,
324            #[cfg(feature = "ansi_logs")]
325            false,
326        );
327        let mut visitor = Visitor::new(&mut serializer);
328        fields.record(&mut visitor);
329        Ok(())
330    }
331}
332
333struct Visitor<'a, W> {
334    serializer: &'a mut Serializer<W>,
335    state: Result<(), SerializerError>,
336    debug_fmt_buffer: String,
337}
338
339impl<'a, W> Visitor<'a, W> {
340    fn new(serializer: &'a mut Serializer<W>) -> Self {
341        Self {
342            serializer,
343            state: Ok(()),
344            debug_fmt_buffer: String::new(),
345        }
346    }
347}
348
349impl<'a, W> Visit for Visitor<'a, W>
350where
351    W: fmt::Write,
352{
353    fn record_f64(&mut self, field: &Field, value: f64) {
354        if self.state.is_ok() {
355            self.record_debug_no_quote(field, value);
356        }
357    }
358
359    fn record_i64(&mut self, field: &Field, value: i64) {
360        if self.state.is_ok() {
361            self.record_debug_no_quote(field, value);
362        }
363    }
364
365    fn record_u64(&mut self, field: &Field, value: u64) {
366        if self.state.is_ok() {
367            self.record_debug_no_quote(field, value);
368        }
369    }
370
371    fn record_bool(&mut self, field: &Field, value: bool) {
372        if self.state.is_ok() {
373            self.record_debug_no_quote(field, value);
374        }
375    }
376
377    fn record_str(&mut self, field: &Field, value: &str) {
378        if self.state.is_ok() {
379            self.state = self.serializer.serialize_entry(field.name(), value);
380        }
381    }
382
383    fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
384        if self.state.is_ok() {
385            self.record_debug(field, &format_args!("{}", value));
386        }
387    }
388
389    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
390        if self.state.is_ok() {
391            self.debug_fmt_buffer.clear();
392            let _ = write!(self.debug_fmt_buffer, "{:?}", value);
393            self.state = self
394                .serializer
395                .serialize_entry(field.name(), &self.debug_fmt_buffer);
396        }
397    }
398}
399
400impl<'a, W> Visitor<'a, W>
401where
402    W: fmt::Write,
403{
404    fn record_debug_no_quote(&mut self, field: &Field, value: impl fmt::Debug) {
405        if self.state.is_ok() {
406            self.state = self
407                .serializer
408                .serialize_entry_no_quote(field.name(), value);
409        }
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use std::{
416        io,
417        sync::{Arc, Mutex},
418    };
419
420    use tracing::info_span;
421    #[allow(unused_imports)]
422    use tracing_subscriber::fmt::{MakeWriter, SubscriberBuilder};
423
424    use super::*;
425
426    #[derive(Clone, Debug)]
427    struct MockWriter {
428        buf: Arc<Mutex<Vec<u8>>>,
429    }
430
431    #[derive(Clone, Debug)]
432    struct MockMakeWriter {
433        buf: Arc<Mutex<Vec<u8>>>,
434    }
435
436    impl MockMakeWriter {
437        fn new() -> Self {
438            Self {
439                buf: Arc::new(Mutex::new(Vec::new())),
440            }
441        }
442        fn get_content(&self) -> String {
443            let buf = self.buf.lock().unwrap();
444            std::str::from_utf8(&buf[..]).unwrap().to_owned()
445        }
446    }
447
448    impl MockWriter {
449        fn new(buf: Arc<Mutex<Vec<u8>>>) -> Self {
450            Self { buf }
451        }
452    }
453
454    impl io::Write for MockWriter {
455        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
456            self.buf.lock().unwrap().write(buf)
457        }
458
459        fn flush(&mut self) -> io::Result<()> {
460            self.buf.lock().unwrap().flush()
461        }
462    }
463
464    impl<'a> MakeWriter<'a> for MockMakeWriter {
465        type Writer = MockWriter;
466
467        fn make_writer(&'a self) -> Self::Writer {
468            MockWriter::new(self.buf.clone())
469        }
470    }
471
472    macro_rules! contains {
473        ($content:expr, [$($item:expr),+$(,)?], [$($neg:expr),*$(,)?]) => {
474            eprintln!("{}", $content);
475
476            $(
477                assert!($content.contains($item), "failed to find expected string '{}'", $item);
478            )+
479
480            $(
481                assert!(!$content.contains($neg), "found unexpected string '{}'", $neg);
482            )*
483        }
484    }
485
486    #[cfg(not(feature = "ansi_logs"))]
487    mod no_ansi {
488        use super::*;
489
490        fn subscriber() -> SubscriberBuilder<FieldsFormatter, EventsFormatter> {
491            builder::builder().subscriber_builder()
492        }
493
494        #[test]
495        fn enable_thread_name_and_id() {
496            use tracing::subscriber;
497
498            let mock_writer = MockMakeWriter::new();
499            let subscriber = builder::builder()
500                .with_thread_names(true)
501                .with_thread_ids(true)
502                .subscriber_builder()
503                .with_writer(mock_writer.clone())
504                .finish();
505
506            std::thread::Builder::new()
507                .name("worker-1".to_string())
508                .spawn(move || {
509                    subscriber::with_default(subscriber, || {
510                        tracing::info!("message");
511                    });
512                })
513                .unwrap()
514                .join()
515                .unwrap();
516
517            let content = mock_writer.get_content();
518            contains!(content, ["thread_name=worker-1", "thread_id="], []);
519        }
520
521        #[test]
522        fn span_and_span_path_with_quoting() {
523            use tracing::subscriber;
524
525            let mock_writer = MockMakeWriter::new();
526            let subscriber = subscriber().with_writer(mock_writer.clone()).finish();
527
528            subscriber::with_default(subscriber, || {
529                let _top = info_span!("top").entered();
530                // the ' ' requires quoting
531                let _middle = info_span!("mid dle").entered();
532                let _bottom = info_span!("bottom").entered();
533
534                tracing::info!("message");
535            });
536
537            let content = mock_writer.get_content();
538
539            contains!(
540                content,
541                [
542                    "span=bottom",
543                    "span_path=\"top>mid dle>bottom\"",
544                    "info",
545                    "ts=20"
546                ],
547                []
548            );
549        }
550
551        #[test]
552        fn span_and_span_path_without_quoting() {
553            use tracing::subscriber;
554
555            let mock_writer = MockMakeWriter::new();
556            let subscriber = subscriber().with_writer(mock_writer.clone()).finish();
557
558            subscriber::with_default(subscriber, || {
559                let _top = info_span!("top").entered();
560                let _middle = info_span!("middle").entered();
561                let _bottom = info_span!("bottom").entered();
562
563                tracing::info!("message");
564            });
565
566            let content = mock_writer.get_content();
567
568            contains!(
569                content,
570                [
571                    "span=bottom",
572                    "span_path=top>middle>bottom",
573                    "info",
574                    "ts=20"
575                ],
576                []
577            );
578        }
579
580        #[test]
581        fn disable_span_and_span_path() {
582            use tracing::subscriber;
583
584            let mock_writer = MockMakeWriter::new();
585            let subscriber = builder::builder()
586                .with_span_name(false)
587                .with_span_path(false)
588                .subscriber_builder()
589                .with_writer(mock_writer.clone())
590                .finish();
591
592            subscriber::with_default(subscriber, || {
593                let _top = info_span!("top").entered();
594                let _middle = info_span!("middle").entered();
595                let _bottom = info_span!("bottom").entered();
596
597                tracing::info!("message");
598            });
599
600            let content = mock_writer.get_content();
601
602            contains!(content, ["level=info", "ts=20"], ["span=", "span_path="]);
603        }
604
605        #[test]
606        fn enable_module_path() {
607            use tracing::subscriber;
608
609            let mock_writer = MockMakeWriter::new();
610            let subscriber = builder::builder()
611                .with_module_path(true)
612                .subscriber_builder()
613                .with_writer(mock_writer.clone())
614                .finish();
615
616            subscriber::with_default(subscriber, || {
617                let _top = info_span!("top").entered();
618                let _middle = info_span!("middle").entered();
619                let _bottom = info_span!("bottom").entered();
620
621                tracing::info!("message");
622            });
623
624            let content = mock_writer.get_content();
625
626            contains!(
627                content,
628                ["module_path=tracing_logfmt::formatter::tests", "info"],
629                []
630            );
631        }
632
633        #[test]
634        #[cfg(not(windows))]
635        fn enable_location() {
636            use tracing::subscriber;
637
638            let mock_writer = MockMakeWriter::new();
639            let subscriber = builder::builder()
640                .with_location(true)
641                .subscriber_builder()
642                .with_writer(mock_writer.clone())
643                .finish();
644
645            subscriber::with_default(subscriber, || {
646                let _top = info_span!("top").entered();
647                let _middle = info_span!("middle").entered();
648                let _bottom = info_span!("bottom").entered();
649
650                tracing::info!("message");
651            });
652
653            let content = mock_writer.get_content();
654            let split = content.split("location=src/formatter.rs:").last().unwrap();
655            let line = &split[..3];
656            assert!(line.parse::<u32>().is_ok());
657
658            contains!(content, ["location=src/formatter.rs:", "info"], []);
659        }
660
661        #[test]
662        #[cfg(windows)]
663        fn enable_location() {
664            use tracing::subscriber;
665
666            let mock_writer = MockMakeWriter::new();
667            let subscriber = builder::builder()
668                .with_location(true)
669                .subscriber_builder()
670                .with_writer(mock_writer.clone())
671                .finish();
672
673            subscriber::with_default(subscriber, || {
674                let _top = info_span!("top").entered();
675                let _middle = info_span!("middle").entered();
676                let _bottom = info_span!("bottom").entered();
677
678                tracing::info!("message");
679            });
680
681            let content = mock_writer.get_content();
682            let split = content.split(r"location=src\formatter.rs:").last().unwrap();
683            let line = &split[..3];
684            assert!(line.parse::<u32>().is_ok());
685
686            contains!(content, [r"location=src\formatter.rs:", "info"], []);
687        }
688    }
689
690    #[cfg(feature = "ansi_logs")]
691    mod ansi {
692        use super::*;
693
694        fn make_ansi_key_value(key: &str, value: &str) -> String {
695            use nu_ansi_term::Color;
696            let mut key = Color::Rgb(109, 139, 140).bold().paint(key).to_string();
697            key.push_str(value);
698            key
699        }
700
701        // We use this since locally you'll default to true for ansi colors, but
702        // CI will get auto-detected as not a terminal, disabling colors and failing tests
703        fn builder() -> builder::Builder {
704            builder::builder().with_ansi_color(true)
705        }
706
707        #[test]
708        fn disable_span_and_span_path() {
709            use nu_ansi_term::Color;
710            use tracing::subscriber;
711
712            let mock_writer = MockMakeWriter::new();
713            let subscriber = builder()
714                .with_span_name(false)
715                .with_span_path(false)
716                .subscriber_builder()
717                .with_writer(mock_writer.clone())
718                .finish();
719
720            subscriber::with_default(subscriber, || {
721                let _top = info_span!("top").entered();
722                let _middle = info_span!("middle").entered();
723                let _bottom = info_span!("bottom").entered();
724
725                tracing::info!("message");
726            });
727
728            let content = mock_writer.get_content();
729            let message = make_ansi_key_value("message", "=message");
730            let target = make_ansi_key_value("target", "=tracing_logfmt::formatter::tests");
731            let ts = make_ansi_key_value("ts", "=");
732
733            contains!(
734                content,
735                [
736                    &Color::Green.bold().paint("info").to_string(),
737                    &ts,
738                    &target,
739                    &message
740                ],
741                ["span=", "span_path="]
742            );
743        }
744
745        #[test]
746        fn span_and_span_path_without_quoting() {
747            use tracing::subscriber;
748
749            let mock_writer = MockMakeWriter::new();
750            let subscriber = builder()
751                .subscriber_builder()
752                .with_writer(mock_writer.clone())
753                .finish();
754
755            subscriber::with_default(subscriber, || {
756                let _top = info_span!("top").entered();
757                let _middle = info_span!("middle").entered();
758                let _bottom = info_span!("bottom").entered();
759
760                tracing::info!("message");
761            });
762
763            let content = mock_writer.get_content();
764
765            let span = make_ansi_key_value("span", "=bottom");
766            let span_path = make_ansi_key_value("span_path", "=top>middle>bottom");
767            let ts = make_ansi_key_value("ts", "=20");
768
769            contains!(content, [&span, &span_path, "info", &ts], []);
770        }
771
772        #[test]
773        fn span_and_span_path_with_quoting() {
774            use tracing::subscriber;
775
776            let mock_writer = MockMakeWriter::new();
777            let subscriber = builder()
778                .subscriber_builder()
779                .with_writer(mock_writer.clone())
780                .finish();
781
782            subscriber::with_default(subscriber, || {
783                let _top = info_span!("top").entered();
784                // the ' ' requires quoting
785                let _middle = info_span!("mid dle").entered();
786                let _bottom = info_span!("bottom").entered();
787
788                tracing::info!("message");
789            });
790
791            let content = mock_writer.get_content();
792
793            let span = make_ansi_key_value("span", "=bottom");
794            let span_path = make_ansi_key_value("span_path", "=\"top>mid dle>bottom\"");
795            let ts = make_ansi_key_value("ts", "=20");
796
797            contains!(content, [&span, &span_path, "info", &ts], []);
798        }
799
800        #[test]
801        fn enable_thread_name_and_id() {
802            use tracing::subscriber;
803
804            let mock_writer = MockMakeWriter::new();
805            let subscriber = builder()
806                .with_thread_names(true)
807                .with_thread_ids(true)
808                .subscriber_builder()
809                .with_writer(mock_writer.clone())
810                .finish();
811
812            std::thread::Builder::new()
813                .name("worker-1".to_string())
814                .spawn(move || {
815                    subscriber::with_default(subscriber, || {
816                        tracing::info!("message");
817                    });
818                })
819                .unwrap()
820                .join()
821                .unwrap();
822
823            let content = mock_writer.get_content();
824
825            let thread_name_prefix = make_ansi_key_value("thread_name", "=");
826            let thread_id_prefix = make_ansi_key_value("thread_id", "=");
827
828            contains!(
829                content,
830                [&format!("{thread_name_prefix}worker-1"), &thread_id_prefix],
831                []
832            );
833        }
834
835        #[test]
836        fn disable_ansi_color() {
837            use tracing::subscriber;
838
839            let mock_writer = MockMakeWriter::new();
840            let subscriber = builder()
841                // disable timestamp so it can be asserted
842                .with_timestamp(false)
843                .with_ansi_color(false)
844                .subscriber_builder()
845                .with_writer(mock_writer.clone())
846                .finish();
847
848            subscriber::with_default(subscriber, || {
849                tracing::info!("message");
850            });
851
852            let content = mock_writer.get_content();
853
854            // assert that there is no ansi color sequences
855            assert_eq!(
856                content,
857                "level=info target=tracing_logfmt::formatter::tests::ansi message=message\n"
858            );
859        }
860    }
861}