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