1use std::fmt;
25use std::time::{SystemTime, UNIX_EPOCH};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
29pub enum LogLevel {
30 Trace = 0,
31 Debug = 1,
32 Info = 2,
33 Warn = 3,
34 Error = 4,
35}
36
37impl fmt::Display for LogLevel {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 LogLevel::Trace => write!(f, "TRACE"),
41 LogLevel::Debug => write!(f, "DEBUG"),
42 LogLevel::Info => write!(f, "INFO"),
43 LogLevel::Warn => write!(f, "WARN"),
44 LogLevel::Error => write!(f, "ERROR"),
45 }
46 }
47}
48
49impl std::str::FromStr for LogLevel {
50 type Err = String;
51
52 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
53 match s.to_uppercase().as_str() {
54 "TRACE" => Ok(LogLevel::Trace),
55 "DEBUG" => Ok(LogLevel::Debug),
56 "INFO" => Ok(LogLevel::Info),
57 "WARN" => Ok(LogLevel::Warn),
58 "ERROR" => Ok(LogLevel::Error),
59 _ => Err(format!("Invalid log level: {}", s)),
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct LogEntry {
67 pub timestamp: u64,
69
70 pub level: LogLevel,
72
73 pub context: String,
75
76 pub message: String,
78
79 pub metadata: Vec<(String, String)>,
81
82 pub error: Option<String>,
84}
85
86impl LogEntry {
87 pub fn new(level: LogLevel, context: impl Into<String>, message: impl Into<String>) -> Self {
89 let timestamp = SystemTime::now()
90 .duration_since(UNIX_EPOCH)
91 .unwrap()
92 .as_millis() as u64;
93
94 Self {
95 timestamp,
96 level,
97 context: context.into(),
98 message: message.into(),
99 metadata: Vec::new(),
100 error: None,
101 }
102 }
103
104 pub fn with_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
106 self.metadata.push((key.into(), value.into()));
107 self
108 }
109
110 pub fn with_fields(mut self, fields: &[(impl AsRef<str>, impl AsRef<str>)]) -> Self {
112 for (key, value) in fields {
113 self.metadata.push((key.as_ref().to_string(), value.as_ref().to_string()));
114 }
115 self
116 }
117
118 pub fn with_error(mut self, error: impl fmt::Display) -> Self {
120 self.error = Some(error.to_string());
121 self
122 }
123
124 pub fn to_json(&self) -> String {
126 let mut s = String::from('{');
127
128 s.push_str(&format!(r#""timestamp":{}"#, self.timestamp));
129 s.push_str(&format!(r#","level":"{}""#, self.level));
130 s.push_str(&format!(r#","context":"{}""#, escape_json(&self.context)));
131 s.push_str(&format!(r#","message":"{}""#, escape_json(&self.message)));
132
133 for (key, value) in &self.metadata {
134 s.push_str(&format!(r#","{}":"{}""#, escape_json(key), escape_json(value)));
135 }
136
137 if let Some(ref error) = self.error {
138 s.push_str(&format!(r#","error":"{}""#, escape_json(error)));
139 }
140
141 s.push('}');
142 s
143 }
144
145 pub fn to_text(&self) -> String {
147 let timestamp = chrono::DateTime::<chrono::Utc>::from_timestamp_millis(self.timestamp as i64)
148 .unwrap()
149 .format("%Y-%m-%d %H:%M:%S%.3f");
150
151 let mut s = format!("[{}] {} {}: {}", timestamp, self.level, self.context, self.message);
152
153 for (key, value) in &self.metadata {
154 s.push_str(&format!(" {}={}", key, value));
155 }
156
157 if let Some(ref error) = self.error {
158 s.push_str(&format!(" error={}", error));
159 }
160
161 s
162 }
163}
164
165fn escape_json(s: &str) -> String {
167 s.replace('\\', "\\\\")
168 .replace('"', "\\\"")
169 .replace('\n', "\\n")
170 .replace('\r', "\\r")
171 .replace('\t', "\\t")
172}
173
174pub trait LogObserver: Send + Sync {
176 fn on_log(&self, entry: &LogEntry);
178}
179
180pub struct ConsoleLogObserver {
182 format: LogFormat,
184
185 min_level: LogLevel,
187}
188
189#[derive(Debug, Clone, Copy)]
191pub enum LogFormat {
192 Text,
194
195 Json,
197}
198
199impl ConsoleLogObserver {
200 pub fn new(min_level: LogLevel, format: LogFormat) -> Self {
202 Self { format, min_level }
203 }
204
205 pub fn text(min_level: LogLevel) -> Self {
207 Self::new(min_level, LogFormat::Text)
208 }
209
210 pub fn json(min_level: LogLevel) -> Self {
212 Self::new(min_level, LogFormat::Json)
213 }
214}
215
216impl LogObserver for ConsoleLogObserver {
217 fn on_log(&self, entry: &LogEntry) {
218 if entry.level < self.min_level {
219 return;
220 }
221
222 let output = match self.format {
223 LogFormat::Text => entry.to_text(),
224 LogFormat::Json => entry.to_json(),
225 };
226
227 match entry.level {
228 LogLevel::Error => eprintln!("{}", output),
229 LogLevel::Warn => eprintln!("{}", output),
230 _ => println!("{}", output),
231 };
232 }
233}
234
235pub struct Logger {
237 context: String,
239
240 min_level: LogLevel,
242
243 observers: Vec<std::sync::Arc<dyn LogObserver>>,
245}
246
247impl fmt::Debug for Logger {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 f.debug_struct("Logger")
250 .field("context", &self.context)
251 .field("min_level", &self.min_level)
252 .field("observers_count", &self.observers.len())
253 .finish()
254 }
255}
256
257impl Logger {
258 pub fn new(context: impl Into<String>) -> Self {
260 Self {
261 context: context.into(),
262 min_level: LogLevel::Info,
263 observers: Vec::new(),
264 }
265 }
266
267 pub fn with_min_level(mut self, level: LogLevel) -> Self {
269 self.min_level = level;
270 self
271 }
272
273 pub fn with_observer(mut self, observer: std::sync::Arc<dyn LogObserver>) -> Self {
275 self.observers.push(observer);
276 self
277 }
278
279 pub fn trace(&self, message: impl fmt::Display, fields: &[(impl AsRef<str>, impl AsRef<str>)]) {
281 self.log(LogLevel::Trace, message, fields, None as Option<&str>);
282 }
283
284 pub fn debug(&self, message: impl fmt::Display, fields: &[(impl AsRef<str>, impl AsRef<str>)]) {
286 self.log(LogLevel::Debug, message, fields, None as Option<&str>);
287 }
288
289 pub fn info(&self, message: impl fmt::Display, fields: &[(impl AsRef<str>, impl AsRef<str>)]) {
291 self.log(LogLevel::Info, message, fields, None as Option<&str>);
292 }
293
294 pub fn warn(&self, message: impl fmt::Display, fields: &[(impl AsRef<str>, impl AsRef<str>)]) {
296 self.log(LogLevel::Warn, message, fields, None as Option<&str>);
297 }
298
299 pub fn error(&self, message: impl fmt::Display, error: Option<impl fmt::Display>) {
301 const EMPTY: &[(&str, &str)] = &[];
302 self.log(LogLevel::Error, message, EMPTY, error);
303 }
304
305 fn log(
307 &self,
308 level: LogLevel,
309 message: impl fmt::Display,
310 fields: &[(impl AsRef<str>, impl AsRef<str>)],
311 error: Option<impl fmt::Display>,
312 ) {
313 if level < self.min_level {
314 return;
315 }
316
317 let mut entry = LogEntry::new(level, &self.context, message.to_string());
318
319 for (key, value) in fields {
320 entry = entry.with_field(key.as_ref(), value.as_ref());
321 }
322
323 if let Some(e) = error {
324 entry = entry.with_error(e);
325 }
326
327 for observer in &self.observers {
329 observer.on_log(&entry);
330 }
331
332 if self.observers.is_empty() {
334 match level {
335 LogLevel::Trace => {
336 tracing::trace!(context = %self.context, message = %entry.message, "TRACE")
337 },
338 LogLevel::Debug => {
339 tracing::debug!(context = %self.context, message = %entry.message, "DEBUG")
340 },
341 LogLevel::Info => {
342 tracing::info!(context = %self.context, message = %entry.message, "INFO")
343 },
344 LogLevel::Warn => {
345 tracing::warn!(context = %self.context, message = %entry.message, "WARN")
346 },
347 LogLevel::Error => {
348 tracing::error!(context = %self.context, message = %entry.message, error = ?entry.error, "ERROR")
349 },
350 }
351 }
352 }
353}
354
355impl Clone for Logger {
356 fn clone(&self) -> Self {
357 Self {
358 context: self.context.clone(),
359 min_level: self.min_level,
360 observers: self.observers.clone(),
361 }
362 }
363}
364
365pub struct GlobalLogger {
367 loggers: std::sync::RwLock<std::collections::HashMap<String, Logger>>,
368}
369
370impl GlobalLogger {
371 pub fn instance() -> std::sync::Arc<Self> {
373 static INSTANCE: std::sync::OnceLock<std::sync::Arc<GlobalLogger>> = std::sync::OnceLock::new();
374 INSTANCE
375 .get_or_init(|| {
376 std::sync::Arc::new(Self {
377 loggers: std::sync::RwLock::new(std::collections::HashMap::new()),
378 })
379 })
380 .clone()
381 }
382
383 pub fn get(&self, context: &str) -> Logger {
385 let loggers = self.loggers.read().unwrap();
386 loggers
387 .get(context)
388 .cloned()
389 .unwrap_or_else(|| Logger::new(context))
390 }
391
392 pub fn register(&self, logger: Logger) {
394 let mut loggers = self.loggers.write().unwrap();
395 loggers.insert(logger.context.clone(), logger);
396 }
397
398 pub fn set_min_level(&self, level: LogLevel) {
400 let mut loggers = self.loggers.write().unwrap();
401 for logger in loggers.values_mut() {
402 logger.min_level = level;
403 }
404 }
405}
406
407pub fn logger(context: &str) -> Logger {
409 GlobalLogger::instance().get(context)
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use std::str::FromStr;
416
417 #[test]
418 fn test_log_level_from_str() {
419 assert_eq!(LogLevel::from_str("INFO").unwrap(), LogLevel::Info);
420 assert_eq!(LogLevel::from_str("error").unwrap(), LogLevel::Error);
421 assert!(LogLevel::from_str("INVALID").is_err());
422 }
423
424 #[test]
425 fn test_log_entry_creation() {
426 let entry = LogEntry::new(LogLevel::Info, "TestContext", "Test message")
427 .with_field("key1", "value1")
428 .with_field("key2", "value2");
429
430 assert_eq!(entry.level, LogLevel::Info);
431 assert_eq!(entry.context, "TestContext");
432 assert_eq!(entry.message, "Test message");
433 assert_eq!(entry.metadata.len(), 2);
434 }
435
436 #[test]
437 fn test_log_entry_json() {
438 let entry = LogEntry::new(LogLevel::Error, "Test", "Error message")
439 .with_field("code", "500")
440 .with_error("Connection failed");
441
442 let json = entry.to_json();
443 assert!(json.contains(r#""level":"ERROR""#));
444 assert!(json.contains(r#""context":"Test""#));
445 assert!(json.contains(r#""message":"Error message""#));
446 assert!(json.contains(r#""code":"500""#));
447 assert!(json.contains(r#""error":"Connection failed""#));
448 }
449
450 #[test]
451 fn test_log_entry_text() {
452 let entry = LogEntry::new(LogLevel::Info, "MyAgent", "Processing complete")
453 .with_field("duration_ms", "150");
454
455 let text = entry.to_text();
456 assert!(text.contains("INFO"));
457 assert!(text.contains("MyAgent"));
458 assert!(text.contains("Processing complete"));
459 assert!(text.contains("duration_ms=150"));
460 }
461
462 #[test]
463 fn test_logger() {
464 let logger = Logger::new("TestLogger").with_min_level(LogLevel::Debug);
465
466 const EMPTY_FIELDS: &[(&str, &str)] = &[];
467
468 logger.trace("Trace msg", EMPTY_FIELDS);
470 logger.debug("Debug msg", &[("key", "value")]);
471 logger.info("Info msg", EMPTY_FIELDS);
472 logger.warn("Warn msg", EMPTY_FIELDS);
473 logger.error("Error msg", Some("error details"));
474 }
475
476 #[test]
477 fn test_logger_with_observer() {
478 struct TestObserver {
479 entries: std::sync::Arc<std::sync::Mutex<Vec<LogEntry>>>,
480 }
481
482 impl LogObserver for TestObserver {
483 fn on_log(&self, entry: &LogEntry) {
484 self.entries.lock().unwrap().push(entry.clone());
485 }
486 }
487
488 let entries = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
489 let observer = std::sync::Arc::new(TestObserver {
490 entries: entries.clone(),
491 });
492
493 let logger = Logger::new("Test")
494 .with_min_level(LogLevel::Info)
495 .with_observer(observer);
496
497 logger.info("Test message", &[("key", "value")]);
498
499 let logged = entries.lock().unwrap();
500 assert_eq!(logged.len(), 1);
501 assert_eq!(logged[0].message, "Test message");
502 assert_eq!(logged[0].context, "Test");
503 }
504}