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}