1use crate::{CircularBuffer, LevelConfig, TuiLoggerFile};
2use chrono::{DateTime, Local};
3use env_filter::Filter;
4use log::{Level, LevelFilter, Log, Metadata, Record};
5use parking_lot::Mutex;
6use std::collections::HashMap;
7use std::io::Write;
8use std::mem;
9use std::thread;
10
11#[derive(Debug, Clone, Copy, PartialEq, Hash)]
14pub enum TuiLoggerLevelOutput {
15 Abbreviated,
16 Long,
17}
18pub(crate) struct HotSelect {
20 pub filter: Option<Filter>,
21 pub hashtable: HashMap<u64, LevelFilter>,
22 pub default: LevelFilter,
23}
24pub(crate) struct HotLog {
25 pub events: CircularBuffer<ExtLogRecord>,
26 pub mover_thread: Option<thread::JoinHandle<()>>,
27}
28
29enum StringOrStatic {
30 StaticString(&'static str),
31 IsString(String),
32}
33impl StringOrStatic {
34 fn as_str(&self) -> &str {
35 match self {
36 Self::StaticString(s) => s,
37 Self::IsString(s) => &s,
38 }
39 }
40}
41
42pub struct ExtLogRecord {
43 pub timestamp: DateTime<Local>,
44 pub level: Level,
45 target: String,
46 file: Option<StringOrStatic>,
47 module_path: Option<StringOrStatic>,
48 pub line: Option<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) -> Option<&str> {
58 self.file.as_ref().map(|f| f.as_str())
59 }
60 #[inline]
61 pub fn module_path(&self) -> Option<&str> {
62 self.module_path.as_ref().map(|mp| mp.as_str())
63 }
64 #[inline]
65 pub fn msg(&self) -> &str {
66 &self.msg
67 }
68 fn from(record: &Record) -> Self {
69 let file: Option<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 let module_path: Option<StringOrStatic> = record
78 .module_path_static()
79 .map(|s| StringOrStatic::StaticString(s))
80 .or_else(|| {
81 record
82 .module_path()
83 .map(|s| StringOrStatic::IsString(s.to_string()))
84 });
85 ExtLogRecord {
86 timestamp: chrono::Local::now(),
87 level: record.level(),
88 target: record.target().to_string(),
89 file,
90 module_path,
91 line: record.line(),
92 msg: format!("{}", record.args()),
93 }
94 }
95 fn overrun(timestamp: DateTime<Local>, total: usize, elements: usize) -> Self {
96 ExtLogRecord {
97 timestamp,
98 level: Level::Warn,
99 target: "TuiLogger".to_string(),
100 file: None,
101 module_path: None,
102 line: None,
103 msg: format!(
104 "There have been {} events lost, {} recorded out of {}",
105 total - elements,
106 elements,
107 total
108 ),
109 }
110 }
111}
112pub(crate) struct TuiLoggerInner {
113 pub hot_depth: usize,
114 pub events: CircularBuffer<ExtLogRecord>,
115 pub dump: Option<TuiLoggerFile>,
116 pub total_events: usize,
117 pub default: LevelFilter,
118 pub targets: LevelConfig,
119 pub filter: Option<Filter>,
120}
121pub struct TuiLogger {
122 pub hot_select: Mutex<HotSelect>,
123 pub hot_log: Mutex<HotLog>,
124 pub inner: Mutex<TuiLoggerInner>,
125}
126impl TuiLogger {
127 pub fn move_events(&self) {
128 if self.hot_log.lock().events.total_elements() == 0 {
130 return;
131 }
132 let mut received_events = {
134 let hot_depth = self.inner.lock().hot_depth;
135 let new_circular = CircularBuffer::new(hot_depth);
136 let mut hl = self.hot_log.lock();
137 mem::replace(&mut hl.events, new_circular)
138 };
139 let mut tli = self.inner.lock();
140 let total = received_events.total_elements();
141 let elements = received_events.len();
142 tli.total_events += total;
143 let mut consumed = received_events.take();
144 let mut reversed = Vec::with_capacity(consumed.len() + 1);
145 while let Some(log_entry) = consumed.pop() {
146 reversed.push(log_entry);
147 }
148 if total > elements {
149 let new_log_entry =
151 ExtLogRecord::overrun(reversed[reversed.len() - 1].timestamp, total, elements);
152 reversed.push(new_log_entry);
153 }
154 while let Some(log_entry) = reversed.pop() {
155 if tli.targets.get(&log_entry.target).is_none() {
156 let mut default_level = tli.default;
157 if let Some(filter) = tli.filter.as_ref() {
158 let metadata = log::MetadataBuilder::new()
160 .level(log_entry.level)
161 .target(&log_entry.target)
162 .build();
163 if filter.enabled(&metadata) {
164 for lf in [
166 LevelFilter::Trace,
167 LevelFilter::Debug,
168 LevelFilter::Info,
169 LevelFilter::Warn,
170 LevelFilter::Error,
171 ] {
172 let metadata = log::MetadataBuilder::new()
173 .level(lf.to_level().unwrap())
174 .target(&log_entry.target)
175 .build();
176 if filter.enabled(&metadata) {
177 default_level = lf;
179 let h = fxhash::hash64(&log_entry.target);
182 self.hot_select.lock().hashtable.insert(h, lf);
183 break;
184 }
185 }
186 }
187 }
188 tli.targets.set(&log_entry.target, default_level);
189 }
190 if let Some(ref mut file_options) = tli.dump {
191 let mut output = String::new();
192 let (lev_long, lev_abbr, with_loc) = match log_entry.level {
193 log::Level::Error => ("ERROR", "E", true),
194 log::Level::Warn => ("WARN ", "W", true),
195 log::Level::Info => ("INFO ", "I", false),
196 log::Level::Debug => ("DEBUG", "D", true),
197 log::Level::Trace => ("TRACE", "T", true),
198 };
199 if let Some(fmt) = file_options.timestamp_fmt.as_ref() {
200 output.push_str(&format!("{}", log_entry.timestamp.format(fmt)));
201 output.push(file_options.format_separator);
202 }
203 match file_options.format_output_level {
204 None => {}
205 Some(TuiLoggerLevelOutput::Abbreviated) => {
206 output.push_str(lev_abbr);
207 output.push(file_options.format_separator);
208 }
209 Some(TuiLoggerLevelOutput::Long) => {
210 output.push_str(lev_long);
211 output.push(file_options.format_separator);
212 }
213 }
214 if file_options.format_output_target {
215 output.push_str(&log_entry.target);
216 output.push(file_options.format_separator);
217 }
218 if with_loc {
219 if file_options.format_output_file {
220 if let Some(file) = log_entry.file() {
221 output.push_str(file);
222 output.push(file_options.format_separator);
223 }
224 }
225 if file_options.format_output_line {
226 if let Some(line) = log_entry.line.as_ref() {
227 output.push_str(&format!("{}", line));
228 output.push(file_options.format_separator);
229 }
230 }
231 }
232 output.push_str(&log_entry.msg);
233 if let Err(_e) = writeln!(file_options.dump, "{}", output) {
234 }
236 }
237 tli.events.push(log_entry);
238 }
239 }
240}
241lazy_static! {
242 pub static ref TUI_LOGGER: TuiLogger = {
243 let hs = HotSelect {
244 filter: None,
245 hashtable: HashMap::with_capacity(1000),
246 default: LevelFilter::Info,
247 };
248 let hl = HotLog {
249 events: CircularBuffer::new(1000),
250 mover_thread: None,
251 };
252 let tli = TuiLoggerInner {
253 hot_depth: 1000,
254 events: CircularBuffer::new(10000),
255 total_events: 0,
256 dump: None,
257 default: LevelFilter::Info,
258 targets: LevelConfig::new(),
259 filter: None,
260 };
261 TuiLogger {
262 hot_select: Mutex::new(hs),
263 hot_log: Mutex::new(hl),
264 inner: Mutex::new(tli),
265 }
266 };
267}
268
269impl Log for TuiLogger {
270 fn enabled(&self, metadata: &Metadata) -> bool {
271 let h = fxhash::hash64(metadata.target());
272 let hs = self.hot_select.lock();
273 if let Some(&levelfilter) = hs.hashtable.get(&h) {
274 metadata.level() <= levelfilter
275 } else if let Some(envfilter) = hs.filter.as_ref() {
276 envfilter.enabled(metadata)
277 } else {
278 metadata.level() <= hs.default
279 }
280 }
281
282 fn log(&self, record: &Record) {
283 if self.enabled(record.metadata()) {
284 self.raw_log(record)
285 }
286 }
287
288 fn flush(&self) {}
289}
290
291impl TuiLogger {
292 pub fn raw_log(&self, record: &Record) {
293 let log_entry = ExtLogRecord::from(record);
294 let mut events_lock = self.hot_log.lock();
295 events_lock.events.push(log_entry);
296 let need_signal =
297 (events_lock.events.total_elements() % (events_lock.events.capacity() / 2)) == 0;
298 if need_signal {
299 events_lock
300 .mover_thread
301 .as_ref()
302 .map(|jh| thread::Thread::unpark(jh.thread()));
303 }
304 }
305}