use std::collections::HashMap;
use std::fmt;
use std::io::Write;
use std::sync::{Arc, Mutex};
use chrono::Local;
use once_cell::sync::Lazy;
use serde::Serialize;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
}
impl Level {
pub fn as_str(&self) -> &'static str {
match self {
Level::Trace => "TRCE",
Level::Debug => "DEBG",
Level::Info => "INFO",
Level::Warn => "WARN",
Level::Error => "ERRO",
}
}
pub fn color(&self) -> Color {
match self {
Level::Trace => Color::Cyan,
Level::Debug => Color::Blue,
Level::Info => Color::Green,
Level::Warn => Color::Yellow,
Level::Error => Color::Red,
}
}
}
impl fmt::Display for Level {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct LogEntry {
pub level: String,
pub message: String,
pub fields: HashMap<String, String>,
pub timestamp: String,
}
#[derive(Debug, Clone)]
pub struct Config {
pub level: Level,
pub use_colors: bool,
pub show_timestamp: bool,
pub json_output: bool,
pub timestamp_format: String,
pub field_order: Option<Vec<String>>,
}
impl Default for Config {
fn default() -> Self {
Self {
level: Level::Info,
use_colors: atty::is(atty::Stream::Stderr),
show_timestamp: true,
json_output: false,
timestamp_format: "%Y-%m-%d %H:%M:%S%.3f".to_string(),
field_order: None,
}
}
}
#[derive(Debug, Clone)]
pub struct Logger {
config: Config,
context: HashMap<String, String>,
}
impl Logger {
pub fn new() -> Self {
Self {
config: Config::default(),
context: HashMap::new(),
}
}
pub fn with_config(config: Config) -> Self {
Self {
config,
context: HashMap::new(),
}
}
pub fn with_level(mut self, level: Level) -> Self {
self.config.level = level;
self
}
pub fn with_colors(mut self, use_colors: bool) -> Self {
self.config.use_colors = use_colors;
self
}
pub fn with_timestamp(mut self, show_timestamp: bool) -> Self {
self.config.show_timestamp = show_timestamp;
self
}
pub fn with_json_output(mut self, json_output: bool) -> Self {
self.config.json_output = json_output;
self
}
pub fn with_timestamp_format(mut self, format: &str) -> Self {
self.config.timestamp_format = format.to_string();
self
}
pub fn with_field_order(mut self, order: Vec<String>) -> Self {
self.config.field_order = Some(order);
self
}
pub fn with<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.context.insert(key.into(), value.into());
self
}
pub fn log(&self, level: Level, message: &str, fields: &[(&str, &str)]) {
if level < self.config.level {
return;
}
let mut entry_fields = self.context.clone();
for (key, value) in fields {
entry_fields.insert(key.to_string(), value.to_string());
}
let entry = LogEntry {
level: level.as_str().to_string(),
message: message.to_string(),
fields: entry_fields,
timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
};
self.write_entry(&entry);
}
pub fn trace(&self, message: &str, fields: &[(&str, &str)]) {
self.log(Level::Trace, message, fields);
}
pub fn debug(&self, message: &str, fields: &[(&str, &str)]) {
self.log(Level::Debug, message, fields);
}
pub fn info(&self, message: &str, fields: &[(&str, &str)]) {
self.log(Level::Info, message, fields);
}
pub fn warn(&self, message: &str, fields: &[(&str, &str)]) {
self.log(Level::Warn, message, fields);
}
pub fn error(&self, message: &str, fields: &[(&str, &str)]) {
self.log(Level::Error, message, fields);
}
fn write_entry(&self, entry: &LogEntry) {
let result = std::panic::catch_unwind(|| {
if self.config.json_output {
self.write_json_entry(entry);
} else {
self.write_text_entry(entry);
}
});
let _ = result;
}
fn write_json_entry(&self, entry: &LogEntry) {
let color_choice = if self.config.use_colors {
ColorChoice::Auto
} else {
ColorChoice::Never
};
let mut stderr = StandardStream::stderr(color_choice);
let mut json_map = serde_json::Map::new();
if self.config.show_timestamp {
json_map.insert(
"timestamp".to_string(),
serde_json::Value::String(entry.timestamp.clone()),
);
}
json_map.insert(
"level".to_string(),
serde_json::Value::String(entry.level.clone()),
);
json_map.insert(
"message".to_string(),
serde_json::Value::String(entry.message.clone()),
);
if let Some(ref field_order) = self.config.field_order {
for field_name in field_order {
if let Some(value) = entry.fields.get(field_name) {
json_map.insert(field_name.clone(), serde_json::Value::String(value.clone()));
}
}
for (key, value) in &entry.fields {
if !field_order.contains(key) {
json_map.insert(key.clone(), serde_json::Value::String(value.clone()));
}
}
} else {
for (key, value) in &entry.fields {
json_map.insert(key.clone(), serde_json::Value::String(value.clone()));
}
}
if let Ok(json_str) = serde_json::to_string(&json_map) {
let _ = writeln!(stderr, "{}", json_str);
}
let _ = stderr.flush();
}
fn write_text_entry(&self, entry: &LogEntry) {
let color_choice = if self.config.use_colors {
ColorChoice::Auto
} else {
ColorChoice::Never
};
let mut stderr = StandardStream::stderr(color_choice);
if self.config.show_timestamp {
let _ = stderr.set_color(ColorSpec::new().set_fg(Some(Color::Rgb(128, 128, 128))));
let timestamp_str = chrono::DateTime::parse_from_rfc3339(&entry.timestamp)
.map(|dt| dt.format(&self.config.timestamp_format).to_string())
.unwrap_or_else(|_| entry.timestamp.clone());
let _ = write!(stderr, "{} ", timestamp_str);
let _ = stderr.reset();
}
let _ = stderr.set_color(ColorSpec::new().set_bold(true));
let _ = write!(stderr, "{} ", entry.level);
let _ = stderr.reset();
let _ = write!(stderr, "{}", entry.message);
if let Some(ref field_order) = self.config.field_order {
for field_name in field_order {
if let Some(value) = entry.fields.get(field_name) {
let _ =
stderr.set_color(ColorSpec::new().set_fg(Some(Color::Rgb(128, 128, 128))));
let _ = write!(stderr, " {}=", field_name);
let _ = stderr.reset();
let _ = write!(stderr, "{}", value);
}
}
for (key, value) in &entry.fields {
if !field_order.contains(key) {
let _ =
stderr.set_color(ColorSpec::new().set_fg(Some(Color::Rgb(128, 128, 128))));
let _ = write!(stderr, " {}=", key);
let _ = stderr.reset();
let _ = write!(stderr, "{}", value);
}
}
} else {
for (key, value) in &entry.fields {
let _ = stderr.set_color(ColorSpec::new().set_fg(Some(Color::Rgb(128, 128, 128))));
let _ = write!(stderr, " {}=", key);
let _ = stderr.reset();
let _ = write!(stderr, "{}", value);
}
}
let _ = writeln!(stderr);
let _ = stderr.flush();
}
}
impl Default for Logger {
fn default() -> Self {
Self::new()
}
}
static GLOBAL_LOGGER: Lazy<Arc<Mutex<Logger>>> = Lazy::new(|| Arc::new(Mutex::new(Logger::new())));
pub fn set_global_logger(logger: Logger) {
if let Ok(mut global) = GLOBAL_LOGGER.lock() {
*global = logger;
}
}
pub fn global_logger() -> Logger {
GLOBAL_LOGGER.lock().unwrap().clone()
}
pub fn with_global_logger<F>(f: F)
where
F: FnOnce(&Logger),
{
if let Ok(logger) = GLOBAL_LOGGER.lock() {
f(&*logger);
}
}
#[macro_export]
macro_rules! trace {
($msg:expr) => {
$crate::with_global_logger(|logger| logger.trace($msg, &[]));
};
($msg:expr, $($key:expr, $value:expr),* $(,)?) => {
$crate::with_global_logger(|logger| {
let fields = &[$(($key, $value)),*];
logger.trace($msg, fields);
});
};
}
#[macro_export]
macro_rules! debug {
($msg:expr) => {
$crate::with_global_logger(|logger| logger.debug($msg, &[]));
};
($msg:expr, $($key:expr, $value:expr),* $(,)?) => {
$crate::with_global_logger(|logger| {
let fields = &[$(($key, $value)),*];
logger.debug($msg, fields);
});
};
}
#[macro_export]
macro_rules! info {
($msg:expr) => {
$crate::with_global_logger(|logger| logger.info($msg, &[]));
};
($msg:expr, $($key:expr, $value:expr),* $(,)?) => {
$crate::with_global_logger(|logger| {
let fields = &[$(($key, $value)),*];
logger.info($msg, fields);
});
};
}
#[macro_export]
macro_rules! warn {
($msg:expr) => {
$crate::with_global_logger(|logger| logger.warn($msg, &[]));
};
($msg:expr, $($key:expr, $value:expr),* $(,)?) => {
$crate::with_global_logger(|logger| {
let fields = &[$(($key, $value)),*];
logger.warn($msg, fields);
});
};
}
#[macro_export]
macro_rules! error {
($msg:expr) => {
$crate::with_global_logger(|logger| logger.error($msg, &[]));
};
($msg:expr, $($key:expr, $value:expr),* $(,)?) => {
$crate::with_global_logger(|logger| {
let fields = &[$(($key, $value)),*];
logger.error($msg, fields);
});
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_level_ordering() {
assert!(Level::Trace < Level::Debug);
assert!(Level::Debug < Level::Info);
assert!(Level::Info < Level::Warn);
assert!(Level::Warn < Level::Error);
}
#[test]
fn test_level_strings() {
assert_eq!(Level::Trace.as_str(), "TRCE");
assert_eq!(Level::Debug.as_str(), "DEBG");
assert_eq!(Level::Info.as_str(), "INFO");
assert_eq!(Level::Warn.as_str(), "WARN");
assert_eq!(Level::Error.as_str(), "ERRO");
}
#[test]
fn test_logger_creation() {
let logger = Logger::new();
assert_eq!(logger.config.level, Level::Info);
let logger = Logger::new().with_level(Level::Debug);
assert_eq!(logger.config.level, Level::Debug);
}
#[test]
fn test_logger_with_context() {
let logger = Logger::new()
.with("service", "test")
.with("version", "1.0.0");
assert_eq!(logger.context.get("service"), Some(&"test".to_string()));
assert_eq!(logger.context.get("version"), Some(&"1.0.0".to_string()));
}
#[test]
fn test_logger_configuration() {
let config = Config {
level: Level::Debug,
use_colors: false,
show_timestamp: false,
json_output: false,
timestamp_format: "%Y-%m-%d %H:%M:%S%.3f".to_string(),
field_order: None,
};
let logger = Logger::with_config(config.clone());
assert_eq!(logger.config.level, Level::Debug);
assert!(!logger.config.use_colors);
assert!(!logger.config.show_timestamp);
}
#[test]
fn test_global_logger() {
let custom_logger = Logger::new()
.with_level(Level::Trace)
.with("global", "test");
set_global_logger(custom_logger);
let retrieved = global_logger();
assert_eq!(retrieved.config.level, Level::Trace);
assert_eq!(retrieved.context.get("global"), Some(&"test".to_string()));
}
#[test]
fn test_macros_compile() {
trace!("Test trace message");
debug!("Test debug message");
info!("Test info message");
warn!("Test warn message");
error!("Test error message");
trace!("Test with fields", "key1", "value1", "key2", "value2");
info!("User login", "user_id", "12345", "ip", "192.168.1.1");
}
#[test]
fn test_log_entry_creation() {
let _logger = Logger::new();
let now = Local::now();
let entry = LogEntry {
level: Level::Info.as_str().to_string(),
message: "test message".to_string(),
fields: HashMap::new(),
timestamp: now.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
};
assert_eq!(entry.level, "INFO");
assert_eq!(entry.message, "test message");
assert!(entry.fields.is_empty());
}
#[test]
fn test_level_filtering() {
let logger = Logger::new().with_level(Level::Warn);
assert_eq!(logger.config.level, Level::Warn);
}
#[test]
fn test_colors_configuration() {
let logger_with_colors = Logger::new().with_colors(true);
let logger_without_colors = Logger::new().with_colors(false);
assert!(logger_with_colors.config.use_colors);
assert!(!logger_without_colors.config.use_colors);
}
#[test]
fn test_timestamp_configuration() {
let logger_with_timestamp = Logger::new().with_timestamp(true);
let logger_without_timestamp = Logger::new().with_timestamp(false);
assert!(logger_with_timestamp.config.show_timestamp);
assert!(!logger_without_timestamp.config.show_timestamp);
}
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.level, Level::Info);
assert_eq!(config.show_timestamp, true);
}
#[test]
fn test_method_chaining() {
let logger = Logger::new()
.with_level(Level::Trace)
.with_colors(false)
.with_timestamp(true)
.with("chain", "test")
.with("fluent", "api");
assert_eq!(logger.config.level, Level::Trace);
assert!(!logger.config.use_colors);
assert!(logger.config.show_timestamp);
assert_eq!(logger.context.len(), 2);
}
#[test]
fn test_level_colors() {
assert_eq!(Level::Trace.color(), Color::Cyan);
assert_eq!(Level::Debug.color(), Color::Blue);
assert_eq!(Level::Info.color(), Color::Green);
assert_eq!(Level::Warn.color(), Color::Yellow);
assert_eq!(Level::Error.color(), Color::Red);
}
#[test]
fn test_level_display() {
assert_eq!(format!("{}", Level::Trace), "TRCE");
assert_eq!(format!("{}", Level::Debug), "DEBG");
assert_eq!(format!("{}", Level::Info), "INFO");
assert_eq!(format!("{}", Level::Warn), "WARN");
assert_eq!(format!("{}", Level::Error), "ERRO");
}
#[test]
fn test_json_output() {
let logger = Logger::new().with_json_output(true);
assert!(logger.config.json_output);
}
#[test]
fn test_timestamp_format() {
let custom_format = "%H:%M:%S";
let logger = Logger::new().with_timestamp_format(custom_format);
assert_eq!(logger.config.timestamp_format, custom_format);
}
#[test]
fn test_field_order() {
let expected_order = vec![
"timestamp".to_string(),
"level".to_string(),
"message".to_string(),
];
let logger = Logger::new().with_field_order(expected_order.clone());
assert_eq!(logger.config.field_order, Some(expected_order));
}
#[test]
fn test_json_output_validity() {
let logger = Logger::new().with_json_output(true);
logger.info("Test message", &[("key", "value")]);
}
#[test]
fn test_json_with_field_order() {
let field_order = vec![
"level".to_string(),
"message".to_string(),
"custom_field".to_string(),
];
let logger = Logger::new()
.with_json_output(true)
.with_field_order(field_order.clone());
assert!(logger.config.json_output);
assert_eq!(logger.config.field_order, Some(field_order.clone()));
}
#[test]
fn test_custom_timestamp_format() {
let format = "%Y/%m/%d %H:%M";
let logger = Logger::new()
.with_timestamp_format(format)
.with_timestamp(true);
logger.info("Test timestamp format", &[]);
assert_eq!(logger.config.timestamp_format, format);
}
}