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