claude_agent_sdk/observability/
logger.rs

1//! # Structured Logging for Agent SDK
2//!
3//! This module provides structured logging capabilities with level-based filtering,
4//! context support, and integration with the tracing ecosystem.
5//!
6//! ## Features
7//!
8//! - **Structured Logging**: JSON-formatted logs with consistent field names
9//! - **Context Support**: Attach context to log messages automatically
10//! - **Level Filtering**: Support for trace, debug, info, warn, error levels
11//! - **Tracing Integration**: Compatible with the `tracing` ecosystem
12//! - **Performance**: Low-overhead logging with lazy evaluation
13//!
14//! ## Example
15//!
16//! ```no_run
17//! use claude_agent_sdk::observability::Logger;
18//!
19//! let logger = Logger::new("MyAgent");
20//! logger.info("Starting agent execution", &[("task_id", "123")]);
21//! logger.error("Failed to execute", Some(&anyhow::anyhow!("Connection error")));
22//! ```
23
24use std::fmt;
25use std::time::{SystemTime, UNIX_EPOCH};
26
27/// Log level
28#[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/// Structured log entry
65#[derive(Debug, Clone)]
66pub struct LogEntry {
67    /// Timestamp (milliseconds since epoch)
68    pub timestamp: u64,
69
70    /// Log level
71    pub level: LogLevel,
72
73    /// Logger context/name
74    pub context: String,
75
76    /// Log message
77    pub message: String,
78
79    /// Additional key-value pairs
80    pub metadata: Vec<(String, String)>,
81
82    /// Optional error details
83    pub error: Option<String>,
84}
85
86impl LogEntry {
87    /// Create a new log entry
88    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    /// Add metadata field
105    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    /// Add multiple metadata fields
111    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    /// Add error information
119    pub fn with_error(mut self, error: impl fmt::Display) -> Self {
120        self.error = Some(error.to_string());
121        self
122    }
123
124    /// Convert to JSON string
125    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    /// Convert to human-readable text
146    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
165/// Escape JSON string
166fn escape_json(s: &str) -> String {
167    s.replace('\\', "\\\\")
168        .replace('"', "\\\"")
169        .replace('\n', "\\n")
170        .replace('\r', "\\r")
171        .replace('\t', "\\t")
172}
173
174/// Observer for log entries
175pub trait LogObserver: Send + Sync {
176    /// Called when a log entry is created
177    fn on_log(&self, entry: &LogEntry);
178}
179
180/// Default console observer that prints to stdout/stderr
181pub struct ConsoleLogObserver {
182    /// Output format
183    format: LogFormat,
184
185    /// Minimum level to output
186    min_level: LogLevel,
187}
188
189/// Log output format
190#[derive(Debug, Clone, Copy)]
191pub enum LogFormat {
192    /// Human-readable text format
193    Text,
194
195    /// JSON format
196    Json,
197}
198
199impl ConsoleLogObserver {
200    /// Create a new console observer
201    pub fn new(min_level: LogLevel, format: LogFormat) -> Self {
202        Self { format, min_level }
203    }
204
205    /// Create a text format observer
206    pub fn text(min_level: LogLevel) -> Self {
207        Self::new(min_level, LogFormat::Text)
208    }
209
210    /// Create a JSON format observer
211    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
235/// Structured logger
236pub struct Logger {
237    /// Logger context/name
238    context: String,
239
240    /// Minimum log level
241    min_level: LogLevel,
242
243    /// Observers for log entries
244    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    /// Create a new logger with the given context
259    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    /// Set the minimum log level
268    pub fn with_min_level(mut self, level: LogLevel) -> Self {
269        self.min_level = level;
270        self
271    }
272
273    /// Add an observer
274    pub fn with_observer(mut self, observer: std::sync::Arc<dyn LogObserver>) -> Self {
275        self.observers.push(observer);
276        self
277    }
278
279    /// Log a trace message
280    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    /// Log a debug message
285    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    /// Log an info message
290    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    /// Log a warning message
295    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    /// Log an error message
300    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    /// Internal log method
306    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        // Notify observers
328        for observer in &self.observers {
329            observer.on_log(&entry);
330        }
331
332        // Default: use tracing if available
333        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
365/// Global logger registry (for convenience)
366pub struct GlobalLogger {
367    loggers: std::sync::RwLock<std::collections::HashMap<String, Logger>>,
368}
369
370impl GlobalLogger {
371    /// Get the global logger instance
372    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    /// Get or create a logger for a context
384    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    /// Register a logger
393    pub fn register(&self, logger: Logger) {
394        let mut loggers = self.loggers.write().unwrap();
395        loggers.insert(logger.context.clone(), logger);
396    }
397
398    /// Set the default minimum log level for all loggers
399    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
407/// Get a logger for the given context
408pub 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        // These should not panic
469        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}