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