ssstar_testing/logging.rs
1//! Logging in tests is important for troubleshooting, but works very differently then in
2//! production.
3use crate::Result;
4use std::{
5 cell::RefCell,
6 future::Future,
7 io::Write,
8 panic::AssertUnwindSafe,
9 sync::{Arc, Mutex},
10 time::Duration,
11};
12use tracing_subscriber::fmt::MakeWriter;
13
14/// An implementation of `MakeWriter` that captures all log events for a single test
15#[derive(Clone)]
16struct TestWriter {
17 log_events: Arc<Mutex<Vec<u8>>>,
18}
19
20impl TestWriter {
21 fn new() -> Self {
22 Self {
23 log_events: Arc::new(Mutex::new(Vec::<u8>::new())),
24 }
25 }
26
27 /// Clear the writer's buffer, returning the current contents as a string
28 /// Panics of non-UTF8 text has been written to the buffer
29 fn take_string(&self) -> String {
30 let mut guard = self.log_events.lock().unwrap();
31
32 let buffer: Vec<u8> = std::mem::take(&mut guard);
33
34 String::from_utf8(buffer).unwrap()
35 }
36}
37
38impl<'a> Write for &'a TestWriter {
39 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
40 let mut guard = self.log_events.lock().unwrap();
41
42 // Vec already implements Write
43 guard.write(buf)
44 }
45
46 fn flush(&mut self) -> std::io::Result<()> {
47 // no flush needed when you write to a fix
48 Ok(())
49 }
50}
51
52impl<'a> MakeWriter<'a> for TestWriter {
53 type Writer = &'a Self;
54
55 fn make_writer(&'a self) -> Self::Writer {
56 self
57 }
58}
59
60/// Run a test with logging enabled.
61///
62/// This takes the place of `tokio::test` because it needs to initialize the tokio runtime in some
63/// more customized way to ensure logging is done correctly.
64///
65/// It initializes a single `tracing` Dispatch object for this test only, as well as a dedicated
66/// tokio runtime. It configures `tracing` to log to a buffer, and dumps this buffer to the
67/// console at the end of the test or in the event of a panic. Importantly, and unlike the default
68/// behavior you get when initializing tracing, this automatically initializes all threads in the
69/// tokio runtime for this test, to use that same logging config. So this will pick up all log
70/// events in tokio async tasks as well, but only those for this specific test.
71///
72/// This makes the log output for each test much more actionable because it's not interspersed with
73/// log events from other tests.
74pub fn test_with_logging(test: impl Future<Output = Result<()>>) -> Result<()> {
75 // All log events for this test will be stored in this vec.
76 let test_writer = TestWriter::new();
77
78 let dispatch = {
79 use tracing_subscriber::prelude::*;
80 use tracing_subscriber::{fmt, EnvFilter};
81
82 // Note the use of `TestWriter` here which writes the log events to stdout in a way that
83 // Rust unit tests are able to capture (at least on the main thread)
84 let format = fmt::layer()
85 .with_level(true) // include level in output
86 .with_target(true) // targets aren't that useful but filters operate on targets so they're important to know
87 .with_thread_ids(true) // thread IDs are helpful when multiple parallel tests are running at the same time
88 .with_thread_names(false) // but thread names are pretty shit
89 .with_writer(test_writer.clone());
90
91 // Get the log filter from the RUST_LOG env var, or if not set use a reasonable default
92 let filter = EnvFilter::try_from_default_env()
93 .or_else(|_| EnvFilter::try_new("h2=warn,hyper=info,rustls=info,aws=info,debug"))
94 .unwrap();
95
96 // Create a `fmt` subscriber that uses our custom event format, and set it
97 // as the default.
98 let subscriber = tracing_subscriber::registry().with(filter).with(format);
99
100 tracing::Dispatch::new(subscriber)
101 };
102
103 let dispatch = Arc::new(dispatch);
104
105 // This dispatch contains the logging config for this particular test. It needs to be made the
106 // default dispatcher in each thread as well.
107
108 tracing::dispatcher::with_default(&dispatch, || {
109 std::thread_local! {
110 static THREAD_DISPATCHER_GUARD: RefCell<Option<tracing::subscriber::DefaultGuard>> = RefCell::new(None);
111 }
112
113 let mut builder = tokio::runtime::Builder::new_multi_thread();
114 builder.enable_all();
115 {
116 let dispatch = dispatch.clone();
117 builder.on_thread_start(move || {
118 let dispatch = dispatch.clone();
119
120 THREAD_DISPATCHER_GUARD.with(|cell| {
121 cell.replace(Some(tracing::dispatcher::set_default(&dispatch)));
122 })
123 });
124 }
125
126 builder.on_thread_stop(|| {
127 // Drop the dispatcher guard so it's no longer the thread-local default
128 THREAD_DISPATCHER_GUARD.with(|cell| cell.replace(None));
129 });
130
131 let runtime = builder.build()?;
132
133 // I'm pretty sure Tokio `Runtime` structs are safe to pass across unwind boundaries, but I
134 // don't know for sure
135 // It's not realistic to require all test futures to be explicitly unwind safe, so I'll
136 // just assume they're safe too
137 let result = std::panic::catch_unwind(AssertUnwindSafe(move || {
138 let result = runtime.block_on(test);
139 runtime.shutdown_timeout(Duration::from_secs(10));
140
141 result
142 }));
143
144 // Test has run, maybe succeed maybe failed maybe panicked
145 // Print all of the log events now
146 let log_events = test_writer.take_string();
147
148 println!("Log events from this test: \n{}", log_events);
149
150 match result {
151 Ok(result) => {
152 // Test did not panic, so return the result the test did complete with
153 result
154 }
155 Err(err) => {
156 // Test panicked. Just re-throw the panic now that we've written the log output
157 std::panic::resume_unwind(err)
158 }
159 }
160 })?;
161
162 Ok(())
163}