1use std::{
2 borrow::Cow,
3 boxed::Box,
4 collections::VecDeque,
5 string::{String, ToString},
6 sync::{Arc, LazyLock, Mutex},
7 vec::Vec,
8};
9
10use compact_str::CompactString;
11use log::{Level, LevelFilter, Log};
12
13static LOGGER: LazyLock<DebugLogger> = LazyLock::new(DebugLogger::default);
14
15const HISTORY_SIZE: usize = 1000;
19
20#[derive(Default)]
21struct DebugLoggerImpl {
22 inner: Option<Box<dyn Log>>,
23 captured: VecDeque<LogEntry>,
24}
25
26#[derive(Clone)]
27pub struct LogEntry {
28 pub level: Level,
29 pub target: CompactString,
30 pub file: Option<Cow<'static, str>>,
31 pub line: Option<u32>,
32 pub message: String,
33}
34
35#[derive(Default, Clone)]
36pub struct DebugLogger(Arc<Mutex<DebugLoggerImpl>>);
37
38impl Log for DebugLogger {
39 fn enabled(&self, metadata: &log::Metadata) -> bool {
40 let guard = self.0.lock().unwrap();
41 guard.inner.as_ref().is_some_and(|inner| inner.enabled(metadata))
42 }
43
44 fn log(&self, record: &log::Record) {
45 let mut guard = self.0.lock().unwrap();
46 if !guard.inner.as_ref().is_some_and(|inner| inner.enabled(record.metadata())) {
47 return;
48 }
49
50 let target = CompactString::new(record.target());
51 let file = record
52 .file_static()
53 .map(Cow::Borrowed)
54 .or_else(|| record.file().map(|f| f.to_string()).map(Cow::Owned));
55 let entry = LogEntry {
56 target,
57 level: record.level(),
58 file,
59 line: record.line(),
60 message: format!("{}", record.args()),
61 };
62 guard.captured.push_back(entry);
63 if guard.captured.len() > HISTORY_SIZE {
64 guard.captured.pop_front();
65 }
66 if let Some(inner) = guard.inner.as_ref() {
67 inner.log(record);
68 }
69 }
70
71 fn flush(&self) {}
72}
73
74impl DebugLogger {
75 pub fn install_with_max_level(
77 inner: Box<dyn Log>,
78 max_level: LevelFilter,
79 ) -> Result<(), log::SetLoggerError> {
80 let logger = &*LOGGER;
81 log::set_logger(logger)?;
82 logger.set_inner(inner);
84 log::set_max_level(max_level);
85 Ok(())
86 }
87
88 pub fn get() -> &'static Self {
89 &LOGGER
90 }
91
92 pub fn take_captured(&self) -> VecDeque<LogEntry> {
93 let mut guard = self.0.lock().unwrap();
94 core::mem::take(&mut guard.captured)
95 }
96
97 pub fn count_matching<F>(&self, mut predicate: F) -> usize
99 where
100 F: FnMut(&LogEntry) -> bool,
101 {
102 let guard = self.0.lock().unwrap();
103 guard.captured.iter().filter(move |entry| predicate(entry)).count()
104 }
105
106 pub fn contains_matching<F>(&self, predicate: F) -> bool
108 where
109 F: FnMut(&LogEntry) -> bool,
110 {
111 let guard = self.0.lock().unwrap();
112 guard.captured.iter().any(predicate)
113 }
114
115 pub fn select_matching<F>(&self, mut predicate: F) -> Vec<LogEntry>
117 where
118 F: FnMut(&LogEntry) -> bool,
119 {
120 let guard = self.0.lock().unwrap();
121 guard
122 .captured
123 .iter()
124 .filter_map(move |entry| {
125 if predicate(entry) {
126 Some(entry.clone())
127 } else {
128 None
129 }
130 })
131 .collect()
132 }
133
134 pub fn clone_captured(&self) -> VecDeque<LogEntry> {
135 self.0.lock().unwrap().captured.clone()
136 }
137
138 fn set_inner(&self, logger: Box<dyn Log>) {
139 drop(self.0.lock().unwrap().inner.replace(logger));
140 }
141
142 pub fn init_for_tests() -> Result<(), log::SetLoggerError> {
144 use env_logger::Env;
145 let env = Env::new().filter_or("MIDENC_TRACE", "info");
146 let mut builder = env_logger::Builder::from_env(env);
147 builder.format_indent(Some(2));
148 builder.format_timestamp(None);
149 Self::install_with_max_level(Box::new(builder.build()), LevelFilter::Trace)
150 }
151}