use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};
#[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) -> std::result::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: u64,
pub level: LogLevel,
pub context: String,
pub message: String,
pub metadata: Vec<(String, String)>,
pub error: Option<String>,
}
impl LogEntry {
pub fn new(level: LogLevel, context: impl Into<String>, message: impl Into<String>) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
Self {
timestamp,
level,
context: context.into(),
message: message.into(),
metadata: Vec::new(),
error: None,
}
}
pub fn with_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
pub fn with_fields(mut self, fields: &[(impl AsRef<str>, impl AsRef<str>)]) -> Self {
for (key, value) in fields {
self.metadata.push((key.as_ref().to_string(), value.as_ref().to_string()));
}
self
}
pub fn with_error(mut self, error: impl fmt::Display) -> Self {
self.error = Some(error.to_string());
self
}
pub fn to_json(&self) -> String {
let mut s = String::from('{');
s.push_str(&format!(r#""timestamp":{}"#, self.timestamp));
s.push_str(&format!(r#","level":"{}""#, self.level));
s.push_str(&format!(r#","context":"{}""#, escape_json(&self.context)));
s.push_str(&format!(r#","message":"{}""#, escape_json(&self.message)));
for (key, value) in &self.metadata {
s.push_str(&format!(r#","{}":"{}""#, escape_json(key), escape_json(value)));
}
if let Some(ref error) = self.error {
s.push_str(&format!(r#","error":"{}""#, escape_json(error)));
}
s.push('}');
s
}
pub fn to_text(&self) -> String {
let timestamp = chrono::DateTime::<chrono::Utc>::from_timestamp_millis(self.timestamp as i64)
.unwrap()
.format("%Y-%m-%d %H:%M:%S%.3f");
let mut s = format!("[{}] {} {}: {}", timestamp, self.level, self.context, self.message);
for (key, value) in &self.metadata {
s.push_str(&format!(" {}={}", key, value));
}
if let Some(ref error) = self.error {
s.push_str(&format!(" error={}", error));
}
s
}
}
fn escape_json(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub trait LogObserver: Send + Sync {
fn on_log(&self, entry: &LogEntry);
}
pub struct ConsoleLogObserver {
format: LogFormat,
min_level: LogLevel,
}
#[derive(Debug, Clone, Copy)]
pub enum LogFormat {
Text,
Json,
}
impl ConsoleLogObserver {
pub fn new(min_level: LogLevel, format: LogFormat) -> Self {
Self { format, min_level }
}
pub fn text(min_level: LogLevel) -> Self {
Self::new(min_level, LogFormat::Text)
}
pub fn json(min_level: LogLevel) -> Self {
Self::new(min_level, LogFormat::Json)
}
}
impl LogObserver for ConsoleLogObserver {
fn on_log(&self, entry: &LogEntry) {
if entry.level < self.min_level {
return;
}
let output = match self.format {
LogFormat::Text => entry.to_text(),
LogFormat::Json => entry.to_json(),
};
match entry.level {
LogLevel::Error => eprintln!("{}", output),
LogLevel::Warn => eprintln!("{}", output),
_ => println!("{}", output),
};
}
}
pub struct Logger {
context: String,
min_level: LogLevel,
observers: Vec<std::sync::Arc<dyn LogObserver>>,
}
impl fmt::Debug for Logger {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Logger")
.field("context", &self.context)
.field("min_level", &self.min_level)
.field("observers_count", &self.observers.len())
.finish()
}
}
impl Logger {
pub fn new(context: impl Into<String>) -> Self {
Self {
context: context.into(),
min_level: LogLevel::Info,
observers: Vec::new(),
}
}
pub fn with_min_level(mut self, level: LogLevel) -> Self {
self.min_level = level;
self
}
pub fn with_observer(mut self, observer: std::sync::Arc<dyn LogObserver>) -> Self {
self.observers.push(observer);
self
}
pub fn trace(&self, message: impl fmt::Display, fields: &[(impl AsRef<str>, impl AsRef<str>)]) {
self.log(LogLevel::Trace, message, fields, None as Option<&str>);
}
pub fn debug(&self, message: impl fmt::Display, fields: &[(impl AsRef<str>, impl AsRef<str>)]) {
self.log(LogLevel::Debug, message, fields, None as Option<&str>);
}
pub fn info(&self, message: impl fmt::Display, fields: &[(impl AsRef<str>, impl AsRef<str>)]) {
self.log(LogLevel::Info, message, fields, None as Option<&str>);
}
pub fn warn(&self, message: impl fmt::Display, fields: &[(impl AsRef<str>, impl AsRef<str>)]) {
self.log(LogLevel::Warn, message, fields, None as Option<&str>);
}
pub fn error(&self, message: impl fmt::Display, error: Option<impl fmt::Display>) {
const EMPTY: &[(&str, &str)] = &[];
self.log(LogLevel::Error, message, EMPTY, error);
}
fn log(
&self,
level: LogLevel,
message: impl fmt::Display,
fields: &[(impl AsRef<str>, impl AsRef<str>)],
error: Option<impl fmt::Display>,
) {
if level < self.min_level {
return;
}
let mut entry = LogEntry::new(level, &self.context, message.to_string());
for (key, value) in fields {
entry = entry.with_field(key.as_ref(), value.as_ref());
}
if let Some(e) = error {
entry = entry.with_error(e);
}
for observer in &self.observers {
observer.on_log(&entry);
}
if self.observers.is_empty() {
match level {
LogLevel::Trace => {
tracing::trace!(context = %self.context, message = %entry.message, "TRACE")
},
LogLevel::Debug => {
tracing::debug!(context = %self.context, message = %entry.message, "DEBUG")
},
LogLevel::Info => {
tracing::info!(context = %self.context, message = %entry.message, "INFO")
},
LogLevel::Warn => {
tracing::warn!(context = %self.context, message = %entry.message, "WARN")
},
LogLevel::Error => {
tracing::error!(context = %self.context, message = %entry.message, error = ?entry.error, "ERROR")
},
}
}
}
}
impl Clone for Logger {
fn clone(&self) -> Self {
Self {
context: self.context.clone(),
min_level: self.min_level,
observers: self.observers.clone(),
}
}
}
pub struct GlobalLogger {
loggers: std::sync::RwLock<std::collections::HashMap<String, Logger>>,
}
impl GlobalLogger {
pub fn instance() -> std::sync::Arc<Self> {
static INSTANCE: std::sync::OnceLock<std::sync::Arc<GlobalLogger>> = std::sync::OnceLock::new();
INSTANCE
.get_or_init(|| {
std::sync::Arc::new(Self {
loggers: std::sync::RwLock::new(std::collections::HashMap::new()),
})
})
.clone()
}
pub fn get(&self, context: &str) -> Logger {
let loggers = self.loggers.read().unwrap();
loggers
.get(context)
.cloned()
.unwrap_or_else(|| Logger::new(context))
}
pub fn register(&self, logger: Logger) {
let mut loggers = self.loggers.write().unwrap();
loggers.insert(logger.context.clone(), logger);
}
pub fn set_min_level(&self, level: LogLevel) {
let mut loggers = self.loggers.write().unwrap();
for logger in loggers.values_mut() {
logger.min_level = level;
}
}
}
pub fn logger(context: &str) -> Logger {
GlobalLogger::instance().get(context)
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_log_level_from_str() {
assert_eq!(LogLevel::from_str("INFO").unwrap(), LogLevel::Info);
assert_eq!(LogLevel::from_str("error").unwrap(), LogLevel::Error);
assert!(LogLevel::from_str("INVALID").is_err());
}
#[test]
fn test_log_entry_creation() {
let entry = LogEntry::new(LogLevel::Info, "TestContext", "Test message")
.with_field("key1", "value1")
.with_field("key2", "value2");
assert_eq!(entry.level, LogLevel::Info);
assert_eq!(entry.context, "TestContext");
assert_eq!(entry.message, "Test message");
assert_eq!(entry.metadata.len(), 2);
}
#[test]
fn test_log_entry_json() {
let entry = LogEntry::new(LogLevel::Error, "Test", "Error message")
.with_field("code", "500")
.with_error("Connection failed");
let json = entry.to_json();
assert!(json.contains(r#""level":"ERROR""#));
assert!(json.contains(r#""context":"Test""#));
assert!(json.contains(r#""message":"Error message""#));
assert!(json.contains(r#""code":"500""#));
assert!(json.contains(r#""error":"Connection failed""#));
}
#[test]
fn test_log_entry_text() {
let entry = LogEntry::new(LogLevel::Info, "MyAgent", "Processing complete")
.with_field("duration_ms", "150");
let text = entry.to_text();
assert!(text.contains("INFO"));
assert!(text.contains("MyAgent"));
assert!(text.contains("Processing complete"));
assert!(text.contains("duration_ms=150"));
}
#[test]
fn test_logger() {
let logger = Logger::new("TestLogger").with_min_level(LogLevel::Debug);
const EMPTY_FIELDS: &[(&str, &str)] = &[];
logger.trace("Trace msg", EMPTY_FIELDS);
logger.debug("Debug msg", &[("key", "value")]);
logger.info("Info msg", EMPTY_FIELDS);
logger.warn("Warn msg", EMPTY_FIELDS);
logger.error("Error msg", Some("error details"));
}
#[test]
fn test_logger_with_observer() {
struct TestObserver {
entries: std::sync::Arc<std::sync::Mutex<Vec<LogEntry>>>,
}
impl LogObserver for TestObserver {
fn on_log(&self, entry: &LogEntry) {
self.entries.lock().unwrap().push(entry.clone());
}
}
let entries = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let observer = std::sync::Arc::new(TestObserver {
entries: entries.clone(),
});
let logger = Logger::new("Test")
.with_min_level(LogLevel::Info)
.with_observer(observer);
logger.info("Test message", &[("key", "value")]);
let logged = entries.lock().unwrap();
assert_eq!(logged.len(), 1);
assert_eq!(logged[0].message, "Test message");
assert_eq!(logged[0].context, "Test");
}
}