1#![cfg_attr(docsrs, feature(doc_cfg))]
187#[macro_use]
188extern crate lazy_static;
189
190use std::collections::hash_map::Iter;
191use std::collections::hash_map::Keys;
192use std::collections::HashMap;
193use std::io::Write;
194use std::mem;
195use std::sync::Arc;
196use std::thread;
197
198use chrono::{DateTime, Local};
199use log::{Level, Log, Metadata, Record, SetLoggerError};
200use parking_lot::Mutex;
201use ratatui::{
202 buffer::Buffer,
203 layout::Rect,
204 style::{Modifier, Style},
205 widgets::{Block, Widget},
206};
207use widget::inner::TuiLoggerInner;
208use widget::inner::TuiWidgetInnerState;
209
210mod circular;
211#[cfg(feature = "slog-support")]
212#[cfg_attr(docsrs, doc(cfg(feature = "slog-support")))]
213mod slog;
214#[cfg(feature = "tracing-support")]
215#[cfg_attr(docsrs, doc(cfg(feature = "tracing-support")))]
216mod tracing_subscriber;
217
218pub use crate::circular::CircularBuffer;
219#[cfg(feature = "slog-support")]
220#[cfg_attr(docsrs, doc(cfg(feature = "slog-support")))]
221pub use crate::slog::TuiSlogDrain;
222#[cfg(feature = "tracing-support")]
223#[cfg_attr(docsrs, doc(cfg(feature = "tracing-support")))]
224pub use crate::tracing_subscriber::TuiTracingSubscriberLayer;
225#[doc(no_inline)]
226pub use log::LevelFilter;
227
228pub mod widget;
229pub use widget::inner::TuiWidgetState;
230pub use widget::logformatter::LogFormatter;
231pub use widget::smart::TuiLoggerSmartWidget;
232pub use widget::standard::TuiLoggerWidget;
233
234pub mod file;
235pub use file::TuiLoggerFile;
236
237pub struct ExtLogRecord {
238 pub timestamp: DateTime<Local>,
239 pub level: Level,
240 pub target: String,
241 pub file: String,
242 pub line: u32,
243 pub msg: String,
244}
245
246fn advance_levelfilter(levelfilter: LevelFilter) -> (Option<LevelFilter>, Option<LevelFilter>) {
247 match levelfilter {
248 LevelFilter::Trace => (None, Some(LevelFilter::Debug)),
249 LevelFilter::Debug => (Some(LevelFilter::Trace), Some(LevelFilter::Info)),
250 LevelFilter::Info => (Some(LevelFilter::Debug), Some(LevelFilter::Warn)),
251 LevelFilter::Warn => (Some(LevelFilter::Info), Some(LevelFilter::Error)),
252 LevelFilter::Error => (Some(LevelFilter::Warn), Some(LevelFilter::Off)),
253 LevelFilter::Off => (Some(LevelFilter::Error), None),
254 }
255}
256
257#[derive(Default)]
264pub struct LevelConfig {
265 config: HashMap<String, LevelFilter>,
266 generation: u64,
267 origin_generation: u64,
268 default_display_level: Option<LevelFilter>,
269}
270impl LevelConfig {
271 pub fn new() -> LevelConfig {
273 LevelConfig {
274 config: HashMap::new(),
275 generation: 0,
276 origin_generation: 0,
277 default_display_level: None,
278 }
279 }
280 pub fn set(&mut self, target: &str, level: LevelFilter) {
282 if let Some(lev) = self.config.get_mut(target) {
283 if *lev != level {
284 *lev = level;
285 self.generation += 1;
286 }
287 return;
288 }
289 self.config.insert(target.to_string(), level);
290 self.generation += 1;
291 }
292 pub fn set_default_display_level(&mut self, level: LevelFilter) {
294 self.default_display_level = Some(level);
295 }
296 pub fn keys(&self) -> Keys<String, LevelFilter> {
298 self.config.keys()
299 }
300 pub fn get(&self, target: &str) -> Option<LevelFilter> {
302 self.config.get(target).cloned()
303 }
304 pub fn iter(&self) -> Iter<String, LevelFilter> {
306 self.config.iter()
307 }
308 fn merge(&mut self, origin: &LevelConfig) {
314 if self.origin_generation != origin.generation {
315 for (target, origin_levelfilter) in origin.iter() {
316 if let Some(levelfilter) = self.get(target) {
317 if levelfilter <= *origin_levelfilter {
318 continue;
319 }
320 }
321 let levelfilter = self
322 .default_display_level
323 .map(|lvl| {
324 if lvl > *origin_levelfilter {
325 *origin_levelfilter
326 } else {
327 lvl
328 }
329 })
330 .unwrap_or(*origin_levelfilter);
331 self.set(target, levelfilter);
332 }
333 self.generation = origin.generation;
334 }
335 }
336}
337
338struct HotSelect {
340 hashtable: HashMap<u64, LevelFilter>,
341 default: LevelFilter,
342}
343struct HotLog {
344 events: CircularBuffer<ExtLogRecord>,
345 mover_thread: Option<thread::JoinHandle<()>>,
346}
347
348struct TuiLogger {
349 hot_select: Mutex<HotSelect>,
350 hot_log: Mutex<HotLog>,
351 inner: Mutex<TuiLoggerInner>,
352}
353impl TuiLogger {
354 pub fn move_events(&self) {
355 if self.hot_log.lock().events.total_elements() == 0 {
357 return;
358 }
359 let mut received_events = {
361 let hot_depth = self.inner.lock().hot_depth;
362 let new_circular = CircularBuffer::new(hot_depth);
363 let mut hl = self.hot_log.lock();
364 mem::replace(&mut hl.events, new_circular)
365 };
366 let mut tli = self.inner.lock();
367 let total = received_events.total_elements();
368 let elements = received_events.len();
369 tli.total_events += total;
370 let mut consumed = received_events.take();
371 let mut reversed = Vec::with_capacity(consumed.len() + 1);
372 while let Some(log_entry) = consumed.pop() {
373 reversed.push(log_entry);
374 }
375 if total > elements {
376 let new_log_entry = ExtLogRecord {
378 timestamp: reversed[reversed.len() - 1].timestamp,
379 level: Level::Warn,
380 target: "TuiLogger".to_string(),
381 file: "?".to_string(),
382 line: 0,
383 msg: format!(
384 "There have been {} events lost, {} recorded out of {}",
385 total - elements,
386 elements,
387 total
388 ),
389 };
390 reversed.push(new_log_entry);
391 }
392 let default_level = tli.default;
393 while let Some(log_entry) = reversed.pop() {
394 if tli.targets.get(&log_entry.target).is_none() {
395 tli.targets.set(&log_entry.target, default_level);
396 }
397 if let Some(ref mut file_options) = tli.dump {
398 let mut output = String::new();
399 let (lev_long, lev_abbr, with_loc) = match log_entry.level {
400 log::Level::Error => ("ERROR", "E", true),
401 log::Level::Warn => ("WARN ", "W", true),
402 log::Level::Info => ("INFO ", "I", false),
403 log::Level::Debug => ("DEBUG", "D", true),
404 log::Level::Trace => ("TRACE", "T", true),
405 };
406 if let Some(fmt) = file_options.timestamp_fmt.as_ref() {
407 output.push_str(&format!("{}", log_entry.timestamp.format(fmt)));
408 output.push(file_options.format_separator);
409 }
410 match file_options.format_output_level {
411 None => {}
412 Some(TuiLoggerLevelOutput::Abbreviated) => {
413 output.push_str(lev_abbr);
414 output.push(file_options.format_separator);
415 }
416 Some(TuiLoggerLevelOutput::Long) => {
417 output.push_str(lev_long);
418 output.push(file_options.format_separator);
419 }
420 }
421 if file_options.format_output_target {
422 output.push_str(&log_entry.target);
423 output.push(file_options.format_separator);
424 }
425 if with_loc {
426 if file_options.format_output_file {
427 output.push_str(&log_entry.file);
428 output.push(file_options.format_separator);
429 }
430 if file_options.format_output_line {
431 output.push_str(&format!("{}", log_entry.line));
432 output.push(file_options.format_separator);
433 }
434 }
435 output.push_str(&log_entry.msg);
436 if let Err(_e) = writeln!(file_options.dump, "{}", output) {
437 }
439 }
440 tli.events.push(log_entry);
441 }
442 }
443}
444lazy_static! {
445 static ref TUI_LOGGER: TuiLogger = {
446 let hs = HotSelect {
447 hashtable: HashMap::with_capacity(1000),
448 default: LevelFilter::Info,
449 };
450 let hl = HotLog {
451 events: CircularBuffer::new(1000),
452 mover_thread: None,
453 };
454 let tli = TuiLoggerInner {
455 hot_depth: 1000,
456 events: CircularBuffer::new(10000),
457 total_events: 0,
458 dump: None,
459 default: LevelFilter::Info,
460 targets: LevelConfig::new(),
461 };
462 TuiLogger {
463 hot_select: Mutex::new(hs),
464 hot_log: Mutex::new(hl),
465 inner: Mutex::new(tli),
466 }
467 };
468}
469
470#[derive(Debug)]
472pub enum TuiLoggerError {
473 SetLoggerError(SetLoggerError),
474 ThreadError(std::io::Error),
475}
476impl std::error::Error for TuiLoggerError {
477 fn description(&self) -> &str {
478 match self {
479 TuiLoggerError::SetLoggerError(_) => "SetLoggerError",
480 TuiLoggerError::ThreadError(_) => "ThreadError",
481 }
482 }
483 fn cause(&self) -> Option<&dyn std::error::Error> {
484 match self {
485 TuiLoggerError::SetLoggerError(_) => None,
486 TuiLoggerError::ThreadError(err) => Some(err),
487 }
488 }
489}
490impl std::fmt::Display for TuiLoggerError {
491 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492 match self {
493 TuiLoggerError::SetLoggerError(err) => write!(f, "SetLoggerError({})", err),
494 TuiLoggerError::ThreadError(err) => write!(f, "ThreadError({})", err),
495 }
496 }
497}
498
499pub fn init_logger(max_level: LevelFilter) -> Result<(), TuiLoggerError> {
501 let join_handle = thread::Builder::new()
502 .name("tui-logger::move_events".into())
503 .spawn(|| {
504 let duration = std::time::Duration::from_millis(10);
505 loop {
506 thread::park_timeout(duration);
507 TUI_LOGGER.move_events();
508 }
509 })
510 .map_err(|err| TuiLoggerError::ThreadError(err))?;
511 TUI_LOGGER.hot_log.lock().mover_thread = Some(join_handle);
512 if cfg!(feature = "tracing-support") {
513 set_default_level(max_level);
514 Ok(())
515 } else {
516 log::set_max_level(max_level);
517 log::set_logger(&*TUI_LOGGER).map_err(|err| TuiLoggerError::SetLoggerError(err))
518 }
519}
520
521#[cfg(feature = "slog-support")]
522#[cfg_attr(docsrs, doc(cfg(feature = "slog-support")))]
523pub fn slog_drain() -> TuiSlogDrain {
524 TuiSlogDrain
525}
526
527#[cfg(feature = "tracing-support")]
528#[cfg_attr(docsrs, doc(cfg(feature = "tracing-support")))]
529pub fn tracing_subscriber_layer() -> TuiTracingSubscriberLayer {
530 TuiTracingSubscriberLayer
531}
532
533pub fn set_hot_buffer_depth(depth: usize) {
536 TUI_LOGGER.inner.lock().hot_depth = depth;
537}
538
539pub fn set_buffer_depth(depth: usize) {
542 TUI_LOGGER.inner.lock().events = CircularBuffer::new(depth);
543}
544
545pub fn set_log_file(file_options: TuiLoggerFile) {
547 TUI_LOGGER.inner.lock().dump = Some(file_options);
548}
549
550pub fn set_default_level(levelfilter: LevelFilter) {
552 TUI_LOGGER.hot_select.lock().default = levelfilter;
553 TUI_LOGGER.inner.lock().default = levelfilter;
554}
555
556pub fn set_level_for_target(target: &str, levelfilter: LevelFilter) {
558 let h = fxhash::hash64(&target);
559 TUI_LOGGER.inner.lock().targets.set(target, levelfilter);
560 let mut hs = TUI_LOGGER.hot_select.lock();
561 hs.hashtable.insert(h, levelfilter);
562}
563
564impl TuiLogger {
565 fn raw_log(&self, record: &Record) {
566 let log_entry = ExtLogRecord {
567 timestamp: chrono::Local::now(),
568 level: record.level(),
569 target: record.target().to_string(),
570 file: record.file().unwrap_or("?").to_string(),
571 line: record.line().unwrap_or(0),
572 msg: format!("{}", record.args()),
573 };
574 let mut events_lock = self.hot_log.lock();
575 events_lock.events.push(log_entry);
576 let need_signal =
577 (events_lock.events.total_elements() % (events_lock.events.capacity() / 2)) == 0;
578 if need_signal {
579 events_lock
580 .mover_thread
581 .as_ref()
582 .map(|jh| thread::Thread::unpark(jh.thread()));
583 }
584 }
585}
586
587impl Log for TuiLogger {
588 fn enabled(&self, metadata: &Metadata) -> bool {
589 let h = fxhash::hash64(metadata.target());
590 let hs = self.hot_select.lock();
591 if let Some(&levelfilter) = hs.hashtable.get(&h) {
592 metadata.level() <= levelfilter
593 } else {
594 metadata.level() <= hs.default
595 }
596 }
597
598 fn log(&self, record: &Record) {
599 if self.enabled(record.metadata()) {
600 self.raw_log(record)
601 }
602 }
603
604 fn flush(&self) {}
605}
606
607#[derive(Default)]
609pub struct Drain;
610
611impl Drain {
612 pub fn new() -> Self {
614 Drain
615 }
616 pub fn log(&self, record: &Record) {
618 TUI_LOGGER.raw_log(record)
619 }
620}
621
622#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
623pub enum TuiWidgetEvent {
624 SpaceKey,
625 UpKey,
626 DownKey,
627 LeftKey,
628 RightKey,
629 PlusKey,
630 MinusKey,
631 HideKey,
632 FocusKey,
633 PrevPageKey,
634 NextPageKey,
635 EscapeKey,
636}
637
638pub struct TuiLoggerTargetWidget<'b> {
641 block: Option<Block<'b>>,
642 style: Style,
644 style_show: Style,
645 style_hide: Style,
646 style_off: Option<Style>,
647 highlight_style: Style,
648 state: Arc<Mutex<TuiWidgetInnerState>>,
649 targets: Vec<String>,
650}
651impl<'b> Default for TuiLoggerTargetWidget<'b> {
652 fn default() -> TuiLoggerTargetWidget<'b> {
653 TuiLoggerTargetWidget {
655 block: None,
656 style: Default::default(),
657 style_off: None,
658 style_hide: Style::default(),
659 style_show: Style::default().add_modifier(Modifier::REVERSED),
660 highlight_style: Style::default().add_modifier(Modifier::REVERSED),
661 state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
662 targets: vec![],
663 }
664 }
665}
666impl<'b> TuiLoggerTargetWidget<'b> {
667 pub fn block(mut self, block: Block<'b>) -> TuiLoggerTargetWidget<'b> {
668 self.block = Some(block);
669 self
670 }
671 fn opt_style(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
672 if let Some(s) = style {
673 self.style = s;
674 }
675 self
676 }
677 fn opt_style_off(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
678 if style.is_some() {
679 self.style_off = style;
680 }
681 self
682 }
683 fn opt_style_hide(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
684 if let Some(s) = style {
685 self.style_hide = s;
686 }
687 self
688 }
689 fn opt_style_show(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
690 if let Some(s) = style {
691 self.style_show = s;
692 }
693 self
694 }
695 fn opt_highlight_style(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
696 if let Some(s) = style {
697 self.highlight_style = s;
698 }
699 self
700 }
701 pub fn style(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
702 self.style = style;
703 self
704 }
705 pub fn style_off(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
706 self.style_off = Some(style);
707 self
708 }
709 pub fn style_hide(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
710 self.style_hide = style;
711 self
712 }
713 pub fn style_show(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
714 self.style_show = style;
715 self
716 }
717 pub fn highlight_style(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
718 self.highlight_style = style;
719 self
720 }
721 fn inner_state(mut self, state: Arc<Mutex<TuiWidgetInnerState>>) -> TuiLoggerTargetWidget<'b> {
722 self.state = state;
723 self
724 }
725 pub fn state(mut self, state: &TuiWidgetState) -> TuiLoggerTargetWidget<'b> {
726 self.state = state.inner.clone();
727 self
728 }
729}
730impl<'b> Widget for TuiLoggerTargetWidget<'b> {
731 fn render(mut self, area: Rect, buf: &mut Buffer) {
732 buf.set_style(area, self.style);
733 let list_area = match self.block.take() {
734 Some(b) => {
735 let inner_area = b.inner(area);
736 b.render(area, buf);
737 inner_area
738 }
739 None => area,
740 };
741 if list_area.width < 8 || list_area.height < 1 {
742 return;
743 }
744
745 let la_left = list_area.left();
746 let la_top = list_area.top();
747 let la_width = list_area.width as usize;
748
749 {
750 let inner = &TUI_LOGGER.inner.lock();
751 let hot_targets = &inner.targets;
752 let mut state = self.state.lock();
753 let hide_off = state.hide_off;
754 let offset = state.offset;
755 let focus_selected = state.focus_selected;
756 {
757 let targets = &mut state.config;
758 targets.merge(hot_targets);
759 self.targets.clear();
760 for (t, levelfilter) in targets.iter() {
761 if hide_off && levelfilter == &LevelFilter::Off {
762 continue;
763 }
764 self.targets.push(t.clone());
765 }
766 self.targets.sort();
767 }
768 state.nr_items = self.targets.len();
769 if state.selected >= state.nr_items {
770 state.selected = state.nr_items.max(1) - 1;
771 }
772 if state.selected < state.nr_items {
773 state.opt_selected_target = Some(self.targets[state.selected].clone());
774 let t = &self.targets[state.selected];
775 let (more, less) = if let Some(levelfilter) = state.config.get(t) {
776 advance_levelfilter(levelfilter)
777 } else {
778 (None, None)
779 };
780 state.opt_selected_visibility_less = less;
781 state.opt_selected_visibility_more = more;
782 let (more, less) = if let Some(levelfilter) = hot_targets.get(t) {
783 advance_levelfilter(levelfilter)
784 } else {
785 (None, None)
786 };
787 state.opt_selected_recording_less = less;
788 state.opt_selected_recording_more = more;
789 }
790 let list_height = (list_area.height as usize).min(self.targets.len());
791 let offset = if list_height > self.targets.len() {
792 0
793 } else if state.selected < state.nr_items {
794 let sel = state.selected;
795 if sel >= offset + list_height {
796 sel - list_height + 1
798 } else if sel.min(offset) + list_height > self.targets.len() {
799 self.targets.len() - list_height
800 } else {
801 sel.min(offset)
802 }
803 } else {
804 0
805 };
806 state.offset = offset;
807
808 let targets = &(&state.config);
809 let default_level = inner.default;
810 for i in 0..list_height {
811 let t = &self.targets[i + offset];
812 let hot_level_filter = hot_targets.get(t).unwrap_or(default_level);
822 let level_filter = targets.get(t).unwrap_or(default_level);
823 for (j, sym, lev) in &[
824 (0, "E", Level::Error),
825 (1, "W", Level::Warn),
826 (2, "I", Level::Info),
827 (3, "D", Level::Debug),
828 (4, "T", Level::Trace),
829 ] {
830 if let Some(cell) = buf.cell_mut((la_left + j, la_top + i as u16)) {
831 let cell_style = if hot_level_filter >= *lev {
832 if level_filter >= *lev {
833 if !focus_selected || i + offset == state.selected {
834 self.style_show
835 } else {
836 self.style_hide
837 }
838 } else {
839 self.style_hide
840 }
841 } else if let Some(style_off) = self.style_off {
842 style_off
843 } else {
844 cell.set_symbol(" ");
845 continue;
846 };
847 cell.set_style(cell_style);
848 cell.set_symbol(sym);
849 }
850 }
851 buf.set_stringn(la_left + 5, la_top + i as u16, ":", la_width, self.style);
852 buf.set_stringn(
853 la_left + 6,
854 la_top + i as u16,
855 t,
856 la_width,
857 if i + offset == state.selected {
858 self.highlight_style
859 } else {
860 self.style
861 },
862 );
863 }
864 }
865 }
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Hash)]
871pub enum TuiLoggerLevelOutput {
872 Abbreviated,
873 Long,
874}