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 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 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); }
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); }
152}