use crate::color::Color;
use crate::console::{ConsoleOptions, RenderResult, Renderable};
use crate::segment::Segment;
use crate::style::Style;
use crate::table::{Column, Table};
#[derive(Debug, Clone)]
pub struct LogRender {
show_time: bool,
show_level: bool,
show_path: bool,
time_format: String,
level_width: usize,
omit_repeated_times: bool,
last_time: Option<String>,
}
impl LogRender {
pub fn new() -> Self {
Self {
show_time: true,
show_level: true,
show_path: true,
time_format: "[%x %X]".to_string(),
level_width: 8,
omit_repeated_times: true,
last_time: None,
}
}
pub fn show_time(mut self, value: bool) -> Self {
self.show_time = value;
self
}
pub fn show_level(mut self, value: bool) -> Self {
self.show_level = value;
self
}
pub fn show_path(mut self, value: bool) -> Self {
self.show_path = value;
self
}
pub fn time_format(mut self, format: impl Into<String>) -> Self {
self.time_format = format.into();
self
}
pub fn level_width(mut self, width: usize) -> Self {
self.level_width = width;
self
}
pub fn omit_repeated_times(mut self, value: bool) -> Self {
self.omit_repeated_times = value;
self
}
pub fn get_level_style(level: &str) -> Style {
use crate::theme::default_theme;
let theme = default_theme();
let key = format!("logging.level.{}", level.to_lowercase());
theme.get(&key).cloned().unwrap_or_else(|| match level.to_lowercase().as_str() {
"debug" => Style::new().color(
crate::color::Color::parse("bright_black").unwrap_or_else(|_| Color::default()),
),
"info" => Style::new().color(
crate::color::Color::parse("bright_cyan").unwrap_or_else(|_| Color::default()),
),
"warning" => Style::new().color(
crate::color::Color::parse("bright_yellow").unwrap_or_else(|_| Color::default()),
),
"error" => Style::new().color(
crate::color::Color::parse("bright_red").unwrap_or_else(|_| Color::default()),
).bold(true),
"critical" => Style::new().color(
crate::color::Color::parse("red").unwrap_or_else(|_| Color::default()),
).bold(true).reverse(true),
_ => Style::new(),
})
}
pub fn get_time_style() -> Style {
Style::new().color(
crate::color::Color::parse("bright_black").unwrap_or_else(|_| Color::default()),
)
}
pub fn get_message_style() -> Style {
Style::new()
}
pub fn get_path_style() -> Style {
Style::new().color(
crate::color::Color::parse("bright_black").unwrap_or_else(|_| Color::default()),
)
}
pub fn render_log(
&mut self,
time: Option<&str>,
level: &str,
message: &str,
path: Option<&str>,
line_no: Option<u32>,
) -> LogRecord {
let time_str = if self.show_time {
let ts = time.unwrap_or("");
if self.omit_repeated_times {
if let Some(ref last) = self.last_time {
if last == ts {
"".to_string()
} else {
self.last_time = Some(ts.to_string());
ts.to_string()
}
} else {
self.last_time = Some(ts.to_string());
ts.to_string()
}
} else {
ts.to_string()
}
} else {
String::new()
};
let path_str = if self.show_path {
match (path, line_no) {
(Some(p), Some(l)) => format!("{p}:{l}"),
(Some(p), None) => p.to_string(),
(None, Some(l)) => format!("<unknown>:{l}"),
(None, None) => String::new(),
}
} else {
String::new()
};
let padded_level = if self.show_level {
format!("{level:>width$}", width = self.level_width)
} else {
String::new()
};
LogRecord {
time: time_str,
level: padded_level,
message: message.to_string(),
path: path_str,
show_time: self.show_time,
show_level: self.show_level,
show_path: self.show_path,
}
}
pub fn render_batch(
&mut self,
records: &[(Option<&str>, &str, &str, Option<&str>, Option<u32>)],
) -> LogTable {
let rendered: Vec<LogRecord> = records
.iter()
.map(|(time, level, msg, path, line)| {
self.render_log(*time, level, msg, *path, *line)
})
.collect();
LogTable { records: rendered }
}
pub fn reset_time_cache(&mut self) {
self.last_time = None;
}
}
impl Default for LogRender {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct LogRecord {
time: String,
level: String,
message: String,
path: String,
show_time: bool,
show_level: bool,
show_path: bool,
}
impl Renderable for LogRecord {
fn render(&self, _options: &ConsoleOptions) -> RenderResult {
let time_style = LogRender::get_time_style();
let level_style = LogRender::get_level_style(&self.level.trim());
let msg_style = LogRender::get_message_style();
let path_style = LogRender::get_path_style();
let mut line: Vec<Segment> = Vec::new();
if self.show_time && !self.time.is_empty() {
line.push(Segment::styled(&self.time, time_style.clone()));
line.push(Segment::new(" "));
}
if self.show_level && !self.level.is_empty() {
line.push(Segment::styled(&self.level, level_style));
line.push(Segment::new(" "));
}
line.push(Segment::styled(&self.message, msg_style));
if self.show_path && !self.path.is_empty() {
line.push(Segment::new(" "));
line.push(Segment::styled(&self.path, path_style));
}
line.push(Segment::line());
RenderResult {
lines: vec![line],
items: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct LogTable {
records: Vec<LogRecord>,
}
impl Renderable for LogTable {
fn render(&self, options: &ConsoleOptions) -> RenderResult {
if self.records.is_empty() {
return RenderResult {
lines: Vec::new(),
items: Vec::new(),
};
}
let mut table = Table::new();
table.show_header = false;
table.show_edge = false;
table.show_lines = false;
let first = &self.records[0];
if first.show_time {
table.add_column(Column::new("Time"));
}
if first.show_level {
table.add_column(Column::new("Level"));
}
table.add_column(Column::new("Message"));
if first.show_path {
table.add_column(Column::new("Path"));
}
for record in &self.records {
let mut cells: Vec<crate::table::Cell> = Vec::new();
if record.show_time {
let time_str = if record.time.is_empty() {
String::new()
} else {
LogRender::get_time_style().render(&record.time)
};
cells.push(crate::table::Cell::new(time_str));
}
if record.show_level {
let level_str = LogRender::get_level_style(record.level.trim()).render(&record.level);
cells.push(crate::table::Cell::new(level_str));
}
cells.push(crate::table::Cell::new(
LogRender::get_message_style().render(&record.message),
));
if record.show_path && !record.path.is_empty() {
cells.push(crate::table::Cell::new(
LogRender::get_path_style().render(&record.path),
));
}
table.add_row(cells);
}
table.render(options)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_render_defaults() {
let lr = LogRender::new();
assert!(lr.show_time);
assert!(lr.show_level);
assert!(lr.show_path);
}
#[test]
fn test_log_render_builder() {
let lr = LogRender::new()
.show_time(false)
.show_level(false)
.show_path(false)
.level_width(10)
.omit_repeated_times(false);
assert!(!lr.show_time);
assert!(!lr.show_level);
assert!(!lr.show_path);
assert_eq!(lr.level_width, 10);
assert!(!lr.omit_repeated_times);
}
#[test]
fn test_log_render_single() {
let mut lr = LogRender::new();
let record = lr.render_log(
Some("2024-01-15 10:30:00"),
"INFO",
"Hello world",
Some("src/main.rs"),
Some(42),
);
let opts = ConsoleOptions::default();
let result = record.render(&opts);
let ansi = result.to_ansi();
assert!(ansi.contains("Hello world"));
}
#[test]
fn test_log_render_no_path() {
let mut lr = LogRender::new().show_path(false);
let record = lr.render_log(None, "DEBUG", "debug message", None, None);
let opts = ConsoleOptions::default();
let result = record.render(&opts);
let ansi = result.to_ansi();
assert!(ansi.contains("debug message"));
}
#[test]
fn test_log_render_level_styles() {
let debug_style = LogRender::get_level_style("DEBUG");
let info_style = LogRender::get_level_style("INFO");
let warn_style = LogRender::get_level_style("WARNING");
let error_style = LogRender::get_level_style("ERROR");
let critical_style = LogRender::get_level_style("CRITICAL");
assert!(!debug_style.is_null() || true);
assert!(!info_style.is_null() || true);
assert!(!warn_style.is_null() || true);
assert!(!error_style.is_null() || true);
assert!(!critical_style.is_null() || true);
}
#[test]
fn test_log_render_batch() {
let mut lr = LogRender::new().show_path(false).show_time(false);
let records = vec![
(None, "INFO", "first", None, None),
(None, "ERROR", "second", None, None),
];
let table = lr.render_batch(&records);
let opts = ConsoleOptions::default();
let result = table.render(&opts);
let ansi = result.to_ansi();
assert!(ansi.contains("first"));
assert!(ansi.contains("second"));
}
#[test]
fn test_log_render_time_dedup() {
let mut lr = LogRender::new().show_path(false).show_level(false);
let r1 = lr.render_log(Some("2024-01-01"), "INFO", "msg1", None, None);
let r2 = lr.render_log(Some("2024-01-01"), "INFO", "msg2", None, None);
assert!(!r1.time.is_empty());
assert!(r2.time.is_empty());
}
#[test]
fn test_log_render_reset_cache() {
let mut lr = LogRender::new().show_path(false).show_level(false);
lr.render_log(Some("ts"), "INFO", "msg1", None, None);
lr.reset_time_cache();
let r = lr.render_log(Some("ts"), "INFO", "msg2", None, None);
assert!(!r.time.is_empty());
}
}