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