tree_logger/
logger.rs

1// Based off of the great SimpleLogger crate: https://crates.io/crates/simple_logger
2use colored::*;
3use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
4use rustc_hash::FxHashMap;
5use std::sync::{Arc, Mutex};
6use strip_ansi_escapes::strip;
7use termsize::Size;
8
9use crate::constants;
10
11pub struct TreeLogger {
12    default_level: LevelFilter,
13    threads_enabled: bool,
14    colors_enabled: bool,
15    use_stderr: bool,
16    data: LoggingData,
17}
18
19#[derive(Debug, Default, Clone)]
20struct LoggingData {
21    // Maps thread ids to logging data
22    internal_data: Arc<Mutex<FxHashMap<String, InternalLoggingData>>>,
23}
24
25#[derive(Debug, Default, Clone)]
26struct InternalLoggingData {
27    indentation: usize,
28    next_id: usize,
29    events: Vec<LoggingEvent>,
30}
31
32#[derive(Debug, Clone)]
33struct LoggingEvent {
34    id: Option<usize>,
35    indentation: usize,
36    elapsed: Option<u128>,
37    level: Level,
38    target: String,
39    args: String,
40    thread: String,
41    quiet: bool,
42}
43
44impl LoggingEvent {
45    fn get_args(&self) -> String {
46        use ansi_term::Colour::{Cyan, Red};
47        match self.elapsed {
48            Some(elapsed) => {
49                if elapsed > 100 {
50                    format!("{}: {}", self.args, Red.paint(format!("{elapsed}ms")))
51                } else {
52                    format!("{}: {}", self.args, Cyan.paint(format!("{elapsed}ms")))
53                }
54            }
55            None => self.args.clone(),
56        }
57    }
58}
59
60impl LoggingData {
61    fn get_name(&self) -> String {
62        let thread = std::thread::current();
63        thread.name().unwrap_or("default").to_string()
64    }
65
66    fn increment(&self) {
67        let mut data = self.internal_data.lock().unwrap();
68        let data = data.entry(self.get_name()).or_default();
69        data.indentation += 1;
70    }
71
72    fn decrement(&self) {
73        let mut data = self.internal_data.lock().unwrap();
74        let data = data.entry(self.get_name()).or_default();
75        data.indentation -= 1;
76    }
77
78    fn push_record(&self, record: &Record, should_log_thread: bool) {
79        let id = if let Some(id_value) = record.key_values().get(constants::ID.into()) {
80            if let Ok(id) = id_value.to_string().parse::<usize>() {
81                Some(id)
82            } else {
83                None
84            }
85        } else {
86            None
87        };
88
89        let quiet = if let Some(quiet) = record.key_values().get(constants::QUIET.into()) {
90            match quiet.to_string().parse::<usize>() {
91                Ok(quiet) => quiet == 1,
92                Err(_) => false,
93            }
94        } else {
95            false
96        };
97
98        self.push(LoggingEvent {
99            id,
100            quiet,
101            level: record.level(),
102            target: if !record.target().is_empty() {
103                record.target()
104            } else {
105                record.module_path().unwrap_or_default()
106            }
107            .to_string(),
108
109            args: record.args().to_string(),
110            indentation: 0,
111            elapsed: None,
112            thread: if should_log_thread {
113                let thread = std::thread::current();
114
115                match thread.name() {
116                    Some(name) => {
117                        if name == "main" {
118                            "".into()
119                        } else {
120                            format!(" @{name}")
121                        }
122                    }
123                    None => "".into(),
124                }
125            } else {
126                "".into()
127            },
128        });
129    }
130
131    fn push(&self, mut event: LoggingEvent) -> usize {
132        let mut data = self.internal_data.lock().unwrap();
133        let data = data.entry(self.get_name()).or_default();
134        event.indentation = data.indentation;
135
136        // TODO: do I need ID anymore?
137        let id = data.next_id;
138        data.next_id += 1;
139
140        data.events.push(event);
141        id
142    }
143
144    fn get_data_to_log(&self) -> Option<Vec<LoggingEvent>> {
145        let mut data = self.internal_data.lock().unwrap();
146        let data = data.entry(self.get_name()).or_default();
147        if data.indentation == 0 {
148            let mut rv = Vec::new();
149            std::mem::swap(&mut data.events, &mut rv);
150            return Some(rv);
151        }
152        None
153    }
154
155    fn set_time(&self, id: usize, ms: u128) {
156        let mut data = self.internal_data.lock().unwrap();
157        let data = data.entry(self.get_name()).or_default();
158        for record in &mut data.events {
159            if let Some(record_id) = record.id {
160                if record_id == id {
161                    record.elapsed = Some(ms);
162                    return;
163                }
164            }
165        }
166        eprintln!("Couldn't set time!");
167    }
168}
169
170impl Default for TreeLogger {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176impl TreeLogger {
177    /// Initializes the global logger with a CustomLogger instance with
178    /// default log level set to `Level::Trace`.
179    ///
180    /// ```no_run
181    /// use tree_logger::TreeLogger;
182    /// TreeLogger::new().with_colors(true).with_threads(true).init().unwrap();
183    /// log::warn!("This is an example message.");
184    /// ```
185    ///
186    /// [`init`]: #method.init
187    #[must_use = "You must call init() to begin logging"]
188    pub fn new() -> TreeLogger {
189        TreeLogger {
190            default_level: LevelFilter::Trace,
191            threads_enabled: false,
192            colors_enabled: false,
193            use_stderr: false,
194            data: LoggingData::default(),
195        }
196    }
197
198    pub fn init(self) -> Result<(), SetLoggerError> {
199        log::set_max_level(self.max_level());
200        log::set_boxed_logger(Box::new(self))
201    }
202
203    #[must_use = "You must call init() to begin logging"]
204    pub fn with_level(mut self, level: LevelFilter) -> TreeLogger {
205        self.default_level = level;
206        self
207    }
208
209    #[must_use = "You must call init() to begin logging"]
210    pub fn with_threads(mut self, enable_threads: bool) -> TreeLogger {
211        self.threads_enabled = enable_threads;
212        self
213    }
214
215    /// Control whether messages are colored or not.
216    #[must_use = "You must call init() to begin logging"]
217    pub fn with_colors(mut self, enable_colors: bool) -> TreeLogger {
218        self.colors_enabled = enable_colors;
219        self
220    }
221
222    pub fn max_level(&self) -> LevelFilter {
223        self.default_level
224    }
225
226    fn get_level_string(&self, level: Level) -> String {
227        let level_string = format!("{:<5}", level.to_string());
228        if self.colors_enabled {
229            match level {
230                Level::Error => level_string.red(),
231                Level::Warn => level_string.yellow(),
232                Level::Info => level_string.cyan(),
233                Level::Debug => level_string.purple(),
234                Level::Trace => level_string.normal(),
235            }
236            .to_string()
237        } else {
238            level_string
239        }
240    }
241
242    fn print_data(&self, data: Vec<LoggingEvent>) {
243        let terminal_width = termsize::get().unwrap_or(Size { rows: 0, cols: 0 }).cols as usize;
244        if data.len() == 1 && data[0].quiet && data[0].elapsed.unwrap_or(u128::MAX) == 0 {
245            return;
246        }
247
248        for record in data {
249            let left = format!(
250                "{} {:indent$}{}",
251                self.get_level_string(record.level),
252                " ",
253                record.get_args(),
254                indent = record.indentation.checked_sub(1).unwrap_or_default() * 2,
255            );
256
257            let right = format!("[{}{}]", record.target, record.thread);
258
259            let width = String::from_utf8(strip(format!("{left}{right}").as_bytes()))
260                .unwrap_or_default()
261                .len();
262            let message = if terminal_width > 0 && width + 5 < terminal_width {
263                format!(
264                    "{}{:padding$}{}",
265                    left,
266                    " ",
267                    right,
268                    padding = terminal_width - width
269                )
270            } else {
271                left
272            };
273
274            if self.use_stderr {
275                eprintln!("{}", message);
276            } else {
277                println!("{}", message);
278            }
279        }
280    }
281}
282
283impl Log for TreeLogger {
284    fn enabled(&self, metadata: &Metadata) -> bool {
285        metadata.level().to_level_filter() <= self.default_level
286    }
287
288    fn log(&self, record: &Record) {
289        if record
290            .key_values()
291            .get(constants::INCREMENT.into())
292            .is_some()
293        {
294            self.data.increment();
295        } else if record
296            .key_values()
297            .get(constants::DECREMENT.into())
298            .is_some()
299        {
300            self.data.decrement();
301        } else if record
302            .key_values()
303            .get(constants::SET_TIME.into())
304            .is_some()
305        {
306            if let Some(time_value) = record.key_values().get(constants::TIME.into()) {
307                if let Ok(time) = time_value.to_string().parse::<u128>() {
308                    if let Some(id_value) = record.key_values().get(constants::ID.into()) {
309                        if let Ok(id) = id_value.to_string().parse::<usize>() {
310                            self.data.set_time(id, time);
311                        }
312                    }
313                }
314            }
315        } else {
316            if !self.enabled(record.metadata()) {
317                return;
318            }
319
320            self.data.push_record(record, self.threads_enabled);
321        }
322
323        if let Some(data) = self.data.get_data_to_log() {
324            self.print_data(data);
325        }
326    }
327
328    fn flush(&self) {}
329}
330
331#[cfg(test)]
332mod test {
333    // use super::*;
334
335    // TODO: how to test?
336    #[test]
337    fn test_module_levels_denylist() {}
338}