use std::cell::RefCell;
use std::io;
use std::sync::{Arc, Mutex, Once};
thread_local! {
static CAPTURE_BUFFER: RefCell<Option<Arc<Mutex<Vec<u8>>>>> = const { RefCell::new(None) };
}
struct ThreadLocalCaptureWriter;
impl io::Write for ThreadLocalCaptureWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
CAPTURE_BUFFER.with(|cell| {
if let Some(target) = cell.borrow().as_ref() {
target
.lock()
.expect("capture buffer poisoned")
.extend_from_slice(buf);
}
});
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for ThreadLocalCaptureWriter {
type Writer = ThreadLocalCaptureWriter;
fn make_writer(&'a self) -> Self::Writer {
ThreadLocalCaptureWriter
}
}
fn ensure_global_capture_subscriber() {
static INIT: Once = Once::new();
INIT.call_once(|| {
let subscriber = tracing_subscriber::fmt()
.with_writer(ThreadLocalCaptureWriter)
.with_ansi(false)
.without_time()
.with_max_level(tracing::Level::TRACE)
.finish();
let _ = tracing::subscriber::set_global_default(subscriber);
});
}
pub(crate) fn capture_logs<R>(f: impl FnOnce() -> R) -> (R, String) {
ensure_global_capture_subscriber();
let buffer = Arc::new(Mutex::new(Vec::new()));
CAPTURE_BUFFER.with(|cell| *cell.borrow_mut() = Some(Arc::clone(&buffer)));
let restore = RestoreCaptureBuffer;
let result = f();
drop(restore);
let logs = String::from_utf8(buffer.lock().expect("capture buffer poisoned").clone())
.expect("captured logs are valid utf8");
(result, logs)
}
struct RestoreCaptureBuffer;
impl Drop for RestoreCaptureBuffer {
fn drop(&mut self) {
CAPTURE_BUFFER.with(|cell| *cell.borrow_mut() = None);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capture_is_scoped_to_its_own_closure() {
let (_, inside) = capture_logs(|| tracing::warn!("scoped_marker_beta"));
assert!(inside.contains("scoped_marker_beta"));
let (_, after) = capture_logs(|| {});
assert!(
!after.contains("scoped_marker_beta"),
"buffers must not leak across capture scopes; after={after:?}"
);
}
}