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#[derive(Debug, Clone, Copy, PartialEq, Hash)]
13pub enum TuiLoggerLevelOutput {
14 Abbreviated,
15 Long,
16}
17pub 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 self.hot_log.lock().events.total_elements() == 0 {
127 return;
128 }
129 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 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 }
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}