tui_logger/logger/
logger.rs

1use crate::{CircularBuffer, LevelConfig, TuiLoggerFile};
2use chrono::{DateTime, Local};
3use env_filter::Filter;
4use log::{Level, LevelFilter, Log, Metadata, Record};
5use parking_lot::Mutex;
6use std::collections::HashMap;
7use std::io::Write;
8use std::mem;
9use std::thread;
10
11/// The TuiLoggerWidget shows the logging messages in an endless scrolling view.
12/// It is controlled by a TuiWidgetState for selected events.
13#[derive(Debug, Clone, Copy, PartialEq, Hash)]
14pub enum TuiLoggerLevelOutput {
15    Abbreviated,
16    Long,
17}
18/// These are the sub-structs for the static TUI_LOGGER struct.
19pub(crate) struct HotSelect {
20    pub filter: Option<Filter>,
21    pub hashtable: HashMap<u64, LevelFilter>,
22    pub default: LevelFilter,
23}
24pub(crate) struct HotLog {
25    pub events: CircularBuffer<ExtLogRecord>,
26    pub mover_thread: Option<thread::JoinHandle<()>>,
27}
28
29enum StringOrStatic {
30    StaticString(&'static str),
31    IsString(String),
32}
33impl StringOrStatic {
34    fn as_str(&self) -> &str {
35        match self {
36            Self::StaticString(s) => s,
37            Self::IsString(s) => &s,
38        }
39    }
40}
41
42pub struct ExtLogRecord {
43    pub timestamp: DateTime<Local>,
44    pub level: Level,
45    target: String,
46    file: Option<StringOrStatic>,
47    module_path: Option<StringOrStatic>,
48    pub line: Option<u32>,
49    msg: String,
50}
51impl ExtLogRecord {
52    #[inline]
53    pub fn target(&self) -> &str {
54        &self.target
55    }
56    #[inline]
57    pub fn file(&self) -> Option<&str> {
58        self.file.as_ref().map(|f| f.as_str())
59    }
60    #[inline]
61    pub fn module_path(&self) -> Option<&str> {
62        self.module_path.as_ref().map(|mp| mp.as_str())
63    }
64    #[inline]
65    pub fn msg(&self) -> &str {
66        &self.msg
67    }
68    fn from(record: &Record) -> Self {
69        let file: Option<StringOrStatic> = record
70            .file_static()
71            .map(|s| StringOrStatic::StaticString(s))
72            .or_else(|| {
73                record
74                    .file()
75                    .map(|s| StringOrStatic::IsString(s.to_string()))
76            });
77        let module_path: Option<StringOrStatic> = record
78            .module_path_static()
79            .map(|s| StringOrStatic::StaticString(s))
80            .or_else(|| {
81                record
82                    .module_path()
83                    .map(|s| StringOrStatic::IsString(s.to_string()))
84            });
85        ExtLogRecord {
86            timestamp: chrono::Local::now(),
87            level: record.level(),
88            target: record.target().to_string(),
89            file,
90            module_path,
91            line: record.line(),
92            msg: format!("{}", record.args()),
93        }
94    }
95    fn overrun(timestamp: DateTime<Local>, total: usize, elements: usize) -> Self {
96        ExtLogRecord {
97            timestamp,
98            level: Level::Warn,
99            target: "TuiLogger".to_string(),
100            file: None,
101            module_path: None,
102            line: None,
103            msg: format!(
104                "There have been {} events lost, {} recorded out of {}",
105                total - elements,
106                elements,
107                total
108            ),
109        }
110    }
111}
112pub(crate) struct TuiLoggerInner {
113    pub hot_depth: usize,
114    pub events: CircularBuffer<ExtLogRecord>,
115    pub dump: Option<TuiLoggerFile>,
116    pub total_events: usize,
117    pub default: LevelFilter,
118    pub targets: LevelConfig,
119    pub filter: Option<Filter>,
120}
121pub struct TuiLogger {
122    pub hot_select: Mutex<HotSelect>,
123    pub hot_log: Mutex<HotLog>,
124    pub inner: Mutex<TuiLoggerInner>,
125}
126impl TuiLogger {
127    pub fn move_events(&self) {
128        // If there are no new events, then just return
129        if self.hot_log.lock().events.total_elements() == 0 {
130            return;
131        }
132        // Exchange new event buffer with the hot buffer
133        let mut received_events = {
134            let hot_depth = self.inner.lock().hot_depth;
135            let new_circular = CircularBuffer::new(hot_depth);
136            let mut hl = self.hot_log.lock();
137            mem::replace(&mut hl.events, new_circular)
138        };
139        let mut tli = self.inner.lock();
140        let total = received_events.total_elements();
141        let elements = received_events.len();
142        tli.total_events += total;
143        let mut consumed = received_events.take();
144        let mut reversed = Vec::with_capacity(consumed.len() + 1);
145        while let Some(log_entry) = consumed.pop() {
146            reversed.push(log_entry);
147        }
148        if total > elements {
149            // Too many events received, so some have been lost
150            let new_log_entry =
151                ExtLogRecord::overrun(reversed[reversed.len() - 1].timestamp, total, elements);
152            reversed.push(new_log_entry);
153        }
154        while let Some(log_entry) = reversed.pop() {
155            if tli.targets.get(&log_entry.target).is_none() {
156                let mut default_level = tli.default;
157                if let Some(filter) = tli.filter.as_ref() {
158                    // Let's check, what the environment filter says about this target.
159                    let metadata = log::MetadataBuilder::new()
160                        .level(log_entry.level)
161                        .target(&log_entry.target)
162                        .build();
163                    if filter.enabled(&metadata) {
164                        // There is no direct access to the levelFilter, so we have to iterate over all possible level filters.
165                        for lf in [
166                            LevelFilter::Trace,
167                            LevelFilter::Debug,
168                            LevelFilter::Info,
169                            LevelFilter::Warn,
170                            LevelFilter::Error,
171                        ] {
172                            let metadata = log::MetadataBuilder::new()
173                                .level(lf.to_level().unwrap())
174                                .target(&log_entry.target)
175                                .build();
176                            if filter.enabled(&metadata) {
177                                // Found the related level filter
178                                default_level = lf;
179                                // In order to avoid checking the directives again,
180                                // we store the level filter in the hashtable for the hot path
181                                let h = fxhash::hash64(&log_entry.target);
182                                self.hot_select.lock().hashtable.insert(h, lf);
183                                break;
184                            }
185                        }
186                    }
187                }
188                tli.targets.set(&log_entry.target, default_level);
189            }
190            if let Some(ref mut file_options) = tli.dump {
191                let mut output = String::new();
192                let (lev_long, lev_abbr, with_loc) = match log_entry.level {
193                    log::Level::Error => ("ERROR", "E", true),
194                    log::Level::Warn => ("WARN ", "W", true),
195                    log::Level::Info => ("INFO ", "I", false),
196                    log::Level::Debug => ("DEBUG", "D", true),
197                    log::Level::Trace => ("TRACE", "T", true),
198                };
199                if let Some(fmt) = file_options.timestamp_fmt.as_ref() {
200                    output.push_str(&format!("{}", log_entry.timestamp.format(fmt)));
201                    output.push(file_options.format_separator);
202                }
203                match file_options.format_output_level {
204                    None => {}
205                    Some(TuiLoggerLevelOutput::Abbreviated) => {
206                        output.push_str(lev_abbr);
207                        output.push(file_options.format_separator);
208                    }
209                    Some(TuiLoggerLevelOutput::Long) => {
210                        output.push_str(lev_long);
211                        output.push(file_options.format_separator);
212                    }
213                }
214                if file_options.format_output_target {
215                    output.push_str(&log_entry.target);
216                    output.push(file_options.format_separator);
217                }
218                if with_loc {
219                    if file_options.format_output_file {
220                        if let Some(file) = log_entry.file() {
221                            output.push_str(file);
222                            output.push(file_options.format_separator);
223                        }
224                    }
225                    if file_options.format_output_line {
226                        if let Some(line) = log_entry.line.as_ref() {
227                            output.push_str(&format!("{}", line));
228                            output.push(file_options.format_separator);
229                        }
230                    }
231                }
232                output.push_str(&log_entry.msg);
233                if let Err(_e) = writeln!(file_options.dump, "{}", output) {
234                    // TODO: What to do in case of write error ?
235                }
236            }
237            tli.events.push(log_entry);
238        }
239    }
240}
241lazy_static! {
242    pub static ref TUI_LOGGER: TuiLogger = {
243        let hs = HotSelect {
244            filter: None,
245            hashtable: HashMap::with_capacity(1000),
246            default: LevelFilter::Info,
247        };
248        let hl = HotLog {
249            events: CircularBuffer::new(1000),
250            mover_thread: None,
251        };
252        let tli = TuiLoggerInner {
253            hot_depth: 1000,
254            events: CircularBuffer::new(10000),
255            total_events: 0,
256            dump: None,
257            default: LevelFilter::Info,
258            targets: LevelConfig::new(),
259            filter: None,
260        };
261        TuiLogger {
262            hot_select: Mutex::new(hs),
263            hot_log: Mutex::new(hl),
264            inner: Mutex::new(tli),
265        }
266    };
267}
268
269impl Log for TuiLogger {
270    fn enabled(&self, metadata: &Metadata) -> bool {
271        let h = fxhash::hash64(metadata.target());
272        let hs = self.hot_select.lock();
273        if let Some(&levelfilter) = hs.hashtable.get(&h) {
274            metadata.level() <= levelfilter
275        } else if let Some(envfilter) = hs.filter.as_ref() {
276            envfilter.enabled(metadata)
277        } else {
278            metadata.level() <= hs.default
279        }
280    }
281
282    fn log(&self, record: &Record) {
283        if self.enabled(record.metadata()) {
284            self.raw_log(record)
285        }
286    }
287
288    fn flush(&self) {}
289}
290
291impl TuiLogger {
292    pub fn raw_log(&self, record: &Record) {
293        let log_entry = ExtLogRecord::from(record);
294        let mut events_lock = self.hot_log.lock();
295        events_lock.events.push(log_entry);
296        let need_signal =
297            (events_lock.events.total_elements() % (events_lock.events.capacity() / 2)) == 0;
298        if need_signal {
299            events_lock
300                .mover_thread
301                .as_ref()
302                .map(|jh| thread::Thread::unpark(jh.thread()));
303        }
304    }
305}