use std::collections::HashMap;
use std::sync::Mutex;
use crate::console::Console;
use crate::markup;
use crate::style::Style;
use crate::text::Text;
const DEFAULT_KEYWORDS: &[&str] = &[
"GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS", "TRACE", "PATCH",
];
#[allow(dead_code)]
pub struct RichHandler {
console: Mutex<Console>,
show_time: bool,
show_level: bool,
show_path: bool,
markup: bool,
rich_tracebacks: bool,
keywords: Vec<String>,
level_styles: HashMap<log::Level, Style>,
}
impl RichHandler {
pub fn new() -> Self {
RichHandler {
console: Mutex::new(Console::new()),
show_time: true,
show_level: true,
show_path: true,
markup: false,
rich_tracebacks: false,
keywords: DEFAULT_KEYWORDS.iter().map(|s| s.to_string()).collect(),
level_styles: Self::default_level_styles(),
}
}
#[must_use]
pub fn with_console(mut self, console: Console) -> Self {
self.console = Mutex::new(console);
self
}
#[must_use]
pub fn with_show_time(mut self, show: bool) -> Self {
self.show_time = show;
self
}
#[must_use]
pub fn with_show_level(mut self, show: bool) -> Self {
self.show_level = show;
self
}
#[must_use]
pub fn with_show_path(mut self, show: bool) -> Self {
self.show_path = show;
self
}
#[must_use]
pub fn with_markup(mut self, markup: bool) -> Self {
self.markup = markup;
self
}
#[must_use]
pub fn with_keywords(mut self, keywords: Vec<String>) -> Self {
self.keywords = keywords;
self
}
fn default_level_styles() -> HashMap<log::Level, Style> {
let mut m = HashMap::new();
m.insert(
log::Level::Error,
Style::parse("bold red").unwrap_or_else(|_| Style::null()),
);
m.insert(
log::Level::Warn,
Style::parse("bold yellow").unwrap_or_else(|_| Style::null()),
);
m.insert(
log::Level::Info,
Style::parse("bold green").unwrap_or_else(|_| Style::null()),
);
m.insert(
log::Level::Debug,
Style::parse("bold blue").unwrap_or_else(|_| Style::null()),
);
m.insert(
log::Level::Trace,
Style::parse("dim").unwrap_or_else(|_| Style::null()),
);
m
}
fn render_time() -> Text {
let now = {
use std::time::{SystemTime, UNIX_EPOCH};
let dur = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let total_secs = dur.as_secs();
let hours = (total_secs / 3600) % 24;
let minutes = (total_secs / 60) % 60;
let seconds = total_secs % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
};
let dim_style = Style::parse("dim").unwrap_or_else(|_| Style::null());
Text::styled(&now, dim_style)
}
fn render_level(&self, level: log::Level) -> Text {
let name = match level {
log::Level::Error => "ERROR",
log::Level::Warn => "WARN",
log::Level::Info => "INFO",
log::Level::Debug => "DEBUG",
log::Level::Trace => "TRACE",
};
let padded = format!("{:<8}", name);
let style = self
.level_styles
.get(&level)
.cloned()
.unwrap_or_else(Style::null);
Text::styled(&padded, style)
}
fn render_message(&self, record: &log::Record) -> Text {
let msg = format!("{}", record.args());
let mut text = if self.markup {
let base = Style::null();
markup::render(&msg, base).unwrap_or_else(|_| Text::new(&msg, Style::null()))
} else {
Text::new(&msg, Style::null())
};
if !self.keywords.is_empty() {
let kw_style = Style::parse("bold on dark_green").unwrap_or_else(|_| Style::null());
let words: Vec<&str> = self.keywords.iter().map(|s| s.as_str()).collect();
text.highlight_words(&words, kw_style, false);
}
text
}
fn render_path(record: &log::Record) -> Text {
let dim_style = Style::parse("dim").unwrap_or_else(|_| Style::null());
let module = record.module_path().unwrap_or("");
let line = record.line().unwrap_or(0);
let path_str = if !module.is_empty() {
format!("{}:{}", module, line)
} else {
format!(":{}", line)
};
Text::styled(&path_str, dim_style)
}
fn emit(&self, record: &log::Record) {
let mut parts = Text::new("", Style::null());
if self.show_time {
let time_text = Self::render_time();
parts.append_text(&time_text);
parts.append_str(" ", None);
}
if self.show_level {
let level_text = self.render_level(record.level());
parts.append_text(&level_text);
parts.append_str(" ", None);
}
let message_text = self.render_message(record);
parts.append_text(&message_text);
if self.show_path {
let path_text = Self::render_path(record);
parts.append_str(" ", None);
parts.append_text(&path_text);
}
if let Ok(mut console) = self.console.lock() {
console.print(&parts);
}
}
}
impl Default for RichHandler {
fn default() -> Self {
Self::new()
}
}
impl log::Log for RichHandler {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
self.emit(record);
}
}
fn flush(&self) {
}
}
pub fn install() -> Result<(), log::SetLoggerError> {
let handler = RichHandler::new();
log::set_boxed_logger(Box::new(handler))?;
log::set_max_level(log::LevelFilter::Trace);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::Console;
#[test]
fn test_default_construction() {
let handler = RichHandler::new();
assert!(handler.show_time);
assert!(handler.show_level);
assert!(handler.show_path);
assert!(!handler.markup);
assert!(!handler.rich_tracebacks);
assert!(!handler.keywords.is_empty());
}
#[test]
fn test_default_trait() {
let handler = RichHandler::default();
assert!(handler.show_time);
}
#[test]
fn test_builder_show_time() {
let handler = RichHandler::new().with_show_time(false);
assert!(!handler.show_time);
}
#[test]
fn test_builder_show_level() {
let handler = RichHandler::new().with_show_level(false);
assert!(!handler.show_level);
}
#[test]
fn test_builder_show_path() {
let handler = RichHandler::new().with_show_path(false);
assert!(!handler.show_path);
}
#[test]
fn test_builder_markup() {
let handler = RichHandler::new().with_markup(true);
assert!(handler.markup);
}
#[test]
fn test_builder_keywords() {
let handler = RichHandler::new().with_keywords(vec!["FOO".to_string(), "BAR".to_string()]);
assert_eq!(handler.keywords, vec!["FOO", "BAR"]);
}
#[test]
fn test_builder_console() {
let console = Console::builder().width(120).build();
let _handler = RichHandler::new().with_console(console);
}
#[test]
fn test_level_styles_all_present() {
let styles = RichHandler::default_level_styles();
assert!(styles.contains_key(&log::Level::Error));
assert!(styles.contains_key(&log::Level::Warn));
assert!(styles.contains_key(&log::Level::Info));
assert!(styles.contains_key(&log::Level::Debug));
assert!(styles.contains_key(&log::Level::Trace));
}
#[test]
fn test_error_style_is_bold_red() {
let styles = RichHandler::default_level_styles();
let error_style = styles.get(&log::Level::Error).unwrap();
assert_eq!(error_style.bold(), Some(true));
assert!(error_style.color().is_some());
assert_eq!(error_style.color().unwrap().name, "red");
}
#[test]
fn test_warn_style_is_bold_yellow() {
let styles = RichHandler::default_level_styles();
let warn_style = styles.get(&log::Level::Warn).unwrap();
assert_eq!(warn_style.bold(), Some(true));
assert_eq!(warn_style.color().unwrap().name, "yellow");
}
#[test]
fn test_info_style_is_bold_green() {
let styles = RichHandler::default_level_styles();
let info_style = styles.get(&log::Level::Info).unwrap();
assert_eq!(info_style.bold(), Some(true));
assert_eq!(info_style.color().unwrap().name, "green");
}
#[test]
fn test_debug_style_is_bold_blue() {
let styles = RichHandler::default_level_styles();
let debug_style = styles.get(&log::Level::Debug).unwrap();
assert_eq!(debug_style.bold(), Some(true));
assert_eq!(debug_style.color().unwrap().name, "blue");
}
#[test]
fn test_trace_style_is_dim() {
let styles = RichHandler::default_level_styles();
let trace_style = styles.get(&log::Level::Trace).unwrap();
assert_eq!(trace_style.dim(), Some(true));
}
#[test]
fn test_render_time_format() {
let time_text = RichHandler::render_time();
let plain = time_text.plain().to_string();
assert_eq!(plain.len(), 8);
assert_eq!(plain.as_bytes()[2], b':');
assert_eq!(plain.as_bytes()[5], b':');
}
#[test]
fn test_render_time_has_dim_style() {
let time_text = RichHandler::render_time();
assert!(!time_text.spans().is_empty());
}
#[test]
fn test_render_level_error() {
let handler = RichHandler::new();
let text = handler.render_level(log::Level::Error);
assert_eq!(text.plain(), "ERROR ");
}
#[test]
fn test_render_level_warn() {
let handler = RichHandler::new();
let text = handler.render_level(log::Level::Warn);
assert_eq!(text.plain(), "WARN ");
}
#[test]
fn test_render_level_info() {
let handler = RichHandler::new();
let text = handler.render_level(log::Level::Info);
assert_eq!(text.plain(), "INFO ");
}
#[test]
fn test_render_level_debug() {
let handler = RichHandler::new();
let text = handler.render_level(log::Level::Debug);
assert_eq!(text.plain(), "DEBUG ");
}
#[test]
fn test_render_level_trace() {
let handler = RichHandler::new();
let text = handler.render_level(log::Level::Trace);
assert_eq!(text.plain(), "TRACE ");
}
#[test]
fn test_render_level_has_style() {
let handler = RichHandler::new();
for level in &[
log::Level::Error,
log::Level::Warn,
log::Level::Info,
log::Level::Debug,
log::Level::Trace,
] {
let text = handler.render_level(*level);
assert!(
!text.spans().is_empty(),
"level {:?} should have a styled span",
level
);
}
}
#[test]
fn test_render_path_with_module() {
let record = log::Record::builder()
.args(format_args!("test"))
.level(log::Level::Info)
.module_path(Some("my_crate::module"))
.line(Some(42))
.build();
let text = RichHandler::render_path(&record);
assert_eq!(text.plain(), "my_crate::module:42");
}
#[test]
fn test_render_path_without_module() {
let record = log::Record::builder()
.args(format_args!("test"))
.level(log::Level::Info)
.line(Some(10))
.build();
let text = RichHandler::render_path(&record);
assert_eq!(text.plain(), ":10");
}
#[test]
fn test_render_path_has_dim_style() {
let record = log::Record::builder()
.args(format_args!("test"))
.level(log::Level::Info)
.module_path(Some("foo"))
.line(Some(1))
.build();
let text = RichHandler::render_path(&record);
assert!(!text.spans().is_empty());
}
#[test]
fn test_emit_no_time() {
let console = Console::builder()
.width(80)
.no_color(true)
.record(true)
.markup(false)
.build();
let handler = RichHandler::new()
.with_console(console)
.with_show_time(false)
.with_show_level(true)
.with_show_path(false);
let record = log::Record::builder()
.args(format_args!("hello world"))
.level(log::Level::Info)
.build();
handler.emit(&record);
let mut console = handler.console.lock().unwrap();
let output = console.export_text(true, false);
assert!(
!output.contains(':'),
"output should not have time, got: {}",
output
);
assert!(output.contains("INFO"));
assert!(output.contains("hello world"));
}
#[test]
fn test_emit_no_level() {
let console = Console::builder()
.width(80)
.no_color(true)
.record(true)
.markup(false)
.build();
let handler = RichHandler::new()
.with_console(console)
.with_show_time(false)
.with_show_level(false)
.with_show_path(false);
let record = log::Record::builder()
.args(format_args!("hello world"))
.level(log::Level::Warn)
.build();
handler.emit(&record);
let mut console = handler.console.lock().unwrap();
let output = console.export_text(true, false);
assert!(!output.contains("WARN"));
assert!(output.contains("hello world"));
}
#[test]
fn test_emit_no_path() {
let console = Console::builder()
.width(80)
.no_color(true)
.record(true)
.markup(false)
.build();
let handler = RichHandler::new()
.with_console(console)
.with_show_time(false)
.with_show_level(false)
.with_show_path(false);
let record = log::Record::builder()
.args(format_args!("hello world"))
.level(log::Level::Info)
.module_path(Some("test_mod"))
.line(Some(99))
.build();
handler.emit(&record);
let mut console = handler.console.lock().unwrap();
let output = console.export_text(true, false);
assert!(!output.contains("test_mod"));
assert!(output.contains("hello world"));
}
#[test]
fn test_emit_with_path() {
let console = Console::builder()
.width(120)
.no_color(true)
.record(true)
.markup(false)
.build();
let handler = RichHandler::new()
.with_console(console)
.with_show_time(false)
.with_show_level(false)
.with_show_path(true);
let record = log::Record::builder()
.args(format_args!("hello"))
.level(log::Level::Debug)
.module_path(Some("mymod"))
.line(Some(7))
.build();
handler.emit(&record);
let mut console = handler.console.lock().unwrap();
let output = console.export_text(true, false);
assert!(output.contains("mymod:7"));
}
#[test]
fn test_render_message_plain() {
let handler = RichHandler::new().with_markup(false).with_keywords(vec![]);
let record = log::Record::builder()
.args(format_args!("simple message"))
.level(log::Level::Info)
.build();
let text = handler.render_message(&record);
assert_eq!(text.plain(), "simple message");
}
#[test]
fn test_render_message_with_markup() {
let handler = RichHandler::new().with_markup(true).with_keywords(vec![]);
let record = log::Record::builder()
.args(format_args!("[bold]hello[/bold] world"))
.level(log::Level::Info)
.build();
let text = handler.render_message(&record);
assert_eq!(text.plain(), "hello world");
assert!(!text.spans().is_empty());
}
#[test]
fn test_emit_all_levels() {
let levels = [
log::Level::Error,
log::Level::Warn,
log::Level::Info,
log::Level::Debug,
log::Level::Trace,
];
let names = ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
for (level, name) in levels.iter().zip(names.iter()) {
let console = Console::builder()
.width(80)
.no_color(true)
.record(true)
.markup(false)
.build();
let handler = RichHandler::new()
.with_console(console)
.with_show_time(false)
.with_show_level(true)
.with_show_path(false);
let record = log::Record::builder()
.args(format_args!("msg"))
.level(*level)
.build();
handler.emit(&record);
let mut console = handler.console.lock().unwrap();
let output = console.export_text(true, false);
assert!(
output.contains(name),
"expected '{}' in output for {:?}, got: {}",
name,
level,
output
);
}
}
#[test]
fn test_keyword_highlighting() {
let handler = RichHandler::new()
.with_markup(false)
.with_keywords(vec!["GET".to_string(), "POST".to_string()]);
let record = log::Record::builder()
.args(format_args!("GET /index.html 200"))
.level(log::Level::Info)
.build();
let text = handler.render_message(&record);
assert_eq!(text.plain(), "GET /index.html 200");
assert!(!text.spans().is_empty(), "expected keyword span for GET");
}
#[test]
fn test_no_keyword_highlighting_when_empty() {
let handler = RichHandler::new().with_markup(false).with_keywords(vec![]);
let record = log::Record::builder()
.args(format_args!("GET /index.html 200"))
.level(log::Level::Info)
.build();
let text = handler.render_message(&record);
assert!(text.spans().is_empty());
}
#[test]
fn test_log_trait_enabled_always_true() {
let handler = RichHandler::new();
let metadata = log::MetadataBuilder::new()
.level(log::Level::Trace)
.target("test")
.build();
assert!(log::Log::enabled(&handler, &metadata));
}
#[test]
fn test_log_trait_log_produces_output() {
let console = Console::builder()
.width(80)
.no_color(true)
.record(true)
.markup(false)
.build();
let handler = RichHandler::new()
.with_console(console)
.with_show_time(false)
.with_show_level(true)
.with_show_path(false);
let record = log::Record::builder()
.args(format_args!("log trait test"))
.level(log::Level::Info)
.build();
log::Log::log(&handler, &record);
let mut console = handler.console.lock().unwrap();
let output = console.export_text(true, false);
assert!(output.contains("log trait test"));
}
#[test]
fn test_log_trait_flush_does_not_panic() {
let handler = RichHandler::new();
log::Log::flush(&handler);
}
#[test]
fn test_install_returns_result() {
let _: fn() -> Result<(), log::SetLoggerError> = install;
}
#[test]
fn test_full_line_with_all_columns() {
let console = Console::builder()
.width(120)
.no_color(true)
.record(true)
.markup(false)
.build();
let handler = RichHandler::new()
.with_console(console)
.with_show_time(true)
.with_show_level(true)
.with_show_path(true);
let record = log::Record::builder()
.args(format_args!("Server starting"))
.level(log::Level::Info)
.module_path(Some("my_app::server"))
.line(Some(42))
.build();
handler.emit(&record);
let mut console = handler.console.lock().unwrap();
let output = console.export_text(true, false);
assert!(output.contains(':'), "expected time in output");
assert!(output.contains("INFO"));
assert!(output.contains("Server starting"));
assert!(output.contains("my_app::server:42"));
}
#[test]
fn test_full_line_minimal_columns() {
let console = Console::builder()
.width(80)
.no_color(true)
.record(true)
.markup(false)
.build();
let handler = RichHandler::new()
.with_console(console)
.with_show_time(false)
.with_show_level(false)
.with_show_path(false);
let record = log::Record::builder()
.args(format_args!("bare message"))
.level(log::Level::Error)
.build();
handler.emit(&record);
let mut console = handler.console.lock().unwrap();
let output = console.export_text(true, false);
assert!(output.contains("bare message"));
assert!(!output.contains("ERROR"));
}
#[test]
fn test_default_keywords_present() {
let handler = RichHandler::new();
assert!(handler.keywords.contains(&"GET".to_string()));
assert!(handler.keywords.contains(&"POST".to_string()));
assert!(handler.keywords.contains(&"PUT".to_string()));
assert!(handler.keywords.contains(&"DELETE".to_string()));
}
}