rovenue/logging/
mod.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4pub mod redact;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub enum LogLevel {
8    Off,
9    Error,
10    Warn,
11    Info,
12    Debug,
13    Trace,
14}
15
16pub struct LogRecord {
17    pub level: LogLevel,
18    pub message: String,
19    pub fields: HashMap<String, String>,
20}
21
22pub trait LogSink: Send + Sync {
23    fn on_log(&self, record: LogRecord);
24}
25
26pub struct Logger {
27    threshold: LogLevel,
28    sink: Mutex<Option<Arc<dyn LogSink>>>,
29}
30
31impl Logger {
32    pub fn new(threshold: LogLevel) -> Self {
33        Self {
34            threshold,
35            sink: Mutex::new(None),
36        }
37    }
38
39    pub fn set_sink(&self, sink: Arc<dyn LogSink>) {
40        let mut guard = self.sink.lock().unwrap_or_else(|e| e.into_inner());
41        *guard = Some(sink);
42    }
43
44    /// Emit means: this level is at or below the configured threshold.
45    /// `Off` threshold emits nothing (lowest emittable level `Error` > `Off`).
46    pub fn enabled(&self, level: LogLevel) -> bool {
47        level != LogLevel::Off && level <= self.threshold
48    }
49
50    pub fn log(
51        &self,
52        level: LogLevel,
53        message: impl FnOnce() -> String,
54        fields: impl FnOnce() -> HashMap<String, String>,
55    ) {
56        if !self.enabled(level) {
57            return;
58        }
59        let sink = {
60            let guard = self.sink.lock().unwrap_or_else(|e| e.into_inner());
61            match guard.as_ref() {
62                Some(s) => s.clone(),
63                None => return,
64            }
65        };
66        let record = LogRecord {
67            level,
68            message: message(),
69            fields: fields(),
70        };
71        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || sink.on_log(record)));
72    }
73
74    pub fn warn(&self, message: &str) {
75        let msg = message.to_string();
76        self.log(LogLevel::Warn, move || msg, HashMap::new);
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use std::sync::atomic::{AtomicU32, Ordering};
84
85    struct Collector(Arc<Mutex<Vec<LogRecord>>>);
86    impl LogSink for Collector {
87        fn on_log(&self, record: LogRecord) {
88            self.0.lock().unwrap().push(record);
89        }
90    }
91
92    struct Panicky;
93    impl LogSink for Panicky {
94        fn on_log(&self, _r: LogRecord) {
95            panic!("boom");
96        }
97    }
98
99    #[test]
100    fn below_threshold_is_not_emitted_and_closures_not_called() {
101        let logger = Logger::new(LogLevel::Warn);
102        let sink = Arc::new(Mutex::new(Vec::new()));
103        logger.set_sink(Arc::new(Collector(sink.clone())));
104        let built = Arc::new(AtomicU32::new(0));
105        let b = built.clone();
106        // Info is above Warn → must NOT emit, and message closure must NOT run.
107        logger.log(
108            LogLevel::Info,
109            || {
110                b.fetch_add(1, Ordering::SeqCst);
111                "should not build".to_string()
112            },
113            HashMap::new,
114        );
115        assert_eq!(sink.lock().unwrap().len(), 0);
116        assert_eq!(built.load(Ordering::SeqCst), 0);
117    }
118
119    #[test]
120    fn at_or_below_threshold_emits() {
121        let logger = Logger::new(LogLevel::Debug);
122        let sink = Arc::new(Mutex::new(Vec::new()));
123        logger.set_sink(Arc::new(Collector(sink.clone())));
124        logger.log(LogLevel::Error, || "err".to_string(), HashMap::new);
125        logger.log(LogLevel::Debug, || "dbg".to_string(), HashMap::new);
126        let got = sink.lock().unwrap();
127        assert_eq!(got.len(), 2);
128        assert_eq!(got[0].message, "err");
129    }
130
131    #[test]
132    fn off_threshold_emits_nothing() {
133        let logger = Logger::new(LogLevel::Off);
134        let sink = Arc::new(Mutex::new(Vec::new()));
135        logger.set_sink(Arc::new(Collector(sink.clone())));
136        logger.log(LogLevel::Error, || "err".to_string(), HashMap::new);
137        assert_eq!(sink.lock().unwrap().len(), 0);
138    }
139
140    #[test]
141    fn no_sink_is_noop() {
142        let logger = Logger::new(LogLevel::Trace);
143        logger.log(LogLevel::Error, || "err".to_string(), HashMap::new); // must not panic
144    }
145
146    #[test]
147    fn panicking_sink_does_not_unwind() {
148        let logger = Logger::new(LogLevel::Trace);
149        logger.set_sink(Arc::new(Panicky));
150        logger.log(LogLevel::Error, || "err".to_string(), HashMap::new); // caught, no panic
151    }
152}