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    Empty,
31}
32impl StringOrStatic {
33    fn as_str(&self) -> &str {
34        match self {
35            Self::StaticString(s) => s,
36            Self::IsString(s) => &s,
37            Self::Empty => "?",
38        }
39    }
40}
41
42pub struct ExtLogRecord {
43    pub timestamp: DateTime<Local>,
44    pub level: Level,
45    target: String,
46    file: StringOrStatic,
47    module_path: StringOrStatic,
48    pub line: u32,
49    msg: String,
50}
51impl ExtLogRecord {
52    #[inline]
53    pub fn target(&self) -> &str {
54        &self.target
55    }
56    #[inline]
57    pub fn file(&self) -> &str {
58        self.file.as_str()
59    }
60    #[inline]
61    pub fn module_path(&self) -> &str {
62        self.module_path.as_str()
63    }
64    #[inline]
65    pub fn msg(&self) -> &str {
66        &self.msg
67    }
68    fn from(record: &Record) -> Self {
69        let file: StringOrStatic = record
70            .file_static()
71            .map(|s| StringOrStatic::StaticString(s))
72            .or_else(|| {
73                record
74                    .file()
75                    .map(|s| StringOrStatic::IsString(s.to_string()))
76            })
77            .unwrap_or_else(|| StringOrStatic::Empty);
78        let module_path: StringOrStatic = record
79            .module_path_static()
80            .map(|s| StringOrStatic::StaticString(s))
81            .or_else(|| {
82                record
83                    .module_path()
84                    .map(|s| StringOrStatic::IsString(s.to_string()))
85            })
86            .unwrap_or_else(|| StringOrStatic::Empty);
87        ExtLogRecord {
88            timestamp: chrono::Local::now(),
89            level: record.level(),
90            target: record.target().to_string(),
91            file,
92            module_path,
93            line: record.line().unwrap_or(0),
94            msg: format!("{}", record.args()),
95        }
96    }
97    fn overrun(timestamp: DateTime<Local>, total: usize, elements: usize) -> Self {
98        ExtLogRecord {
99            timestamp,
100            level: Level::Warn,
101            target: "TuiLogger".to_string(),
102            file: StringOrStatic::Empty,
103            module_path: StringOrStatic::Empty,
104            line: 0,
105            msg: format!(
106                "There have been {} events lost, {} recorded out of {}",
107                total - elements,
108                elements,
109                total
110            ),
111        }
112    }
113}
114pub struct TuiLoggerInner {
115    pub hot_depth: usize,
116    pub events: CircularBuffer<ExtLogRecord>,
117    pub dump: Option<TuiLoggerFile>,
118    pub total_events: usize,
119    pub default: LevelFilter,
120    pub targets: LevelConfig,
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        let default_level = tli.default;
156        while let Some(log_entry) = reversed.pop() {
157            if tli.targets.get(&log_entry.target).is_none() {
158                tli.targets.set(&log_entry.target, default_level);
159            }
160            if let Some(ref mut file_options) = tli.dump {
161                let mut output = String::new();
162                let (lev_long, lev_abbr, with_loc) = match log_entry.level {
163                    log::Level::Error => ("ERROR", "E", true),
164                    log::Level::Warn => ("WARN ", "W", true),
165                    log::Level::Info => ("INFO ", "I", false),
166                    log::Level::Debug => ("DEBUG", "D", true),
167                    log::Level::Trace => ("TRACE", "T", true),
168                };
169                if let Some(fmt) = file_options.timestamp_fmt.as_ref() {
170                    output.push_str(&format!("{}", log_entry.timestamp.format(fmt)));
171                    output.push(file_options.format_separator);
172                }
173                match file_options.format_output_level {
174                    None => {}
175                    Some(TuiLoggerLevelOutput::Abbreviated) => {
176                        output.push_str(lev_abbr);
177                        output.push(file_options.format_separator);
178                    }
179                    Some(TuiLoggerLevelOutput::Long) => {
180                        output.push_str(lev_long);
181                        output.push(file_options.format_separator);
182                    }
183                }
184                if file_options.format_output_target {
185                    output.push_str(&log_entry.target);
186                    output.push(file_options.format_separator);
187                }
188                if with_loc {
189                    if file_options.format_output_file {
190                        output.push_str(log_entry.file.as_str());
191                        output.push(file_options.format_separator);
192                    }
193                    if file_options.format_output_line {
194                        output.push_str(&format!("{}", log_entry.line));
195                        output.push(file_options.format_separator);
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}