use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogLevel::Trace => write!(f, "TRACE"),
LogLevel::Debug => write!(f, "DEBUG"),
LogLevel::Info => write!(f, "INFO"),
LogLevel::Warn => write!(f, "WARN"),
LogLevel::Error => write!(f, "ERROR"),
}
}
}
impl std::str::FromStr for LogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"TRACE" => Ok(LogLevel::Trace),
"DEBUG" => Ok(LogLevel::Debug),
"INFO" => Ok(LogLevel::Info),
"WARN" => Ok(LogLevel::Warn),
"ERROR" => Ok(LogLevel::Error),
_ => Err(format!("Invalid log level: {s}")),
}
}
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub timestamp: DateTime<Utc>,
pub level: LogLevel,
pub logger_name: String,
pub message: String,
pub metadata: HashMap<String, Value>,
}
impl LogEntry {
pub fn new(level: LogLevel, logger_name: String, message: String) -> Self {
Self {
timestamp: Utc::now(),
level,
logger_name,
message,
metadata: HashMap::new(),
}
}
pub fn with_metadata(mut self, key: String, value: Value) -> Self {
self.metadata.insert(key, value);
self
}
pub fn with_metadata_map(mut self, metadata: HashMap<String, Value>) -> Self {
self.metadata.extend(metadata);
self
}
}
pub trait LogFormatter: Send + Sync {
fn format(&self, entry: &LogEntry) -> String;
}
#[derive(Debug, Clone)]
pub struct SimpleFormatter {
pub include_timestamp: bool,
pub include_level: bool,
pub include_logger_name: bool,
}
impl Default for SimpleFormatter {
fn default() -> Self {
Self {
include_timestamp: true,
include_level: true,
include_logger_name: true,
}
}
}
impl LogFormatter for SimpleFormatter {
fn format(&self, entry: &LogEntry) -> String {
let mut parts = Vec::new();
if self.include_timestamp {
parts.push(entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f").to_string());
}
if self.include_level {
parts.push(format!("[{}]", entry.level));
}
if self.include_logger_name {
parts.push(format!("({})", entry.logger_name));
}
parts.push(entry.message.clone());
if !entry.metadata.is_empty() {
let metadata_str = serde_json::to_string(&entry.metadata).unwrap_or_default();
parts.push(format!("metadata: {metadata_str}"));
}
parts.join(" ")
}
}
#[derive(Debug, Clone, Default)]
pub struct JsonFormatter;
impl LogFormatter for JsonFormatter {
fn format(&self, entry: &LogEntry) -> String {
let mut json_entry = json!({
"timestamp": entry.timestamp.to_rfc3339(),
"level": entry.level.to_string(),
"logger": entry.logger_name,
"message": entry.message,
});
if !entry.metadata.is_empty() {
if let Value::Object(ref mut map) = json_entry {
for (key, value) in &entry.metadata {
map.insert(key.clone(), value.clone());
}
}
}
serde_json::to_string(&json_entry).unwrap_or_default()
}
}
pub trait LogOutput: Send + Sync {
fn write(&self, formatted_message: &str);
}
#[derive(Debug, Clone, Default)]
pub struct ConsoleOutput;
impl LogOutput for ConsoleOutput {
fn write(&self, formatted_message: &str) {
println!("{formatted_message}");
}
}
#[derive(Clone)]
pub struct LoggerConfig {
pub level: LogLevel,
pub name: String,
pub formatter: Arc<dyn LogFormatter>,
pub output: Arc<dyn LogOutput>,
}
impl std::fmt::Debug for LoggerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LoggerConfig")
.field("level", &self.level)
.field("name", &self.name)
.field("formatter", &"<formatter>")
.field("output", &"<output>")
.finish()
}
}
impl LoggerConfig {
pub fn new(name: String) -> Self {
Self {
name,
level: LogLevel::Info,
formatter: Arc::new(SimpleFormatter::default()),
output: Arc::new(ConsoleOutput),
}
}
pub fn with_level(mut self, level: LogLevel) -> Self {
self.level = level;
self
}
pub fn with_formatter(mut self, formatter: Arc<dyn LogFormatter>) -> Self {
self.formatter = formatter;
self
}
pub fn with_output(mut self, output: Arc<dyn LogOutput>) -> Self {
self.output = output;
self
}
}
#[derive(Debug, Clone)]
pub struct Logger {
config: LoggerConfig,
}
impl Logger {
pub fn new(config: LoggerConfig) -> Self {
Self { config }
}
pub fn with_name(name: &str) -> Self {
Self::new(LoggerConfig::new(name.to_string()))
}
pub fn is_enabled(&self, level: LogLevel) -> bool {
level >= self.config.level
}
pub fn log(&self, level: LogLevel, message: &str) {
if self.is_enabled(level) {
let entry = LogEntry::new(level, self.config.name.clone(), message.to_string());
let formatted = self.config.formatter.format(&entry);
self.config.output.write(&formatted);
}
}
pub fn log_with_metadata(
&self,
level: LogLevel,
message: &str,
metadata: HashMap<String, Value>,
) {
if self.is_enabled(level) {
let entry = LogEntry::new(level, self.config.name.clone(), message.to_string())
.with_metadata_map(metadata);
let formatted = self.config.formatter.format(&entry);
self.config.output.write(&formatted);
}
}
pub fn trace(&self, message: &str) {
self.log(LogLevel::Trace, message);
}
pub fn debug(&self, message: &str) {
self.log(LogLevel::Debug, message);
}
pub fn info(&self, message: &str) {
self.log(LogLevel::Info, message);
}
pub fn warn(&self, message: &str) {
self.log(LogLevel::Warn, message);
}
pub fn error(&self, message: &str) {
self.log(LogLevel::Error, message);
}
}
static LOGGER_REGISTRY: Lazy<Mutex<HashMap<String, Logger>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
pub fn get_logger(name: &str) -> Logger {
let mut registry = LOGGER_REGISTRY.lock().unwrap();
if let Some(logger) = registry.get(name) {
logger.clone()
} else {
let logger = Logger::with_name(name);
registry.insert(name.to_string(), logger.clone());
logger
}
}
pub fn create_logger(config: LoggerConfig) -> Logger {
let logger = Logger::new(config.clone());
let mut registry = LOGGER_REGISTRY.lock().unwrap();
registry.insert(config.name.clone(), logger.clone());
logger
}
pub fn set_global_level(level: LogLevel) {
let mut registry = LOGGER_REGISTRY.lock().unwrap();
for logger in registry.values_mut() {
logger.config.level = level;
}
}