1use 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 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 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 }
180}
181
182impl Default for TreeLogger {
183 fn default() -> Self {
184 Self::new()
185 }
186}
187
188impl TreeLogger {
189 #[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 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 #[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 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 }