use log::{Level, LevelFilter, Log, Metadata, Record};
use time::{OffsetDateTime, format_description};
use super::{LogEvent, LogLevel, RichLogFormatter};
use crate::console::FastMcpConsole;
use crate::detection::DisplayContext;
pub struct RichLogger {
console: &'static FastMcpConsole,
formatter: RichLogFormatter,
min_level: Level,
show_timestamps: bool,
}
impl RichLogger {
#[must_use]
pub fn new(min_level: Level) -> Self {
Self {
console: crate::console::console(),
formatter: RichLogFormatter::detect(),
min_level,
show_timestamps: true,
}
}
#[must_use]
pub fn builder() -> RichLoggerBuilder {
RichLoggerBuilder::new()
}
pub fn init(min_level: Level) -> Result<(), log::SetLoggerError> {
let logger = Box::new(Self::new(min_level));
log::set_boxed_logger(logger)?;
log::set_max_level(min_level.to_level_filter());
Ok(())
}
pub fn try_init(min_level: Level) {
let _ = Self::init(min_level);
}
fn record_to_event(&self, record: &Record) -> LogEvent {
let level = LogLevel::from(record.level());
let message = format!("{}", record.args());
let mut event = LogEvent::new(level, message).with_target(record.target());
if self.show_timestamps {
let now = OffsetDateTime::now_utc();
if let Ok(fmt) = format_description::parse("[hour]:[minute]:[second]") {
if let Ok(ts) = now.format(&fmt) {
event = event.with_timestamp(ts);
}
}
}
if let Some(file) = record.file() {
event = event.with_file(file);
}
if let Some(line) = record.line() {
event = event.with_line(line);
}
event
}
}
#[derive(Debug)]
pub struct RichLoggerBuilder {
min_level: Level,
show_timestamps: bool,
show_targets: bool,
show_file_line: bool,
max_width: Option<usize>,
}
impl Default for RichLoggerBuilder {
fn default() -> Self {
Self::new()
}
}
impl RichLoggerBuilder {
#[must_use]
pub fn new() -> Self {
Self {
min_level: Level::Info,
show_timestamps: true,
show_targets: true,
show_file_line: false,
max_width: None,
}
}
#[must_use]
pub fn level(mut self, level: Level) -> Self {
self.min_level = level;
self
}
#[must_use]
pub fn level_filter(mut self, filter: LevelFilter) -> Self {
self.min_level = filter.to_level().unwrap_or(Level::Trace);
self
}
#[must_use]
pub fn with_timestamps(mut self, show: bool) -> Self {
self.show_timestamps = show;
self
}
#[must_use]
pub fn with_targets(mut self, show: bool) -> Self {
self.show_targets = show;
self
}
#[must_use]
pub fn with_file_line(mut self, show: bool) -> Self {
self.show_file_line = show;
self
}
#[must_use]
pub fn with_max_width(mut self, width: Option<usize>) -> Self {
self.max_width = width;
self
}
#[must_use]
pub fn build(self) -> RichLogger {
let context = DisplayContext::detect();
let theme = crate::theme::theme();
let formatter = RichLogFormatter::new(theme, context)
.with_timestamp(self.show_timestamps)
.with_target(self.show_targets)
.with_file_line(self.show_file_line)
.with_max_width(self.max_width);
RichLogger {
console: crate::console::console(),
formatter,
min_level: self.min_level,
show_timestamps: self.show_timestamps,
}
}
pub fn init(self) -> Result<(), log::SetLoggerError> {
let level = self.min_level;
let logger = Box::new(self.build());
log::set_boxed_logger(logger)?;
log::set_max_level(level.to_level_filter());
Ok(())
}
pub fn try_init(self) {
let _ = self.init();
}
}
impl Log for RichLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= self.min_level
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let event = self.record_to_event(record);
let line = self.formatter.format_line(&event);
if self.console.is_rich() {
self.console.print(&line);
} else {
eprintln!("{}", crate::console::strip_markup(&line));
}
}
fn flush(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rich_logger_enabled() {
let logger = RichLogger::new(Level::Info);
assert!(
logger.enabled(
&log::Metadata::builder()
.level(Level::Error)
.target("test")
.build()
)
);
assert!(
logger.enabled(
&log::Metadata::builder()
.level(Level::Warn)
.target("test")
.build()
)
);
assert!(
logger.enabled(
&log::Metadata::builder()
.level(Level::Info)
.target("test")
.build()
)
);
assert!(
!logger.enabled(
&log::Metadata::builder()
.level(Level::Debug)
.target("test")
.build()
)
);
assert!(
!logger.enabled(
&log::Metadata::builder()
.level(Level::Trace)
.target("test")
.build()
)
);
}
#[test]
fn test_rich_logger_new() {
let logger = RichLogger::new(Level::Debug);
assert!(
logger.enabled(
&log::Metadata::builder()
.level(Level::Debug)
.target("test")
.build()
)
);
}
#[test]
fn test_builder_default() {
let builder = RichLoggerBuilder::default();
assert_eq!(builder.min_level, Level::Info);
assert!(builder.show_timestamps);
assert!(builder.show_targets);
assert!(!builder.show_file_line);
}
#[test]
fn test_builder_level() {
let builder = RichLoggerBuilder::new().level(Level::Debug);
assert_eq!(builder.min_level, Level::Debug);
}
#[test]
fn test_builder_level_filter() {
let builder = RichLoggerBuilder::new().level_filter(LevelFilter::Warn);
assert_eq!(builder.min_level, Level::Warn);
}
#[test]
fn test_builder_level_filter_off_falls_back_to_trace() {
let builder = RichLoggerBuilder::new().level_filter(LevelFilter::Off);
assert_eq!(builder.min_level, Level::Trace);
}
#[test]
fn test_builder_timestamps() {
let builder = RichLoggerBuilder::new().with_timestamps(false);
assert!(!builder.show_timestamps);
}
#[test]
fn test_builder_targets() {
let builder = RichLoggerBuilder::new().with_targets(false);
assert!(!builder.show_targets);
}
#[test]
fn test_builder_file_line() {
let builder = RichLoggerBuilder::new().with_file_line(true);
assert!(builder.show_file_line);
}
#[test]
fn test_builder_max_width() {
let builder = RichLoggerBuilder::new().with_max_width(Some(80));
assert_eq!(builder.max_width, Some(80));
}
#[test]
fn test_builder_build() {
let logger = RichLoggerBuilder::new()
.level(Level::Debug)
.with_timestamps(false)
.build();
assert!(
logger.enabled(
&log::Metadata::builder()
.level(Level::Debug)
.target("test")
.build()
)
);
assert!(!logger.show_timestamps);
}
#[test]
fn test_logger_builder_method() {
let builder = RichLogger::builder();
assert_eq!(builder.min_level, Level::Info);
}
#[test]
fn test_record_to_event_with_timestamp_and_location() {
let logger = RichLogger::new(Level::Trace);
let record = log::Record::builder()
.args(format_args!("hello world"))
.level(Level::Warn)
.target("fastmcp_rust::logger")
.file(Some("src/logger.rs"))
.line(Some(77))
.build();
let event = logger.record_to_event(&record);
assert_eq!(event.level, LogLevel::Warn);
assert_eq!(event.message, "hello world");
assert_eq!(event.target.as_deref(), Some("fastmcp_rust::logger"));
assert_eq!(event.file.as_deref(), Some("src/logger.rs"));
assert_eq!(event.line, Some(77));
assert!(event.timestamp.is_some());
}
#[test]
fn test_record_to_event_without_timestamp_or_location() {
let logger = RichLoggerBuilder::new().with_timestamps(false).build();
let record = log::Record::builder()
.args(format_args!("no timestamp"))
.level(Level::Info)
.target("fastmcp_rust::logger")
.build();
let event = logger.record_to_event(&record);
assert_eq!(event.level, LogLevel::Info);
assert_eq!(event.message, "no timestamp");
assert_eq!(event.timestamp, None);
assert_eq!(event.file, None);
assert_eq!(event.line, None);
}
#[test]
fn test_log_and_flush_paths() {
let logger = RichLoggerBuilder::new()
.level(Level::Info)
.with_timestamps(false)
.build();
let debug_record = log::Record::builder()
.args(format_args!("ignored debug"))
.level(Level::Debug)
.target("fastmcp_rust::logger")
.build();
logger.log(&debug_record);
let error_record = log::Record::builder()
.args(format_args!("emitted error"))
.level(Level::Error)
.target("fastmcp_rust::logger")
.build();
logger.log(&error_record);
logger.flush();
}
}