Skip to main content

tui_logger/logger/
inner.rs

1use crate::logger::fast_hash::fast_str_hash;
2use crate::{CircularBuffer, LevelConfig, TuiLoggerFile};
3use env_filter::Filter;
4use jiff::Zoned;
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: Zoned,
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: Zoned::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: Zoned, 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 = ExtLogRecord::overrun(
152                reversed[reversed.len() - 1].timestamp.clone(),
153                total,
154                elements,
155            );
156            reversed.push(new_log_entry);
157        }
158        while let Some(log_entry) = reversed.pop() {
159            if tli.targets.get(&log_entry.target).is_none() {
160                let mut default_level = tli.default;
161                if let Some(filter) = tli.filter.as_ref() {
162                    // Let's check, what the environment filter says about this target.
163                    let metadata = log::MetadataBuilder::new()
164                        .level(log_entry.level)
165                        .target(&log_entry.target)
166                        .build();
167                    if filter.enabled(&metadata) {
168                        // There is no direct access to the levelFilter, so we have to iterate over all possible level filters.
169                        for lf in [
170                            LevelFilter::Trace,
171                            LevelFilter::Debug,
172                            LevelFilter::Info,
173                            LevelFilter::Warn,
174                            LevelFilter::Error,
175                        ] {
176                            let metadata = log::MetadataBuilder::new()
177                                .level(lf.to_level().unwrap())
178                                .target(&log_entry.target)
179                                .build();
180                            if filter.enabled(&metadata) {
181                                // Found the related level filter
182                                default_level = lf;
183                                // In order to avoid checking the directives again,
184                                // we store the level filter in the hashtable for the hot path
185                                let h = fast_str_hash(&log_entry.target);
186                                self.hot_select.lock().hashtable.insert(h, lf);
187                                break;
188                            }
189                        }
190                    }
191                }
192                tli.targets.set(&log_entry.target, default_level);
193            }
194            if let Some(ref mut file_options) = tli.dump {
195                let mut output = String::new();
196                let (lev_long, lev_abbr, with_loc) = match log_entry.level {
197                    log::Level::Error => ("ERROR", "E", true),
198                    log::Level::Warn => ("WARN ", "W", true),
199                    log::Level::Info => ("INFO ", "I", false),
200                    log::Level::Debug => ("DEBUG", "D", true),
201                    log::Level::Trace => ("TRACE", "T", true),
202                };
203                if let Some(fmt) = file_options.timestamp_fmt.as_ref() {
204                    output.push_str(&log_entry.timestamp.strftime(fmt).to_string());
205                    output.push(file_options.format_separator);
206                }
207                match file_options.format_output_level {
208                    None => {}
209                    Some(TuiLoggerLevelOutput::Abbreviated) => {
210                        output.push_str(lev_abbr);
211                        output.push(file_options.format_separator);
212                    }
213                    Some(TuiLoggerLevelOutput::Long) => {
214                        output.push_str(lev_long);
215                        output.push(file_options.format_separator);
216                    }
217                }
218                if file_options.format_output_target {
219                    output.push_str(&log_entry.target);
220                    output.push(file_options.format_separator);
221                }
222                if with_loc {
223                    if file_options.format_output_file {
224                        if let Some(file) = log_entry.file() {
225                            output.push_str(file);
226                            output.push(file_options.format_separator);
227                        }
228                    }
229                    if file_options.format_output_line {
230                        if let Some(line) = log_entry.line.as_ref() {
231                            output.push_str(&format!("{}", line));
232                            output.push(file_options.format_separator);
233                        }
234                    }
235                }
236                output.push_str(&log_entry.msg);
237                if let Err(_e) = writeln!(file_options.dump, "{}", output) {
238                    // TODO: What to do in case of write error ?
239                }
240            }
241            tli.events.push(log_entry);
242        }
243    }
244}
245lazy_static! {
246    pub static ref TUI_LOGGER: TuiLogger = {
247        let hs = HotSelect {
248            filter: None,
249            hashtable: HashMap::with_capacity(1000),
250            default: LevelFilter::Info,
251        };
252        let hl = HotLog {
253            events: CircularBuffer::new(1000),
254            mover_thread: None,
255        };
256        let tli = TuiLoggerInner {
257            hot_depth: 1000,
258            events: CircularBuffer::new(10000),
259            total_events: 0,
260            dump: None,
261            default: LevelFilter::Info,
262            targets: LevelConfig::new(),
263            filter: None,
264        };
265        TuiLogger {
266            hot_select: Mutex::new(hs),
267            hot_log: Mutex::new(hl),
268            inner: Mutex::new(tli),
269        }
270    };
271}
272
273impl Log for TuiLogger {
274    fn enabled(&self, metadata: &Metadata) -> bool {
275        let h = fast_str_hash(metadata.target());
276        let hs = self.hot_select.lock();
277        if let Some(&levelfilter) = hs.hashtable.get(&h) {
278            metadata.level() <= levelfilter
279        } else if let Some(envfilter) = hs.filter.as_ref() {
280            envfilter.enabled(metadata)
281        } else {
282            metadata.level() <= hs.default
283        }
284    }
285
286    fn log(&self, record: &Record) {
287        if self.enabled(record.metadata()) {
288            self.raw_log(record)
289        }
290    }
291
292    fn flush(&self) {}
293}
294
295impl TuiLogger {
296    pub fn raw_log(&self, record: &Record) {
297        let log_entry = ExtLogRecord::from(record);
298        let mut events_lock = self.hot_log.lock();
299        events_lock.events.push(log_entry);
300        let need_signal = events_lock
301            .events
302            .total_elements()
303            .is_multiple_of(events_lock.events.capacity() / 2);
304        if need_signal {
305            if let Some(jh) = events_lock.mover_thread.as_ref() {
306                thread::Thread::unpark(jh.thread());
307            }
308        }
309    }
310}