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}
26
27enum StringOrStatic {
28    StaticString(&'static str),
29    IsString(String),
30}
31impl StringOrStatic {
32    fn as_str(&self) -> &str {
33        match self {
34            Self::StaticString(s) => s,
35            Self::IsString(s) => &s,
36        }
37    }
38}
39
40pub struct ExtLogRecord {
41    pub timestamp: DateTime<Local>,
42    pub level: Level,
43    target: String,
44    file: Option<StringOrStatic>,
45    module_path: Option<StringOrStatic>,
46    pub line: Option<u32>,
47    msg: String,
48}
49impl ExtLogRecord {
50    #[inline]
51    pub fn target(&self) -> &str {
52        &self.target
53    }
54    #[inline]
55    pub fn file(&self) -> Option<&str> {
56        self.file.as_ref().map(|f| f.as_str())
57    }
58    #[inline]
59    pub fn module_path(&self) -> Option<&str> {
60        self.module_path.as_ref().map(|mp| mp.as_str())
61    }
62    #[inline]
63    pub fn msg(&self) -> &str {
64        &self.msg
65    }
66    fn from(record: &Record) -> Self {
67        let file: Option<StringOrStatic> = record
68            .file_static()
69            .map(|s| StringOrStatic::StaticString(s))
70            .or_else(|| {
71                record
72                    .file()
73                    .map(|s| StringOrStatic::IsString(s.to_string()))
74            });
75        let module_path: Option<StringOrStatic> = record
76            .module_path_static()
77            .map(|s| StringOrStatic::StaticString(s))
78            .or_else(|| {
79                record
80                    .module_path()
81                    .map(|s| StringOrStatic::IsString(s.to_string()))
82            });
83        ExtLogRecord {
84            timestamp: chrono::Local::now(),
85            level: record.level(),
86            target: record.target().to_string(),
87            file,
88            module_path,
89            line: record.line(),
90            msg: format!("{}", record.args()),
91        }
92    }
93    fn overrun(timestamp: DateTime<Local>, total: usize, elements: usize) -> Self {
94        ExtLogRecord {
95            timestamp,
96            level: Level::Warn,
97            target: "TuiLogger".to_string(),
98            file: None,
99            module_path: None,
100            line: None,
101            msg: format!(
102                "There have been {} events lost, {} recorded out of {}",
103                total - elements,
104                elements,
105                total
106            ),
107        }
108    }
109}
110pub struct TuiLoggerInner {
111    pub hot_depth: usize,
112    pub events: CircularBuffer<ExtLogRecord>,
113    pub dump: Option<TuiLoggerFile>,
114    pub total_events: usize,
115    pub default: LevelFilter,
116    pub targets: LevelConfig,
117}
118pub struct TuiLogger {
119    pub hot_select: Mutex<HotSelect>,
120    pub hot_log: Mutex<HotLog>,
121    pub inner: Mutex<TuiLoggerInner>,
122}
123impl TuiLogger {
124    pub fn move_events(&self) {
125        // If there are no new events, then just return
126        if self.hot_log.lock().events.total_elements() == 0 {
127            return;
128        }
129        // Exchange new event buffer with the hot buffer
130        let mut received_events = {
131            let hot_depth = self.inner.lock().hot_depth;
132            let new_circular = CircularBuffer::new(hot_depth);
133            let mut hl = self.hot_log.lock();
134            mem::replace(&mut hl.events, new_circular)
135        };
136        let mut tli = self.inner.lock();
137        let total = received_events.total_elements();
138        let elements = received_events.len();
139        tli.total_events += total;
140        let mut consumed = received_events.take();
141        let mut reversed = Vec::with_capacity(consumed.len() + 1);
142        while let Some(log_entry) = consumed.pop() {
143            reversed.push(log_entry);
144        }
145        if total > elements {
146            // Too many events received, so some have been lost
147            let new_log_entry =
148                ExtLogRecord::overrun(reversed[reversed.len() - 1].timestamp, total, elements);
149            reversed.push(new_log_entry);
150        }
151        let default_level = tli.default;
152        while let Some(log_entry) = reversed.pop() {
153            if tli.targets.get(&log_entry.target).is_none() {
154                tli.targets.set(&log_entry.target, default_level);
155            }
156            if let Some(ref mut file_options) = tli.dump {
157                let mut output = String::new();
158                let (lev_long, lev_abbr, with_loc) = match log_entry.level {
159                    log::Level::Error => ("ERROR", "E", true),
160                    log::Level::Warn => ("WARN ", "W", true),
161                    log::Level::Info => ("INFO ", "I", false),
162                    log::Level::Debug => ("DEBUG", "D", true),
163                    log::Level::Trace => ("TRACE", "T", true),
164                };
165                if let Some(fmt) = file_options.timestamp_fmt.as_ref() {
166                    output.push_str(&format!("{}", log_entry.timestamp.format(fmt)));
167                    output.push(file_options.format_separator);
168                }
169                match file_options.format_output_level {
170                    None => {}
171                    Some(TuiLoggerLevelOutput::Abbreviated) => {
172                        output.push_str(lev_abbr);
173                        output.push(file_options.format_separator);
174                    }
175                    Some(TuiLoggerLevelOutput::Long) => {
176                        output.push_str(lev_long);
177                        output.push(file_options.format_separator);
178                    }
179                }
180                if file_options.format_output_target {
181                    output.push_str(&log_entry.target);
182                    output.push(file_options.format_separator);
183                }
184                if with_loc {
185                    if file_options.format_output_file {
186                        if let Some(file) = log_entry.file() {
187                            output.push_str(file);
188                            output.push(file_options.format_separator);
189                        }
190                    }
191                    if file_options.format_output_line {
192                        if let Some(line) = log_entry.line.as_ref() {
193                            output.push_str(&format!("{}", line));
194                            output.push(file_options.format_separator);
195                        }
196                    }
197                }
198                output.push_str(&log_entry.msg);
199                if let Err(_e) = writeln!(file_options.dump, "{}", output) {
200                    // TODO: What to do in case of write error ?
201                }
202            }
203            tli.events.push(log_entry);
204        }
205    }
206}
207lazy_static! {
208    pub static ref TUI_LOGGER: TuiLogger = {
209        let hs = HotSelect {
210            hashtable: HashMap::with_capacity(1000),
211            default: LevelFilter::Info,
212        };
213        let hl = HotLog {
214            events: CircularBuffer::new(1000),
215            mover_thread: None,
216        };
217        let tli = TuiLoggerInner {
218            hot_depth: 1000,
219            events: CircularBuffer::new(10000),
220            total_events: 0,
221            dump: None,
222            default: LevelFilter::Info,
223            targets: LevelConfig::new(),
224        };
225        TuiLogger {
226            hot_select: Mutex::new(hs),
227            hot_log: Mutex::new(hl),
228            inner: Mutex::new(tli),
229        }
230    };
231}
232
233impl Log for TuiLogger {
234    fn enabled(&self, metadata: &Metadata) -> bool {
235        let h = fxhash::hash64(metadata.target());
236        let hs = self.hot_select.lock();
237        if let Some(&levelfilter) = hs.hashtable.get(&h) {
238            metadata.level() <= levelfilter
239        } else {
240            metadata.level() <= hs.default
241        }
242    }
243
244    fn log(&self, record: &Record) {
245        if self.enabled(record.metadata()) {
246            self.raw_log(record)
247        }
248    }
249
250    fn flush(&self) {}
251}
252
253impl TuiLogger {
254    pub fn raw_log(&self, record: &Record) {
255        let log_entry = ExtLogRecord::from(record);
256        let mut events_lock = self.hot_log.lock();
257        events_lock.events.push(log_entry);
258        let need_signal =
259            (events_lock.events.total_elements() % (events_lock.events.capacity() / 2)) == 0;
260        if need_signal {
261            events_lock
262                .mover_thread
263                .as_ref()
264                .map(|jh| thread::Thread::unpark(jh.thread()));
265        }
266    }
267}