use crate::config::{Colors, LogLevel, OutputFormat};
use chrono::{DateTime, Local};
use serde_json::json;
use std::collections::HashMap;
use std::fmt::Arguments;
#[derive(Debug, Clone)]
pub struct CallerInfo {
pub file: &'static str,
pub line: u32,
pub module: Option<&'static str>,
}
#[derive(Debug, Clone)]
pub struct ThreadInfo {
pub id: String,
pub name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct LogRecord {
pub level: LogLevel,
pub message: String,
pub timestamp: DateTime<Local>,
pub module: Option<String>,
pub caller: Option<CallerInfo>,
pub thread: Option<ThreadInfo>,
pub metadata: HashMap<String, String>,
}
impl LogRecord {
pub fn new(level: LogLevel, args: Arguments) -> Self {
Self {
level,
message: format!("{args}"),
timestamp: Local::now(),
module: None,
caller: None,
thread: None,
metadata: HashMap::new(),
}
}
pub fn with_module<S: Into<String>>(mut self, module: S) -> Self {
self.module = Some(module.into());
self
}
pub fn with_caller(mut self, caller: CallerInfo) -> Self {
self.caller = Some(caller);
self
}
pub fn with_thread(mut self, thread: ThreadInfo) -> Self {
self.thread = Some(thread);
self
}
pub fn with_metadata<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata.extend(metadata);
self
}
}
fn center_text(text: &str, width: usize) -> String {
let text_len = text.len();
if text_len >= width {
return text.to_string();
}
let padding = width - text_len;
let left_padding = padding / 2;
let right_padding = padding - left_padding;
format!(
"{}{}{}",
" ".repeat(left_padding),
text,
" ".repeat(right_padding)
)
}
pub trait Formatter: Send + Sync {
fn format(&self, record: &LogRecord) -> String;
fn supports_colors(&self) -> bool {
false
}
}
#[derive(Debug, Clone)]
pub struct TextFormatter {
pub colors: bool,
pub datetime_format: String,
pub include_caller: bool,
pub include_thread: bool,
pub include_module: bool,
}
impl Default for TextFormatter {
fn default() -> Self {
Self {
colors: true,
datetime_format: "%Y-%m-%d %H:%M:%S".to_string(),
include_caller: false,
include_thread: false,
include_module: false,
}
}
}
impl TextFormatter {
pub fn new() -> Self {
Self::default()
}
pub fn with_colors(mut self, colors: bool) -> Self {
self.colors = colors;
self
}
pub fn with_datetime_format<S: Into<String>>(mut self, format: S) -> Self {
self.datetime_format = format.into();
self
}
pub fn with_caller(mut self, include: bool) -> Self {
self.include_caller = include;
self
}
pub fn with_thread(mut self, include: bool) -> Self {
self.include_thread = include;
self
}
pub fn with_module(mut self, include: bool) -> Self {
self.include_module = include;
self
}
}
impl Formatter for TextFormatter {
fn format(&self, record: &LogRecord) -> String {
let timestamp = record.timestamp.format(&self.datetime_format);
let level_str = if self.colors {
let color = Colors::for_level(record.level);
let centered = center_text(record.level.as_str(), 7);
format!("{}{}{}", color, centered, Colors::RESET)
} else {
center_text(record.level.as_str(), 7)
};
let mut parts = vec![format!("{}", timestamp), format!("[{}]:", level_str)];
if self.include_thread {
if let Some(ref thread) = record.thread {
let thread_info = if let Some(ref name) = thread.name {
format!("[{}:{}]", name, thread.id)
} else {
format!("[{}]", thread.id)
};
parts.push(thread_info);
}
}
if self.include_module {
if let Some(ref module) = record.module {
parts.push(format!("[{module}]"));
}
}
if self.include_caller {
if let Some(ref caller) = record.caller {
let caller_info = format!("{}:{}", caller.file, caller.line);
parts.push(format!("[{caller_info}]"));
}
}
parts.push(record.message.clone());
if !record.metadata.is_empty() {
let metadata_parts: Vec<String> = record
.metadata
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
parts.push(format!("[{}]", metadata_parts.join(", ")));
}
parts.join(" ")
}
fn supports_colors(&self) -> bool {
self.colors
}
}
#[derive(Debug, Clone)]
pub struct JsonFormatter {
pub pretty: bool,
pub datetime_format: String,
pub include_caller: bool,
pub include_thread: bool,
pub include_module: bool,
}
impl Default for JsonFormatter {
fn default() -> Self {
Self {
pretty: false,
datetime_format: "%Y-%m-%dT%H:%M:%S%.3fZ".to_string(),
include_caller: true,
include_thread: true,
include_module: true,
}
}
}
impl JsonFormatter {
pub fn new() -> Self {
Self::default()
}
pub fn with_pretty(mut self, pretty: bool) -> Self {
self.pretty = pretty;
self
}
pub fn with_datetime_format<S: Into<String>>(mut self, format: S) -> Self {
self.datetime_format = format.into();
self
}
pub fn with_caller(mut self, include: bool) -> Self {
self.include_caller = include;
self
}
pub fn with_thread(mut self, include: bool) -> Self {
self.include_thread = include;
self
}
pub fn with_module(mut self, include: bool) -> Self {
self.include_module = include;
self
}
}
impl Formatter for JsonFormatter {
fn format(&self, record: &LogRecord) -> String {
let mut json_obj = json!({
"timestamp": record.timestamp.format(&self.datetime_format).to_string(),
"level": record.level.as_str(),
"message": record.message,
});
if self.include_module {
if let Some(ref module) = record.module {
json_obj["module"] = json!(module);
}
}
if self.include_caller {
if let Some(ref caller) = record.caller {
json_obj["caller"] = json!({
"file": caller.file,
"line": caller.line,
"module": caller.module,
});
}
}
if self.include_thread {
if let Some(ref thread) = record.thread {
json_obj["thread"] = json!({
"id": thread.id,
"name": thread.name,
});
}
}
if !record.metadata.is_empty() {
json_obj["metadata"] = json!(record.metadata);
}
if self.pretty {
serde_json::to_string_pretty(&json_obj).unwrap_or_else(|_| "{}".to_string())
} else {
serde_json::to_string(&json_obj).unwrap_or_else(|_| "{}".to_string())
}
}
}
#[derive(Debug, Clone)]
pub struct PlainFormatter {
pub datetime_format: String,
pub include_caller: bool,
pub include_thread: bool,
pub include_module: bool,
}
impl Default for PlainFormatter {
fn default() -> Self {
Self {
datetime_format: "%Y-%m-%d %H:%M:%S".to_string(),
include_caller: false,
include_thread: false,
include_module: false,
}
}
}
impl PlainFormatter {
pub fn new() -> Self {
Self::default()
}
pub fn with_datetime_format<S: Into<String>>(mut self, format: S) -> Self {
self.datetime_format = format.into();
self
}
pub fn with_caller(mut self, include: bool) -> Self {
self.include_caller = include;
self
}
pub fn with_thread(mut self, include: bool) -> Self {
self.include_thread = include;
self
}
pub fn with_module(mut self, include: bool) -> Self {
self.include_module = include;
self
}
}
impl Formatter for PlainFormatter {
fn format(&self, record: &LogRecord) -> String {
let timestamp = record.timestamp.format(&self.datetime_format);
let mut parts = vec![
format!("{}", timestamp),
format!("[{}]:", record.level.as_str()),
];
if self.include_thread {
if let Some(ref thread) = record.thread {
let thread_info = if let Some(ref name) = thread.name {
format!("[{}:{}]", name, thread.id)
} else {
format!("[{}]", thread.id)
};
parts.push(thread_info);
}
}
if self.include_module {
if let Some(ref module) = record.module {
parts.push(format!("[{module}]"));
}
}
if self.include_caller {
if let Some(ref caller) = record.caller {
let caller_info = format!("{}:{}", caller.file, caller.line);
parts.push(format!("[{caller_info}]"));
}
}
parts.push(record.message.clone());
if !record.metadata.is_empty() {
let metadata_parts: Vec<String> = record
.metadata
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
parts.push(format!("[{}]", metadata_parts.join(", ")));
}
parts.join(" ")
}
}
pub fn create_formatter(
format: OutputFormat,
colors: bool,
datetime_format: &str,
include_caller: bool,
include_thread: bool,
include_module: bool,
) -> Box<dyn Formatter> {
match format {
OutputFormat::Text => Box::new(
TextFormatter::new()
.with_colors(colors)
.with_datetime_format(datetime_format)
.with_caller(include_caller)
.with_thread(include_thread)
.with_module(include_module),
),
OutputFormat::Json => Box::new(
JsonFormatter::new()
.with_datetime_format(datetime_format)
.with_caller(include_caller)
.with_thread(include_thread)
.with_module(include_module),
),
OutputFormat::Plain => Box::new(
PlainFormatter::new()
.with_datetime_format(datetime_format)
.with_caller(include_caller)
.with_thread(include_thread)
.with_module(include_module),
),
}
}
pub fn get_thread_info() -> ThreadInfo {
let current_thread = std::thread::current();
ThreadInfo {
id: format!("{:?}", current_thread.id()),
name: current_thread.name().map(|s| s.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_formatter() {
let formatter = TextFormatter::new().with_colors(false);
let record = LogRecord::new(LogLevel::Info, format_args!("Test message"));
let output = formatter.format(&record);
assert!(output.contains("[ INFO ]:"));
assert!(output.contains("Test message"));
}
#[test]
fn test_json_formatter() {
let formatter = JsonFormatter::new();
let record = LogRecord::new(LogLevel::Error, format_args!("Error message"));
let output = formatter.format(&record);
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["level"], "ERROR");
assert_eq!(parsed["message"], "Error message");
assert!(parsed["timestamp"].is_string());
}
#[test]
fn test_plain_formatter() {
let formatter = PlainFormatter::new();
let record = LogRecord::new(LogLevel::Warning, format_args!("Warning message"));
let output = formatter.format(&record);
assert!(output.contains("[WARNING]:"));
assert!(output.contains("Warning message"));
assert!(!output.contains("\x1b")); }
#[test]
fn test_formatter_with_metadata() {
let formatter = TextFormatter::new().with_colors(false);
let record = LogRecord::new(LogLevel::Debug, format_args!("Debug message"))
.with_metadata("user_id", "123")
.with_metadata("request_id", "abc-def");
let output = formatter.format(&record);
assert!(output.contains("Debug message"));
assert!(output.contains("user_id=123"));
assert!(output.contains("request_id=abc-def"));
}
#[test]
fn test_formatter_with_caller() {
let formatter = TextFormatter::new().with_colors(false).with_caller(true);
let caller = CallerInfo {
file: "test.rs",
line: 42,
module: Some("test_module"),
};
let record =
LogRecord::new(LogLevel::Info, format_args!("Test message")).with_caller(caller);
let output = formatter.format(&record);
assert!(output.contains("test.rs:42"));
}
}