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