dev_logger/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! Convenient env_logger for testing purpose.
9//!
10//! # Example
11//!
12//! ```
13//! // In lib.rs:
14//! #[cfg(test)]
15//! dev_logger::init!();
16//!
17//! // In test function:
18//! tracing::info!(name = "message");
19//!
20//! // Set RUST_LOG=info and run the test.
21//! ```
22
23use std::env;
24use std::io;
25use std::sync::Arc;
26use std::sync::Mutex;
27
28pub use ctor::ctor;
29use tracing_subscriber::fmt::format::FmtSpan;
30use tracing_subscriber::fmt::MakeWriter;
31use tracing_subscriber::fmt::Subscriber;
32use tracing_subscriber::EnvFilter;
33
34/// Initialize tracing and env_logger for adhoc logging (ex. in a library test)
35/// purpose.
36pub fn init() {
37    let env_name = ["SL_LOG", "RUST_LOG", "LOG"]
38        .into_iter()
39        .find(|name| env::var_os(name).is_some())
40        .unwrap_or("LOG");
41    let builder = Subscriber::builder()
42        .with_env_filter(EnvFilter::from_env(env_name))
43        .with_ansi(false)
44        .with_target(false)
45        .without_time()
46        .with_span_events(FmtSpan::ACTIVE);
47
48    builder.init();
49}
50
51/// Trace the given function using the given filter (in EnvFilter format).
52/// Return strings representing the traced logs.
53pub fn traced(filter: &str, func: impl FnOnce()) -> Vec<String> {
54    #[derive(Clone, Default)]
55    struct Output(Arc<Mutex<Vec<String>>>);
56
57    impl MakeWriter<'_> for Output {
58        type Writer = Output;
59        fn make_writer(&self) -> Self::Writer {
60            self.clone()
61        }
62    }
63
64    impl io::Write for Output {
65        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
66            let mut lines = self.0.lock().unwrap();
67            let mut s = String::from_utf8_lossy(buf).trim().to_string();
68
69            // Buck unittest targets add "_unittest" suffix to crate names
70            // that will affect the "target". Workaround it by removing the
71            // suffix.
72            if cfg!(fbcode_build) {
73                s = s.replace("_unittest: ", ": ");
74                s = s.replace("_unittest::", "::");
75            }
76
77            lines.push(s);
78            Ok(buf.len())
79        }
80        fn flush(&mut self) -> io::Result<()> {
81            Ok(())
82        }
83    }
84
85    let out = Output::default();
86    let builder = Subscriber::builder()
87        .with_env_filter(EnvFilter::new(filter))
88        .with_ansi(false)
89        .without_time()
90        .with_writer(out.clone())
91        .with_span_events(FmtSpan::ACTIVE);
92    let dispatcher = builder.finish();
93    tracing::subscriber::with_default(dispatcher, func);
94
95    let lines = out.0.lock().unwrap();
96    lines.clone()
97}
98
99/// Call `init` on startup. This is useful for tests.
100#[macro_export]
101macro_rules! init {
102    () => {
103        #[dev_logger::ctor]
104        fn dev_logger_init_ctor() {
105            dev_logger::init();
106        }
107    };
108}
109
110#[test]
111fn test_traced() {
112    let lines = traced("info", || {
113        tracing::info_span!("bar", x = 1).in_scope(|| {
114            tracing::info!("foo");
115            tracing::debug!("foo2");
116        });
117    });
118    assert_eq!(
119        lines,
120        [
121            "INFO bar{x=1}: dev_logger: enter",
122            "INFO bar{x=1}: dev_logger: foo",
123            "INFO bar{x=1}: dev_logger: exit"
124        ]
125    );
126}