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 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 self.hot_log.lock().events.total_elements() == 0 {
131 return;
132 }
133 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 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 }
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}