use std::sync::{Arc, RwLock};
use std::{fmt, iter, vec};
use color_eyre::Result;
use itertools::Itertools;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style, Stylize};
use ratatui::text::{Line, Span};
use ratatui::widgets::{
Block, Borders, List, ListItem, ListState, StatefulWidget, StatefulWidgetRef,
};
use time::macros::format_description;
use time::OffsetDateTime;
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_error::ErrorLayer;
use tracing_subscriber::layer::Context;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, Layer};
#[derive(Debug, Default)]
struct LogCollector {
log_events: LogEvents,
}
#[derive(Debug, Default, Clone)]
pub struct LogEvents {
logs: Arc<RwLock<Vec<LogEvent>>>,
}
#[derive(Debug)]
pub struct LogEvent {
pub timestamp: OffsetDateTime,
pub target: String,
pub level: Level,
pub message: String,
pub fields: Vec<(String, String)>,
}
pub fn init_logger<T: Into<Option<Level>>>(log_level: T) -> Result<LogEvents> {
let log_collector = LogCollector::default();
let logs = log_collector.log_events.clone();
let mut env_filter = EnvFilter::from_default_env();
if let Some(log_level) = log_level.into() {
env_filter = env_filter.add_directive(log_level.into());
}
tracing_subscriber::registry()
.with(log_collector)
.with(env_filter)
.with(ErrorLayer::default()) .try_init()?;
Ok(logs)
}
impl<S: Subscriber> Layer<S> for LogCollector {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let log_event = LogEvent::from(event);
self.log_events.push(log_event);
}
}
impl LogEvents {
fn push(&self, log: LogEvent) {
self.logs.write().unwrap().push(log);
}
}
impl From<&Event<'_>> for LogEvent {
fn from(event: &Event) -> Self {
let metadata = event.metadata();
let target = metadata.target();
let level = metadata.level();
let mut log_message = LogEvent {
timestamp: OffsetDateTime::now_utc(),
target: target.to_string(),
level: *level,
message: String::new(),
fields: Vec::new(),
};
event.record(&mut log_message);
log_message
}
}
impl Visit for LogEvent {
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
if field.name() == "message" {
self.message = format!("{:?}", value);
} else {
let name = field.name().to_string();
let value = format!("{:?}", value);
self.fields.push((name, value));
}
}
}
impl StatefulWidgetRef for LogEvents {
type State = ListState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let logs = self.logs.read().unwrap();
let block = Block::default().borders(Borders::ALL).title("Logs");
let list = List::new(logs.iter()).block(block);
StatefulWidget::render(list, area, buf, state);
}
}
impl<'a> From<&'a LogEvent> for ListItem<'a> {
fn from(log: &'a LogEvent) -> Self {
let message_line = Line::from(vec![
timestamp_span(log.timestamp),
log_level_span(log.level),
format!(" {}", log.target).dim(),
format!(" {}", log.message).white(),
]);
let field_lines = log.fields.iter().map(field_line);
let lines = iter::once(message_line).chain(field_lines).collect_vec();
ListItem::new(lines)
}
}
fn log_level_span(log_level: Level) -> Span<'static> {
Span::styled(
format!(" [{:5}]", log_level),
Style::default().fg(match log_level {
Level::ERROR => Color::Red,
Level::WARN => Color::Yellow,
Level::INFO => Color::Green,
Level::DEBUG => Color::Blue,
Level::TRACE => Color::Magenta,
}),
)
}
fn timestamp_span(timestamp: OffsetDateTime) -> Span<'static> {
let format = format_description!("[hour]:[minute]:[second]");
timestamp.format(&format).unwrap().dim()
}
fn field_line((name, value): &(String, String)) -> Line<'_> {
Line::styled(
format!(" {:}={}", name, value),
(Color::LightBlue, Modifier::ITALIC | Modifier::DIM),
)
}