Skip to main content

logging/
logger.rs

1use crate::handler::Handler;
2use crate::types::Level;
3use chrono::Local;
4use std::collections::HashMap;
5use std::error::Error;
6use std::sync::OnceLock;
7use std::sync::atomic::{AtomicU8, Ordering};
8use std::sync::{Arc, Mutex, RwLock};
9use std::thread;
10
11type Handlers = RwLock<Vec<Arc<dyn Handler>>>;
12
13/// Process-global registry of named loggers, created lazily on first use.
14static LOGGERS: OnceLock<Mutex<HashMap<String, Arc<Logger>>>> = OnceLock::new();
15fn loggers() -> &'static Mutex<HashMap<String, Arc<Logger>>> {
16    LOGGERS.get_or_init(|| Mutex::new(HashMap::new()))
17}
18
19/// A named logger with a severity threshold and a set of [`Handler`]s.
20///
21/// Use [`Logger::get`] to obtain a shared logger by name (created once, then
22/// reused), or [`Logger::root`] for the ancestor logger. Messages below
23/// [`level`](Logger::level) are dropped before reaching any handler.
24///
25/// The level is stored in an [`AtomicU8`] and handlers behind an [`RwLock`], so
26/// a `&Logger` can be used concurrently from multiple threads.
27pub struct Logger {
28    name: &'static str,
29    handlers: Handlers,
30    level: AtomicU8,
31}
32
33static ROOT_LOGGER_NAME: &str = "root";
34static ROOT_LOGGER: OnceLock<Arc<Logger>> = OnceLock::new();
35
36impl Logger {
37    /// Builds a logger. The root logger starts empty at [`Level::Info`]; any
38    /// other logger inherits a snapshot of root's handlers and level at
39    /// creation time.
40    pub fn new(name: &'static str) -> Self {
41        if name == ROOT_LOGGER_NAME {
42            Self {
43                name: ROOT_LOGGER_NAME,
44                handlers: RwLock::new(Vec::new()),
45                level: AtomicU8::new(Level::Info as u8),
46            }
47        } else {
48            Self {
49                name,
50                handlers: RwLock::new(Self::root().handlers()),
51                level: AtomicU8::new(Self::root().level() as u8),
52            }
53        }
54    }
55
56    /// Returns the shared logger registered under `name`, creating and
57    /// registering it on first call.
58    pub fn get(name: &'static str) -> Arc<Self> {
59        let mut loggers = loggers().lock().unwrap();
60        loggers
61            .entry(name.to_string())
62            .or_insert_with(|| Arc::new(Logger::new(name)))
63            .clone()
64    }
65
66    /// Returns the shared root logger, the ancestor whose handlers/level new
67    /// loggers inherit.
68    pub fn root() -> Arc<Self> {
69        ROOT_LOGGER
70            .get_or_init(|| Arc::new(Self::new(ROOT_LOGGER_NAME)))
71            .clone()
72    }
73
74    /// Returns a snapshot clone of this logger's handlers.
75    pub fn handlers(&self) -> Vec<Arc<dyn Handler>> {
76        self.handlers.read().unwrap().clone()
77    }
78
79    /// Sets the minimum severity this logger will emit.
80    pub fn set_level(&self, level: Level) {
81        self.level.store(level as u8, Ordering::Relaxed);
82    }
83
84    /// Returns the logger's current minimum severity.
85    pub fn level(&self) -> Level {
86        Level::from_u8(self.level.load(Ordering::Relaxed))
87    }
88
89    /// Appends a handler. Returns an error only if the internal lock was
90    /// poisoned by a panic in another thread.
91    pub fn add_handler(&self, handler: impl Handler + 'static) -> Result<(), Box<dyn Error>> {
92        self.handlers
93            .write()
94            .map_err(|_| "Handlers mutex was poisoned")?
95            .push(Arc::new(handler));
96        Ok(())
97    }
98
99    /// Emits `message` at `level` to every handler, unless `level` is below the
100    /// logger's threshold. Builds one [`Record`](crate::Record) (message,
101    /// level, timestamp, logger name, thread name) and hands it to each handler.
102    pub fn log(&self, level: Level, message: &str) {
103        if level.to_u8() < self.level.load(Ordering::Relaxed) {
104            return;
105        }
106        let handlers = self
107            .handlers
108            .read()
109            .unwrap_or_else(|poisoned| poisoned.into_inner());
110        for handler in handlers.iter() {
111            let formatter = handler.formatter();
112            handler.handle(&HashMap::from([
113                ("message", message),
114                ("level", level.to_str()),
115                (
116                    "timestamp",
117                    Local::now()
118                        .format(formatter.time_format)
119                        .to_string()
120                        .as_str(),
121                ),
122                ("name", self.name),
123                ("thread", thread::current().name().unwrap_or("unknown")),
124            ]));
125        }
126    }
127
128    /// Logs `message` at [`Level::Debug`].
129    pub fn debug(&self, message: &str) {
130        self.log(Level::Debug, message)
131    }
132
133    /// Logs `message` at [`Level::Info`].
134    pub fn info(&self, message: &str) {
135        self.log(Level::Info, message)
136    }
137
138    /// Logs `message` at [`Level::Warning`].
139    pub fn warning(&self, message: &str) {
140        self.log(Level::Warning, message)
141    }
142
143    /// Logs `message` at [`Level::Error`].
144    pub fn error(&self, message: &str) {
145        self.log(Level::Error, message)
146    }
147
148    /// Logs `message` at [`Level::Critical`].
149    pub fn critical(&self, message: &str) {
150        self.log(Level::Critical, message)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::handler::StreamHandler;
158    use std::io::Write;
159    use std::sync::Arc;
160
161    #[derive(Clone)]
162    struct SharedBuf(Arc<Mutex<Vec<u8>>>);
163
164    impl Write for SharedBuf {
165        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
166            self.0.lock().unwrap().extend_from_slice(buf);
167            Ok(buf.len())
168        }
169        fn flush(&mut self) -> std::io::Result<()> {
170            Ok(())
171        }
172    }
173
174    // --- Registry test harness ---------------------------------------------
175    //
176    // `cargo test` runs tests in parallel within a single process, so every
177    // test shares the one global `LOGGERS` map and `ROOT_LOGGER`. Tests that
178    // touch the registry (`get`/`root`) therefore interfere with each other.
179    //
180    // This is the same problem CPython's logging test suite has; its solution
181    // is to run serially and snapshot/restore global state around each test
182    // (see `BaseTest.setUp`/`tearDown`). We do the Rust port of that:
183    //
184    // - `REGISTRY_LOCK` serializes registry tests (replaces Python's implicit
185    //   single-threaded assumption).
186    // - `RegistryGuard` snapshots the map + root's level/handlers on creation
187    //   and restores them on drop, so each test starts (and leaves) clean.
188    //
189    // RULE: every test that calls `Logger::new`, `Logger::get`, or `Logger::root`
190    // MUST acquire a `RegistryGuard` first. `Logger::new` snapshots root's
191    // handlers/level, so a test that pollutes root would otherwise leak into any
192    // *other* test that constructs a logger concurrently. Holding the guard both
193    // serializes those tests and resets root to a clean state on entry.
194    //
195    // Caveat: `ROOT_LOGGER` is a `OnceLock`, so its *identity* can't be reset
196    // once initialized. We restore its *contents* (level + handlers), which is
197    // all the behavior under test depends on.
198
199    static REGISTRY_LOCK: Mutex<()> = Mutex::new(());
200
201    struct RegistryGuard {
202        // Held for the test's duration to serialize registry access. `_` keeps
203        // it alive without an unused-variable warning; never read directly.
204        _lock: std::sync::MutexGuard<'static, ()>,
205        saved_map: HashMap<String, Arc<Logger>>,
206        saved_root_level: Level,
207        saved_root_handlers: Vec<Arc<dyn Handler>>,
208    }
209
210    impl RegistryGuard {
211        fn acquire() -> Self {
212            // Recover from a poisoned lock: a panicking test should not wedge
213            // the rest of the registry tests.
214            let lock = REGISTRY_LOCK
215                .lock()
216                .unwrap_or_else(|poisoned| poisoned.into_inner());
217
218            let saved_map = loggers().lock().unwrap().clone();
219            let root = Logger::root();
220            let guard = Self {
221                _lock: lock,
222                saved_map,
223                saved_root_level: root.level(),
224                saved_root_handlers: root.handlers(),
225            };
226            // Start from a clean slate.
227            loggers().lock().unwrap().clear();
228            root.set_level(Level::Info);
229            *root.handlers.write().unwrap() = Vec::new();
230            guard
231        }
232    }
233
234    impl Drop for RegistryGuard {
235        fn drop(&mut self) {
236            // Restore the registry and root so the next test is unaffected.
237            *loggers().lock().unwrap() = std::mem::take(&mut self.saved_map);
238            let root = Logger::root();
239            root.set_level(self.saved_root_level);
240            *root.handlers.write().unwrap() = std::mem::take(&mut self.saved_root_handlers);
241        }
242    }
243
244    #[test]
245    fn get_returns_the_same_cached_instance() {
246        let _guard = RegistryGuard::acquire();
247
248        let first = Logger::get("cached");
249        let second = Logger::get("cached");
250        // Same registration => same allocation (Arc points at one Logger).
251        assert!(Arc::ptr_eq(&first, &second));
252    }
253
254    #[test]
255    fn child_inherits_root_level_at_creation() {
256        let _guard = RegistryGuard::acquire();
257
258        Logger::root().set_level(Level::Error);
259        // A non-root logger snapshots root's level when created.
260        let child = Logger::new("child_level");
261        assert_eq!(child.level(), Level::Error);
262    }
263
264    #[test]
265    fn child_inherits_root_handlers_at_creation() {
266        let _guard = RegistryGuard::acquire();
267
268        let buf = Arc::new(Mutex::new(Vec::new()));
269        Logger::root()
270            .add_handler(StreamHandler::with_pattern(
271                SharedBuf(buf.clone()),
272                "%(message)",
273            ))
274            .unwrap();
275
276        // Child created after root is configured inherits root's handler.
277        let child = Logger::new("child_handlers");
278        child.set_level(Level::Debug);
279        child.info("via inherited handler");
280
281        let written = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
282        assert_eq!(written, "via inherited handler\n");
283    }
284
285    #[test]
286    fn level_get_set_round_trips() {
287        // Touches root via `Logger::new` (inherits root's level), so it must
288        // hold the guard to avoid cross-test contamination.
289        let _guard = RegistryGuard::acquire();
290
291        let logger = Logger::new("test_level");
292        logger.set_level(Level::Error);
293        assert_eq!(logger.level(), Level::Error);
294    }
295
296    #[test]
297    fn messages_below_level_are_dropped() {
298        let _guard = RegistryGuard::acquire();
299
300        let buf = Arc::new(Mutex::new(Vec::new()));
301        let logger = Logger::new("test_filter");
302        logger.set_level(Level::Warning);
303        logger
304            .add_handler(StreamHandler::with_pattern(
305                SharedBuf(buf.clone()),
306                "%(message)",
307            ))
308            .unwrap();
309
310        logger.info("dropped"); // below Warning
311        logger.warning("kept"); // at threshold
312
313        let written = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
314        assert_eq!(written, "kept\n");
315    }
316
317    #[test]
318    fn record_includes_level_and_message() {
319        let _guard = RegistryGuard::acquire();
320
321        let buf = Arc::new(Mutex::new(Vec::new()));
322        let logger = Logger::new("test_record");
323        logger.set_level(Level::Debug);
324        logger
325            .add_handler(StreamHandler::with_pattern(
326                SharedBuf(buf.clone()),
327                "%(level) %(name) %(message)",
328            ))
329            .unwrap();
330
331        logger.error("boom");
332
333        let written = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
334        assert_eq!(written, "ERROR test_record boom\n");
335    }
336}