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