tui_logger/logger/
inner.rs

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