Skip to main content

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::{
6    fs::File,
7    io::{self, IsTerminal, Write},
8    path::Path,
9    sync::{
10        Arc, Mutex,
11        mpsc::{Sender, channel},
12    },
13    thread,
14};
15use strip_ansi_escapes::strip;
16use termsize::Size;
17
18use crate::constants;
19
20pub struct TreeLogger {
21    default_level: LevelFilter,
22    threads_enabled: bool,
23    colors_enabled: bool,
24    use_stderr: bool,
25    filter_fn: fn(&LoggingEvent) -> bool,
26    data: LoggingData,
27    maybe_sender: Option<Sender<String>>,
28    is_terminal: bool,
29}
30
31#[derive(Debug, Default, Clone)]
32struct LoggingData {
33    // Maps thread ids to logging data
34    internal_data: Arc<Mutex<FxHashMap<String, InternalLoggingData>>>,
35}
36
37#[derive(Debug, Default, Clone)]
38struct InternalLoggingData {
39    indentation: usize,
40    next_id: usize,
41    events: Vec<LoggingEvent>,
42}
43
44#[derive(Debug, Clone)]
45pub struct LoggingEvent {
46    pub id: Option<usize>,
47    pub indentation: usize,
48    pub elapsed: Option<u128>,
49    pub level: Level,
50    pub target: String,
51    pub args: String,
52    pub thread: String,
53    pub quiet: bool,
54}
55
56impl LoggingEvent {
57    fn get_args(&self) -> String {
58        use ansi_term::Colour::{Cyan, Red};
59        match self.elapsed {
60            Some(elapsed) => {
61                if elapsed > 100 {
62                    format!("{}: {}", self.args, Red.paint(humanize_ms(elapsed)))
63                } else {
64                    format!("{}: {}", self.args, Cyan.paint(humanize_ms(elapsed)))
65                }
66            }
67            None => self.args.clone(),
68        }
69    }
70}
71
72impl LoggingData {
73    fn get_name(&self) -> String {
74        let thread = std::thread::current();
75        thread.name().unwrap_or("default").to_string()
76    }
77
78    fn increment(&self) {
79        let mut data = self.internal_data.lock().unwrap();
80        let data = data.entry(self.get_name()).or_default();
81        data.indentation += 1;
82    }
83
84    fn decrement(&self) {
85        let mut data = self.internal_data.lock().unwrap();
86        let data = data.entry(self.get_name()).or_default();
87        data.indentation -= 1;
88    }
89
90    fn push_record(&self, record: &Record, should_log_thread: bool) {
91        let id = if let Some(id_value) = record.key_values().get(constants::ID.into()) {
92            if let Ok(id) = id_value.to_string().parse::<usize>() {
93                Some(id)
94            } else {
95                None
96            }
97        } else {
98            None
99        };
100
101        let quiet = if let Some(quiet) = record.key_values().get(constants::QUIET.into()) {
102            match quiet.to_string().parse::<usize>() {
103                Ok(quiet) => quiet == 1,
104                Err(_) => false,
105            }
106        } else {
107            false
108        };
109
110        self.push(LoggingEvent {
111            id,
112            quiet,
113            level: record.level(),
114            target: if !record.target().is_empty() {
115                record.target()
116            } else {
117                record.module_path().unwrap_or_default()
118            }
119            .to_string(),
120
121            args: record.args().to_string(),
122            indentation: 0,
123            elapsed: None,
124            thread: if should_log_thread {
125                let thread = std::thread::current();
126
127                match thread.name() {
128                    Some(name) => {
129                        if name == "main" {
130                            "".into()
131                        } else {
132                            format!(" @{name}")
133                        }
134                    }
135                    None => "".into(),
136                }
137            } else {
138                "".into()
139            },
140        });
141    }
142
143    fn push(&self, mut event: LoggingEvent) -> usize {
144        let mut data = self.internal_data.lock().unwrap();
145        let data = data.entry(self.get_name()).or_default();
146        event.indentation = data.indentation;
147
148        // TODO: do I need ID anymore?
149        let id = data.next_id;
150        data.next_id += 1;
151
152        data.events.push(event);
153        id
154    }
155
156    fn get_data_to_log(&self) -> Option<Vec<LoggingEvent>> {
157        let mut data = self.internal_data.lock().unwrap();
158        let data = data.entry(self.get_name()).or_default();
159        if data.indentation == 0 {
160            let mut rv = Vec::new();
161            std::mem::swap(&mut data.events, &mut rv);
162            return Some(rv);
163        }
164        None
165    }
166
167    fn set_time(&self, id: usize, ms: u128) {
168        let mut data = self.internal_data.lock().unwrap();
169        let data = data.entry(self.get_name()).or_default();
170        for record in &mut data.events {
171            if let Some(record_id) = record.id {
172                if record_id == id {
173                    record.elapsed = Some(ms);
174                    return;
175                }
176            }
177        }
178        // eprintln!("Couldn't set time!");
179    }
180}
181
182impl Default for TreeLogger {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188impl TreeLogger {
189    /// Initializes the global logger with a CustomLogger instance with
190    /// default log level set to `Level::Trace`.
191    ///
192    /// ```no_run
193    /// use tree_logger::TreeLogger;
194    /// TreeLogger::new().with_colors(true).with_threads(true).init().unwrap();
195    /// log::warn!("This is an example message.");
196    /// ```
197    ///
198    /// [`init`]: #method.init
199    #[must_use = "You must call init() to begin logging"]
200    pub fn new() -> TreeLogger {
201        TreeLogger {
202            default_level: LevelFilter::Trace,
203            threads_enabled: false,
204            colors_enabled: false,
205            use_stderr: false,
206            filter_fn: |_| true,
207            data: LoggingData::default(),
208            maybe_sender: None,
209            is_terminal: io::stdout().is_terminal(),
210        }
211    }
212
213    pub fn init(self) -> Result<(), SetLoggerError> {
214        log::set_max_level(self.max_level());
215        log::set_boxed_logger(Box::new(self))
216    }
217
218    #[must_use = "You must call init() to begin logging"]
219    pub fn with_filter_fn(mut self, filter_fn: fn(&LoggingEvent) -> bool) -> TreeLogger {
220        self.filter_fn = filter_fn;
221        self
222    }
223
224    #[must_use = "You must call init() to begin logging"]
225    pub fn with_level(mut self, level: LevelFilter) -> TreeLogger {
226        self.default_level = level;
227        self
228    }
229
230    #[must_use = "You must call init() to begin logging"]
231    pub fn with_file<T: AsRef<Path>>(mut self, path: T, append: bool) -> TreeLogger {
232        if self.maybe_sender.is_some() {
233            panic!("Can't set file more than once");
234        }
235
236        // TODO: is this reasonable?
237        let (sender, receiver) = channel::<String>();
238        thread::spawn({
239            let mut file = if append {
240                File::options()
241                    .write(true)
242                    .create(true)
243                    .append(true)
244                    .open(path)
245                    .unwrap()
246            } else {
247                File::create(path).unwrap()
248            };
249
250            move || {
251                while let Ok(value) = receiver.recv() {
252                    _ = writeln!(file, "{}", value);
253                }
254            }
255        });
256
257        self.maybe_sender = Some(sender);
258        self
259    }
260
261    #[must_use = "You must call init() to begin logging"]
262    pub fn with_threads(mut self, enable_threads: bool) -> TreeLogger {
263        self.threads_enabled = enable_threads;
264        self
265    }
266
267    /// Control whether messages are colored or not.
268    #[must_use = "You must call init() to begin logging"]
269    pub fn with_colors(mut self, enable_colors: bool) -> TreeLogger {
270        self.colors_enabled = enable_colors;
271        self
272    }
273
274    /// True to use stderr instead of stdout (default).
275    pub fn with_stderr(mut self) -> TreeLogger {
276        self.use_stderr = true;
277        self
278    }
279
280    pub fn max_level(&self) -> LevelFilter {
281        self.default_level
282    }
283
284    fn get_level_string(&self, level: Level) -> String {
285        let level_string = format!("{:<5}", level.to_string());
286        if self.colors_enabled {
287            match level {
288                Level::Error => level_string.red(),
289                Level::Warn => level_string.yellow(),
290                Level::Info => level_string.cyan(),
291                Level::Debug => level_string.purple(),
292                Level::Trace => level_string.normal(),
293            }
294            .to_string()
295        } else {
296            level_string
297        }
298    }
299
300    fn print_data(&self, data: Vec<LoggingEvent>) {
301        if data.len() == 0 {
302            return;
303        }
304
305        if !(self.filter_fn)(&data[0]) {
306            return;
307        }
308
309        if data.len() == 1 && data[0].quiet && data[0].elapsed.unwrap_or(u128::MAX) == 0 {
310            return;
311        }
312
313        let terminal_width = termsize::get().unwrap_or(Size { rows: 0, cols: 0 }).cols as usize;
314        for record in data.iter().filter(|e| (self.filter_fn)(e)) {
315            let left = format!(
316                "{} {:indent$}{}",
317                self.get_level_string(record.level),
318                " ",
319                record.get_args(),
320                indent = record.indentation.checked_sub(1).unwrap_or_default() * 2,
321            );
322
323            let right = format!("[{}{}]", record.target, record.thread);
324
325            let width = String::from_utf8(strip(format!("{left}{right}").as_bytes()))
326                .unwrap_or_default()
327                .len();
328            let message = if terminal_width > 0 && width + 5 < terminal_width {
329                format!(
330                    "{}{:padding$}{}",
331                    left,
332                    " ",
333                    right,
334                    padding = terminal_width - width
335                )
336            } else {
337                left
338            };
339
340            if let Some(sender) = &self.maybe_sender {
341                _ = sender.send(message.clone());
342            }
343
344            if self.use_stderr {
345                eprintln!("{}", message);
346            } else {
347                // Only print if this is a terminal
348                if self.is_terminal {
349                    println!("{}", message);
350                }
351            }
352        }
353    }
354}
355
356impl Log for TreeLogger {
357    fn enabled(&self, metadata: &Metadata) -> bool {
358        metadata.level().to_level_filter() <= self.default_level
359    }
360
361    fn log(&self, record: &Record) {
362        if record
363            .key_values()
364            .get(constants::INCREMENT.into())
365            .is_some()
366        {
367            self.data.increment();
368        } else if record
369            .key_values()
370            .get(constants::DECREMENT.into())
371            .is_some()
372        {
373            self.data.decrement();
374        } else if record
375            .key_values()
376            .get(constants::SET_TIME.into())
377            .is_some()
378        {
379            if let Some(time_value) = record.key_values().get(constants::TIME.into()) {
380                if let Ok(time) = time_value.to_string().parse::<u128>() {
381                    if let Some(id_value) = record.key_values().get(constants::ID.into()) {
382                        if let Ok(id) = id_value.to_string().parse::<usize>() {
383                            self.data.set_time(id, time);
384                        }
385                    }
386                }
387            }
388        } else {
389            if !self.enabled(record.metadata()) {
390                return;
391            }
392
393            self.data.push_record(record, self.threads_enabled);
394        }
395
396        if let Some(data) = self.data.get_data_to_log() {
397            self.print_data(data);
398        }
399    }
400
401    fn flush(&self) {}
402}
403
404pub fn humanize_ms(ms: u128) -> String {
405    match ms {
406        60_000.. => {
407            format!("{}min", ms / 60_000)
408        }
409        1000..60_000 => {
410            format!("{}sec", ms / 1000)
411        }
412        0..1000 => {
413            format!("{}ms", ms)
414        }
415    }
416}
417
418#[cfg(test)]
419mod test {
420    use super::*;
421
422    #[test]
423    fn humanize_tests() {
424        assert_eq!(humanize_ms(10), "10ms");
425        assert_eq!(humanize_ms(500), "500ms");
426        assert_eq!(humanize_ms(1000), "1sec");
427        assert_eq!(humanize_ms(10_000), "10sec");
428        assert_eq!(humanize_ms(59_000), "59sec");
429        assert_eq!(humanize_ms(60_000), "1min");
430    }
431
432    // #[test]
433    // fn file_works() {
434    //     TreeLogger::new()
435    //         .with_colors(true)
436    //         .with_threads(true)
437    //         .with_file("/tmp/logger.txt", true /* append */)
438    //         .init()
439    //         .unwrap();
440    //     log::info!("Did this work?");
441    //     log::info!("Yes it did!");
442    // }
443}