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