tracing_collector/
lib.rs

1use std::{
2    fmt::{self},
3    io::{self},
4    mem,
5    sync::{Mutex, MutexGuard},
6};
7use tracing::{subscriber::DefaultGuard, Level};
8use tracing_subscriber::fmt::MakeWriter;
9use tracing_subscriber::util::SubscriberInitExt;
10
11/// `TracingCollector` creates a tracing subscriber that collects a copy of all traces into a buffer.
12/// These traces can be retrieved by calling its Display implementation, i.e. calling `log.to_string()` or `format!("{log}")`.
13/// This is useful for testing with [insta](https://crates.io/crates/insta) snapshots.
14///
15/// IMPORTANT! `TracingCollector` is meant for use when testing. It collects logs into a memory buffer
16/// which keeps growing until it is read, the program exits or it is dropped. This means that if you are using `TracingCollector`
17/// in production the program will eventually run out of memory.
18///
19/// When the `TracingCollector` is dropped, the buffer is emptied and the tracing subscriber is released but
20/// the memory equivalent of a Mutex and an empty Vec<u8> is leaked.
21///
22/// When reading the traces, they are stripped of ANSI escape codes and prefixed with a `㏒` character. The former allows
23/// the use of colored & formatted terminal output when the test fails or is run with `--nocapture` and the latter
24/// makes the insta inline snapshots work since rust's `r###` raw string literals strips leading whitespace. The prefix can be
25/// changed or removed using the `set_prefix` and `remove_prefix` methods.
26///
27/// Example:
28///
29/// ```rust
30/// #[test]
31/// fn test_logs() {
32///     let log = TracingCollector::init_debug_level();
33///     tracing::info!("First log");
34///
35///     insta::assert_display_snapshot!(log, @r###"
36///     ㏒   INFO  First log
37///         at tests/test.rs:6
38///
39///     "###);
40///
41///     tracing::debug!("Second log");
42///     tracing::info!("Third log");
43///
44///     insta::assert_display_snapshot!(log, @r###"
45///     ㏒  DEBUG  Second log
46///         at tests/test.rs:14
47///
48///       INFO  Third log
49///        at tests/test.rs:15
50///
51///    "###);
52///}
53/// ```
54pub struct TracingCollector {
55    buf: &'static Mutex<Vec<u8>>,
56    trace_guard: Mutex<Option<DefaultGuard>>,
57    prefix: Option<char>,
58}
59
60impl TracingCollector {
61    fn new() -> Self {
62        TracingCollector {
63            buf: Box::leak(Box::new(Mutex::new(vec![]))),
64            trace_guard: Mutex::new(None),
65            prefix: Some('㏒'),
66        }
67    }
68
69    pub fn set_prefix(&mut self, prefix: char) {
70        self.prefix = Some(prefix);
71    }
72
73    pub fn remove_prefix(&mut self) {
74        self.prefix = None;
75    }
76
77    fn set_guard(&self, trace_guard: DefaultGuard) {
78        let mut guard = self.trace_guard.lock().expect("failed to lock mutex");
79        *guard = Some(trace_guard);
80    }
81
82    /// Create a `TracingCollector` that collects traces up to the `TRACE` level.
83    pub fn init_trace_level() -> Self {
84        Self::init(Level::TRACE)
85    }
86
87    /// Create a `TracingCollector` that collects traces up to the `DEBUG` level.
88    pub fn init_debug_level() -> Self {
89        Self::init(Level::DEBUG)
90    }
91
92    /// Create a `TracingCollector` that collects traces up to the `INFO` level.
93    pub fn init_info_level() -> Self {
94        Self::init(Level::INFO)
95    }
96
97    /// Create a new `TracingCollector` that collects traces up to the specified level.
98    pub fn init(max_level: Level) -> Self {
99        let collector = TracingCollector::new();
100
101        let saver = CollectingWriter::new(&collector.buf);
102        let guard = tracing_subscriber::fmt()
103            .pretty()
104            .with_max_level(max_level)
105            .without_time()
106            .with_file(true)
107            .with_line_number(true)
108            .with_target(false)
109            .with_ansi(true)
110            .with_writer(saver)
111            .finish()
112            .set_default();
113
114        collector.set_guard(guard);
115        collector
116    }
117
118    pub fn clear(&self) {
119        self.buf.lock().expect("failed to lock mutex").clear();
120    }
121}
122
123impl fmt::Display for TracingCollector {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        let mut buf = vec![];
126        let mut guard = self.buf.lock().expect("failed to lock mutex");
127        mem::swap(&mut buf, &mut *guard);
128        let cleaned_buf = strip_ansi_escapes::strip(&*buf).expect("failed to strip ansi escapes");
129        let cleaned = String::from_utf8(cleaned_buf).expect("log contains invalid utf8");
130        if let Some(prefix) = self.prefix {
131            write!(f, "{prefix}{cleaned}",)
132        } else {
133            write!(f, "{cleaned}",)
134        }
135    }
136}
137
138impl Drop for TracingCollector {
139    fn drop(&mut self) {
140        let mut vec = self.buf.lock().expect("msg");
141        vec.clear();
142        // reduce the size of the vector as much as possible so that the
143        // leaked memory is only the size of a mutex and an empty vector
144        vec.shrink_to(0);
145    }
146}
147
148struct CollectingWriter<'a> {
149    buf: &'a Mutex<Vec<u8>>,
150}
151
152impl<'a> CollectingWriter<'a> {
153    /// Create a new `CollectingWriter` that writes into the specified buffer (behind a mutex).
154    fn new(buf: &'a Mutex<Vec<u8>>) -> Self {
155        Self { buf }
156    }
157
158    /// Give access to the internal buffer (behind a `MutexGuard`).
159    fn buf(&self) -> io::Result<MutexGuard<'a, Vec<u8>>> {
160        // Note: The `lock` will block. This would be a problem in production code,
161        // but is fine in tests.
162        self.buf
163            .lock()
164            .map_err(|_| io::Error::from(io::ErrorKind::Other))
165    }
166}
167
168impl<'a> io::Write for CollectingWriter<'a> {
169    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
170        let string = String::from_utf8(buf.to_vec()).expect("log contains invalid utf8");
171        println!("{string}");
172        // Lock target buffer
173        let mut target = self.buf()?;
174        // Write to buffer
175        target.write(buf)
176    }
177
178    fn flush(&mut self) -> io::Result<()> {
179        self.buf()?.flush()
180    }
181}
182
183impl<'a> MakeWriter<'_> for CollectingWriter<'a> {
184    type Writer = Self;
185
186    fn make_writer(&self) -> Self::Writer {
187        CollectingWriter::new(self.buf)
188    }
189}