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    pub fn max_level(&self) -> LevelFilter {
275        self.default_level
276    }
277
278    fn get_level_string(&self, level: Level) -> String {
279        let level_string = format!("{:<5}", level.to_string());
280        if self.colors_enabled {
281            match level {
282                Level::Error => level_string.red(),
283                Level::Warn => level_string.yellow(),
284                Level::Info => level_string.cyan(),
285                Level::Debug => level_string.purple(),
286                Level::Trace => level_string.normal(),
287            }
288            .to_string()
289        } else {
290            level_string
291        }
292    }
293
294    fn print_data(&self, data: Vec<LoggingEvent>) {
295        if data.len() == 0 {
296            return;
297        }
298
299        if !(self.filter_fn)(&data[0]) {
300            return;
301        }
302
303        if data.len() == 1 && data[0].quiet && data[0].elapsed.unwrap_or(u128::MAX) == 0 {
304            return;
305        }
306
307        let terminal_width = termsize::get().unwrap_or(Size { rows: 0, cols: 0 }).cols as usize;
308        for record in data.iter().filter(|e| (self.filter_fn)(e)) {
309            let left = format!(
310                "{} {:indent$}{}",
311                self.get_level_string(record.level),
312                " ",
313                record.get_args(),
314                indent = record.indentation.checked_sub(1).unwrap_or_default() * 2,
315            );
316
317            let right = format!("[{}{}]", record.target, record.thread);
318
319            let width = String::from_utf8(strip(format!("{left}{right}").as_bytes()))
320                .unwrap_or_default()
321                .len();
322            let message = if terminal_width > 0 && width + 5 < terminal_width {
323                format!(
324                    "{}{:padding$}{}",
325                    left,
326                    " ",
327                    right,
328                    padding = terminal_width - width
329                )
330            } else {
331                left
332            };
333
334            if let Some(sender) = &self.maybe_sender {
335                _ = sender.send(message.clone());
336            }
337
338            if self.use_stderr {
339                eprintln!("{}", message);
340            } else {
341                // Only print if this is a terminal
342                if self.is_terminal {
343                    println!("{}", message);
344                }
345            }
346        }
347    }
348}
349
350impl Log for TreeLogger {
351    fn enabled(&self, metadata: &Metadata) -> bool {
352        metadata.level().to_level_filter() <= self.default_level
353    }
354
355    fn log(&self, record: &Record) {
356        if record
357            .key_values()
358            .get(constants::INCREMENT.into())
359            .is_some()
360        {
361            self.data.increment();
362        } else if record
363            .key_values()
364            .get(constants::DECREMENT.into())
365            .is_some()
366        {
367            self.data.decrement();
368        } else if record
369            .key_values()
370            .get(constants::SET_TIME.into())
371            .is_some()
372        {
373            if let Some(time_value) = record.key_values().get(constants::TIME.into()) {
374                if let Ok(time) = time_value.to_string().parse::<u128>() {
375                    if let Some(id_value) = record.key_values().get(constants::ID.into()) {
376                        if let Ok(id) = id_value.to_string().parse::<usize>() {
377                            self.data.set_time(id, time);
378                        }
379                    }
380                }
381            }
382        } else {
383            if !self.enabled(record.metadata()) {
384                return;
385            }
386
387            self.data.push_record(record, self.threads_enabled);
388        }
389
390        if let Some(data) = self.data.get_data_to_log() {
391            self.print_data(data);
392        }
393    }
394
395    fn flush(&self) {}
396}
397
398pub fn humanize_ms(ms: u128) -> String {
399    match ms {
400        60_000.. => {
401            format!("{}min", ms / 60_000)
402        }
403        1000..60_000 => {
404            format!("{}sec", ms / 1000)
405        }
406        0..1000 => {
407            format!("{}ms", ms)
408        }
409    }
410}
411
412#[cfg(test)]
413mod test {
414    use super::*;
415
416    #[test]
417    fn humanize_tests() {
418        assert_eq!(humanize_ms(10), "10ms");
419        assert_eq!(humanize_ms(500), "500ms");
420        assert_eq!(humanize_ms(1000), "1sec");
421        assert_eq!(humanize_ms(10_000), "10sec");
422        assert_eq!(humanize_ms(59_000), "59sec");
423        assert_eq!(humanize_ms(60_000), "1min");
424    }
425
426    // #[test]
427    // fn file_works() {
428    //     TreeLogger::new()
429    //         .with_colors(true)
430    //         .with_threads(true)
431    //         .with_file("/tmp/logger.txt", true /* append */)
432    //         .init()
433    //         .unwrap();
434    //     log::info!("Did this work?");
435    //     log::info!("Yes it did!");
436    // }
437}