tui_logger/logger/
logger.rs

1use crate::{CircularBuffer, LevelConfig, TuiLoggerFile};
2use chrono::{DateTime, Local};
3use log::{Level, LevelFilter, Log, Metadata, Record};
4use parking_lot::Mutex;
5use std::collections::HashMap;
6use std::io::Write;
7use std::mem;
8use std::thread;
9
10/// The TuiLoggerWidget shows the logging messages in an endless scrolling view.
11/// It is controlled by a TuiWidgetState for selected events.
12#[derive(Debug, Clone, Copy, PartialEq, Hash)]
13pub enum TuiLoggerLevelOutput {
14    Abbreviated,
15    Long,
16}
17/// These are the sub-structs for the static TUI_LOGGER struct.
18pub struct HotSelect {
19    pub hashtable: HashMap<u64, LevelFilter>,
20    pub default: LevelFilter,
21}
22pub struct HotLog {
23    pub events: CircularBuffer<ExtLogRecord>,
24    pub mover_thread: Option<thread::JoinHandle<()>>,
25}
26pub struct ExtLogRecord {
27    pub timestamp: DateTime<Local>,
28    pub level: Level,
29    pub target: String,
30    pub file: String,
31    pub line: u32,
32    pub msg: String,
33}
34pub struct TuiLoggerInner {
35    pub hot_depth: usize,
36    pub events: CircularBuffer<ExtLogRecord>,
37    pub dump: Option<TuiLoggerFile>,
38    pub total_events: usize,
39    pub default: LevelFilter,
40    pub targets: LevelConfig,
41}
42pub struct TuiLogger {
43    pub hot_select: Mutex<HotSelect>,
44    pub hot_log: Mutex<HotLog>,
45    pub inner: Mutex<TuiLoggerInner>,
46}
47impl TuiLogger {
48    pub fn move_events(&self) {
49        // If there are no new events, then just return
50        if self.hot_log.lock().events.total_elements() == 0 {
51            return;
52        }
53        // Exchange new event buffer with the hot buffer
54        let mut received_events = {
55            let hot_depth = self.inner.lock().hot_depth;
56            let new_circular = CircularBuffer::new(hot_depth);
57            let mut hl = self.hot_log.lock();
58            mem::replace(&mut hl.events, new_circular)
59        };
60        let mut tli = self.inner.lock();
61        let total = received_events.total_elements();
62        let elements = received_events.len();
63        tli.total_events += total;
64        let mut consumed = received_events.take();
65        let mut reversed = Vec::with_capacity(consumed.len() + 1);
66        while let Some(log_entry) = consumed.pop() {
67            reversed.push(log_entry);
68        }
69        if total > elements {
70            // Too many events received, so some have been lost
71            let new_log_entry = ExtLogRecord {
72                timestamp: reversed[reversed.len() - 1].timestamp,
73                level: Level::Warn,
74                target: "TuiLogger".to_string(),
75                file: "?".to_string(),
76                line: 0,
77                msg: format!(
78                    "There have been {} events lost, {} recorded out of {}",
79                    total - elements,
80                    elements,
81                    total
82                ),
83            };
84            reversed.push(new_log_entry);
85        }
86        let default_level = tli.default;
87        while let Some(log_entry) = reversed.pop() {
88            if tli.targets.get(&log_entry.target).is_none() {
89                tli.targets.set(&log_entry.target, default_level);
90            }
91            if let Some(ref mut file_options) = tli.dump {
92                let mut output = String::new();
93                let (lev_long, lev_abbr, with_loc) = match log_entry.level {
94                    log::Level::Error => ("ERROR", "E", true),
95                    log::Level::Warn => ("WARN ", "W", true),
96                    log::Level::Info => ("INFO ", "I", false),
97                    log::Level::Debug => ("DEBUG", "D", true),
98                    log::Level::Trace => ("TRACE", "T", true),
99                };
100                if let Some(fmt) = file_options.timestamp_fmt.as_ref() {
101                    output.push_str(&format!("{}", log_entry.timestamp.format(fmt)));
102                    output.push(file_options.format_separator);
103                }
104                match file_options.format_output_level {
105                    None => {}
106                    Some(TuiLoggerLevelOutput::Abbreviated) => {
107                        output.push_str(lev_abbr);
108                        output.push(file_options.format_separator);
109                    }
110                    Some(TuiLoggerLevelOutput::Long) => {
111                        output.push_str(lev_long);
112                        output.push(file_options.format_separator);
113                    }
114                }
115                if file_options.format_output_target {
116                    output.push_str(&log_entry.target);
117                    output.push(file_options.format_separator);
118                }
119                if with_loc {
120                    if file_options.format_output_file {
121                        output.push_str(&log_entry.file);
122                        output.push(file_options.format_separator);
123                    }
124                    if file_options.format_output_line {
125                        output.push_str(&format!("{}", log_entry.line));
126                        output.push(file_options.format_separator);
127                    }
128                }
129                output.push_str(&log_entry.msg);
130                if let Err(_e) = writeln!(file_options.dump, "{}", output) {
131                    // TODO: What to do in case of write error ?
132                }
133            }
134            tli.events.push(log_entry);
135        }
136    }
137}
138lazy_static! {
139    pub static ref TUI_LOGGER: TuiLogger = {
140        let hs = HotSelect {
141            hashtable: HashMap::with_capacity(1000),
142            default: LevelFilter::Info,
143        };
144        let hl = HotLog {
145            events: CircularBuffer::new(1000),
146            mover_thread: None,
147        };
148        let tli = TuiLoggerInner {
149            hot_depth: 1000,
150            events: CircularBuffer::new(10000),
151            total_events: 0,
152            dump: None,
153            default: LevelFilter::Info,
154            targets: LevelConfig::new(),
155        };
156        TuiLogger {
157            hot_select: Mutex::new(hs),
158            hot_log: Mutex::new(hl),
159            inner: Mutex::new(tli),
160        }
161    };
162}
163
164impl Log for TuiLogger {
165    fn enabled(&self, metadata: &Metadata) -> bool {
166        let h = fxhash::hash64(metadata.target());
167        let hs = self.hot_select.lock();
168        if let Some(&levelfilter) = hs.hashtable.get(&h) {
169            metadata.level() <= levelfilter
170        } else {
171            metadata.level() <= hs.default
172        }
173    }
174
175    fn log(&self, record: &Record) {
176        if self.enabled(record.metadata()) {
177            self.raw_log(record)
178        }
179    }
180
181    fn flush(&self) {}
182}
183
184impl TuiLogger {
185    pub fn raw_log(&self, record: &Record) {
186        let log_entry = ExtLogRecord {
187            timestamp: chrono::Local::now(),
188            level: record.level(),
189            target: record.target().to_string(),
190            file: record.file().unwrap_or("?").to_string(),
191            line: record.line().unwrap_or(0),
192            msg: format!("{}", record.args()),
193        };
194        let mut events_lock = self.hot_log.lock();
195        events_lock.events.push(log_entry);
196        let need_signal =
197            (events_lock.events.total_elements() % (events_lock.events.capacity() / 2)) == 0;
198        if need_signal {
199            events_lock
200                .mover_thread
201                .as_ref()
202                .map(|jh| thread::Thread::unpark(jh.thread()));
203        }
204    }
205}