turul_mcp_builders/
logging.rs

1//! Logging Builder for Runtime Logging Configuration
2//!
3//! This module provides builder patterns for creating logging notifications and requests
4//! at runtime. Supports all MCP logging levels and message formatting.
5
6use serde_json::Value;
7use std::collections::HashMap;
8
9// Import protocol types
10use turul_mcp_protocol::logging::{LoggingLevel, LoggingMessageNotification, SetLevelRequest};
11
12// Import framework traits from local crate
13use crate::traits::{HasLogFormat, HasLogLevel, HasLogTransport, HasLoggingMetadata};
14
15// Re-export the trait for convenience (defined below)
16// pub use LoggingTarget;
17
18/// Builder for creating logging messages at runtime
19pub struct LoggingBuilder {
20    level: LoggingLevel,
21    data: Value,
22    logger: Option<String>,
23    meta: Option<HashMap<String, Value>>,
24    // Filtering and transport settings
25    batch_size: Option<usize>,
26}
27
28impl LoggingBuilder {
29    /// Create a new logging builder with the given level and data
30    pub fn new(level: LoggingLevel, data: Value) -> Self {
31        Self {
32            level,
33            data,
34            logger: None,
35            meta: None,
36            batch_size: None,
37        }
38    }
39
40    /// Set the logger name/identifier
41    pub fn logger(mut self, logger: impl Into<String>) -> Self {
42        self.logger = Some(logger.into());
43        self
44    }
45
46    /// Set meta information
47    pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
48        self.meta = Some(meta);
49        self
50    }
51
52    /// Add a meta key-value pair
53    pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
54        if self.meta.is_none() {
55            self.meta = Some(HashMap::new());
56        }
57        self.meta.as_mut().unwrap().insert(key.into(), value);
58        self
59    }
60
61    /// Set batch size for log messages
62    pub fn batch_size(mut self, size: usize) -> Self {
63        self.batch_size = Some(size);
64        self
65    }
66
67    /// Build the logging message notification
68    pub fn build(self) -> LoggingMessageNotification {
69        let mut notification = LoggingMessageNotification::new(self.level, self.data);
70        if let Some(logger) = self.logger {
71            notification = notification.with_logger(logger);
72        }
73        if let Some(meta) = self.meta {
74            notification = notification.with_meta(meta);
75        }
76        notification
77    }
78
79    /// Build a dynamic logger that implements the definition traits
80    pub fn build_dynamic(self) -> DynamicLogger {
81        DynamicLogger {
82            level: self.level,
83            data: self.data,
84            logger: self.logger,
85            meta: self.meta,
86            batch_size: self.batch_size,
87        }
88    }
89
90    /// Create session-aware logger that can send messages directly to a session
91    pub fn build_session_aware(self) -> SessionAwareLogger {
92        SessionAwareLogger {
93            level: self.level,
94            data: self.data,
95            logger: self.logger,
96            meta: self.meta,
97            batch_size: self.batch_size,
98        }
99    }
100}
101
102/// Dynamic logger created by LoggingBuilder
103#[derive(Debug)]
104pub struct DynamicLogger {
105    level: LoggingLevel,
106    data: Value,
107    logger: Option<String>,
108    #[allow(dead_code)]
109    meta: Option<HashMap<String, Value>>,
110    batch_size: Option<usize>,
111}
112
113// Implement all fine-grained traits for DynamicLogger
114impl HasLoggingMetadata for DynamicLogger {
115    fn method(&self) -> &str {
116        "notifications/message"
117    }
118
119    fn logger_name(&self) -> Option<&str> {
120        self.logger.as_deref()
121    }
122}
123
124impl HasLogLevel for DynamicLogger {
125    fn level(&self) -> LoggingLevel {
126        self.level
127    }
128}
129
130impl HasLogFormat for DynamicLogger {
131    fn data(&self) -> &Value {
132        &self.data
133    }
134}
135
136impl HasLogTransport for DynamicLogger {
137    fn batch_size(&self) -> Option<usize> {
138        self.batch_size
139    }
140
141    fn should_deliver(&self, threshold_level: LoggingLevel) -> bool {
142        self.level.should_log(threshold_level)
143    }
144}
145
146// LoggerDefinition is automatically implemented via blanket impl!
147
148/// Session-aware logger that can send messages to sessions with filtering
149///
150/// This logger is designed to work with session contexts that implement
151/// logging level checking and message sending capabilities.
152#[derive(Debug)]
153pub struct SessionAwareLogger {
154    level: LoggingLevel,
155    data: Value,
156    logger: Option<String>,
157    #[allow(dead_code)]
158    meta: Option<HashMap<String, Value>>,
159    batch_size: Option<usize>,
160}
161
162/// Trait for logging targets that can check levels and send messages
163pub trait LoggingTarget {
164    /// Check if this target should receive a message at the given level
165    fn should_log(&self, level: LoggingLevel) -> bool;
166
167    /// Send a log message to this target
168    fn notify_log(
169        &self,
170        level: LoggingLevel,
171        data: serde_json::Value,
172        logger: Option<String>,
173        meta: Option<std::collections::HashMap<String, serde_json::Value>>,
174    );
175}
176
177impl SessionAwareLogger {
178    /// Send this log message to the specified target if it passes the target's logging level filter
179    pub fn send_to_target<T: LoggingTarget>(&self, target: &T) {
180        if target.should_log(self.level) {
181            let message = self.format_message();
182            target.notify_log(
183                self.level,
184                serde_json::json!(message),
185                self.logger.clone(),
186                self.meta.clone(),
187            );
188        }
189    }
190
191    /// Send this log message to multiple targets with per-target filtering
192    pub fn send_to_targets<T: LoggingTarget>(&self, targets: &[&T]) {
193        for &target in targets {
194            self.send_to_target(target);
195        }
196    }
197
198    /// Check if this message would be sent to the given target
199    pub fn would_send_to_target<T: LoggingTarget>(&self, target: &T) -> bool {
200        target.should_log(self.level)
201    }
202
203    /// Get the formatted message that would be sent
204    pub fn format_message(&self) -> String {
205        match &self.data {
206            Value::String(s) => s.clone(),
207            other => {
208                serde_json::to_string(other).unwrap_or_else(|_| "<invalid log data>".to_string())
209            }
210        }
211    }
212
213    /// Convert logging level to string representation
214    pub fn level_to_string(&self) -> &'static str {
215        match self.level {
216            LoggingLevel::Debug => "debug",
217            LoggingLevel::Info => "info",
218            LoggingLevel::Notice => "notice",
219            LoggingLevel::Warning => "warning",
220            LoggingLevel::Error => "error",
221            LoggingLevel::Critical => "critical",
222            LoggingLevel::Alert => "alert",
223            LoggingLevel::Emergency => "emergency",
224        }
225    }
226}
227
228// Implement all fine-grained traits for SessionAwareLogger (same as DynamicLogger)
229impl HasLoggingMetadata for SessionAwareLogger {
230    fn method(&self) -> &str {
231        "notifications/message"
232    }
233
234    fn logger_name(&self) -> Option<&str> {
235        self.logger.as_deref()
236    }
237}
238
239impl HasLogLevel for SessionAwareLogger {
240    fn level(&self) -> LoggingLevel {
241        self.level
242    }
243}
244
245impl HasLogFormat for SessionAwareLogger {
246    fn data(&self) -> &Value {
247        &self.data
248    }
249}
250
251impl HasLogTransport for SessionAwareLogger {
252    fn batch_size(&self) -> Option<usize> {
253        self.batch_size
254    }
255
256    fn should_deliver(&self, threshold_level: LoggingLevel) -> bool {
257        self.level.should_log(threshold_level)
258    }
259}
260
261// LoggerDefinition is automatically implemented via blanket impl for SessionAwareLogger too!
262
263/// Builder for set level requests
264pub struct SetLevelBuilder {
265    level: LoggingLevel,
266    meta: Option<HashMap<String, Value>>,
267}
268
269impl SetLevelBuilder {
270    pub fn new(level: LoggingLevel) -> Self {
271        Self { level, meta: None }
272    }
273
274    /// Set meta information
275    pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
276        self.meta = Some(meta);
277        self
278    }
279
280    /// Add a meta key-value pair
281    pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
282        if self.meta.is_none() {
283            self.meta = Some(HashMap::new());
284        }
285        self.meta.as_mut().unwrap().insert(key.into(), value);
286        self
287    }
288
289    /// Build the set level request
290    pub fn build(self) -> SetLevelRequest {
291        let mut request = SetLevelRequest::new(self.level);
292        if let Some(meta) = self.meta {
293            request = request.with_meta(meta);
294        }
295        request
296    }
297}
298
299/// Convenience methods for different log levels
300impl LoggingBuilder {
301    /// Create a debug level logging builder
302    pub fn debug(data: Value) -> Self {
303        Self::new(LoggingLevel::Debug, data)
304    }
305
306    /// Create an info level logging builder
307    pub fn info(data: Value) -> Self {
308        Self::new(LoggingLevel::Info, data)
309    }
310
311    /// Create a notice level logging builder
312    pub fn notice(data: Value) -> Self {
313        Self::new(LoggingLevel::Notice, data)
314    }
315
316    /// Create a warning level logging builder
317    pub fn warning(data: Value) -> Self {
318        Self::new(LoggingLevel::Warning, data)
319    }
320
321    /// Create an error level logging builder
322    pub fn error(data: Value) -> Self {
323        Self::new(LoggingLevel::Error, data)
324    }
325
326    /// Create a critical level logging builder
327    pub fn critical(data: Value) -> Self {
328        Self::new(LoggingLevel::Critical, data)
329    }
330
331    /// Create an alert level logging builder
332    pub fn alert(data: Value) -> Self {
333        Self::new(LoggingLevel::Alert, data)
334    }
335
336    /// Create an emergency level logging builder
337    pub fn emergency(data: Value) -> Self {
338        Self::new(LoggingLevel::Emergency, data)
339    }
340
341    /// Create a simple text log message
342    pub fn text(level: LoggingLevel, message: impl Into<String>) -> Self {
343        Self::new(level, Value::String(message.into()))
344    }
345
346    /// Create a structured log message with fields
347    pub fn structured(level: LoggingLevel, fields: HashMap<String, Value>) -> Self {
348        Self::new(
349            level,
350            serde_json::to_value(fields).unwrap_or(Value::Object(serde_json::Map::new())),
351        )
352    }
353
354    /// Create a log message with message and context
355    pub fn with_context(
356        level: LoggingLevel,
357        message: impl Into<String>,
358        context: HashMap<String, Value>,
359    ) -> Self {
360        let mut data = context;
361        data.insert("message".to_string(), Value::String(message.into()));
362        Self::structured(level, data)
363    }
364
365    /// Create a set level request builder
366    pub fn set_level(level: LoggingLevel) -> SetLevelBuilder {
367        SetLevelBuilder::new(level)
368    }
369}
370
371/// Logger level utility functions
372pub struct LogLevel;
373
374impl LogLevel {
375    /// Parse a string to LoggingLevel
376    pub fn parse(level: &str) -> Result<LoggingLevel, String> {
377        match level.to_lowercase().as_str() {
378            "debug" => Ok(LoggingLevel::Debug),
379            "info" => Ok(LoggingLevel::Info),
380            "notice" => Ok(LoggingLevel::Notice),
381            "warning" | "warn" => Ok(LoggingLevel::Warning),
382            "error" => Ok(LoggingLevel::Error),
383            "critical" => Ok(LoggingLevel::Critical),
384            "alert" => Ok(LoggingLevel::Alert),
385            "emergency" => Ok(LoggingLevel::Emergency),
386            _ => Err(format!("Invalid log level: {}", level)),
387        }
388    }
389
390    /// Convert LoggingLevel to string
391    pub fn to_string(level: LoggingLevel) -> String {
392        match level {
393            LoggingLevel::Debug => "debug",
394            LoggingLevel::Info => "info",
395            LoggingLevel::Notice => "notice",
396            LoggingLevel::Warning => "warning",
397            LoggingLevel::Error => "error",
398            LoggingLevel::Critical => "critical",
399            LoggingLevel::Alert => "alert",
400            LoggingLevel::Emergency => "emergency",
401        }
402        .to_string()
403    }
404
405    /// Get all available log levels
406    pub fn all() -> Vec<LoggingLevel> {
407        vec![
408            LoggingLevel::Debug,
409            LoggingLevel::Info,
410            LoggingLevel::Notice,
411            LoggingLevel::Warning,
412            LoggingLevel::Error,
413            LoggingLevel::Critical,
414            LoggingLevel::Alert,
415            LoggingLevel::Emergency,
416        ]
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use serde_json::json;
424    use crate::traits::LoggerDefinition;
425
426    #[test]
427    fn test_logging_builder_basic() {
428        let data = json!({"message": "Test log message"});
429        let notification = LoggingBuilder::new(LoggingLevel::Info, data.clone())
430            .logger("test-logger")
431            .meta_value("request_id", json!("req-123"))
432            .build();
433
434        assert_eq!(notification.method, "notifications/message");
435        assert_eq!(notification.params.level, LoggingLevel::Info);
436        assert_eq!(notification.params.data, data);
437        assert_eq!(notification.params.logger, Some("test-logger".to_string()));
438
439        let meta = notification.params.meta.expect("Expected meta");
440        assert_eq!(meta.get("request_id"), Some(&json!("req-123")));
441    }
442
443    #[test]
444    fn test_logging_level_convenience_methods() {
445        let debug_log = LoggingBuilder::debug(json!({"debug": "info"})).build();
446        assert_eq!(debug_log.params.level, LoggingLevel::Debug);
447
448        let info_log = LoggingBuilder::info(json!({"info": "message"})).build();
449        assert_eq!(info_log.params.level, LoggingLevel::Info);
450
451        let warning_log = LoggingBuilder::warning(json!({"warning": "alert"})).build();
452        assert_eq!(warning_log.params.level, LoggingLevel::Warning);
453
454        let error_log = LoggingBuilder::error(json!({"error": "critical"})).build();
455        assert_eq!(error_log.params.level, LoggingLevel::Error);
456    }
457
458    #[test]
459    fn test_text_logging() {
460        let notification = LoggingBuilder::text(LoggingLevel::Info, "Simple text message")
461            .logger("text-logger")
462            .build();
463
464        assert_eq!(notification.params.level, LoggingLevel::Info);
465        assert_eq!(notification.params.data, json!("Simple text message"));
466        assert_eq!(notification.params.logger, Some("text-logger".to_string()));
467    }
468
469    #[test]
470    fn test_structured_logging() {
471        let mut fields = HashMap::new();
472        fields.insert("user".to_string(), json!("alice"));
473        fields.insert("action".to_string(), json!("login"));
474        fields.insert("success".to_string(), json!(true));
475
476        let notification = LoggingBuilder::structured(LoggingLevel::Notice, fields.clone())
477            .logger("auth-logger")
478            .build();
479
480        assert_eq!(notification.params.level, LoggingLevel::Notice);
481        // The data should be the JSON representation of the fields
482        let expected_data = serde_json::to_value(fields).unwrap();
483        assert_eq!(notification.params.data, expected_data);
484    }
485
486    #[test]
487    fn test_with_context_logging() {
488        let mut context = HashMap::new();
489        context.insert("session_id".to_string(), json!("sess-123"));
490        context.insert("ip_address".to_string(), json!("192.168.1.1"));
491
492        let notification = LoggingBuilder::with_context(
493            LoggingLevel::Info,
494            "User logged in successfully",
495            context.clone(),
496        )
497        .build();
498
499        assert_eq!(notification.params.level, LoggingLevel::Info);
500
501        // Verify the data contains the message and context
502        if let Value::Object(data_obj) = &notification.params.data {
503            assert_eq!(
504                data_obj.get("message"),
505                Some(&json!("User logged in successfully"))
506            );
507            assert_eq!(data_obj.get("session_id"), Some(&json!("sess-123")));
508            assert_eq!(data_obj.get("ip_address"), Some(&json!("192.168.1.1")));
509        } else {
510            panic!("Expected object data");
511        }
512    }
513
514    #[test]
515    fn test_set_level_builder() {
516        let request = LoggingBuilder::set_level(LoggingLevel::Warning)
517            .meta_value("source", json!("admin_panel"))
518            .build();
519
520        assert_eq!(request.method, "logging/setLevel");
521        assert_eq!(request.params.level, LoggingLevel::Warning);
522
523        let meta = request.params.meta.expect("Expected meta");
524        assert_eq!(meta.get("source"), Some(&json!("admin_panel")));
525    }
526
527    #[test]
528    fn test_dynamic_logger_traits() {
529        let logger = LoggingBuilder::info(json!({"message": "Test"}))
530            .logger("test-logger")
531            .batch_size(10)
532            .build_dynamic();
533
534        // Test HasLoggingMetadata
535        assert_eq!(logger.method(), "notifications/message");
536        assert_eq!(logger.logger_name(), Some("test-logger"));
537
538        // Test HasLogLevel
539        assert_eq!(logger.level(), LoggingLevel::Info);
540        // With Info threshold (1), should log Error (4) but not Debug (0)
541        assert!(!logger.should_log(LoggingLevel::Debug)); // Debug (0) < Info (1), so shouldn't log
542        assert!(logger.should_log(LoggingLevel::Error)); // Error (4) >= Info (1), so should log
543
544        // Test HasLogFormat
545        assert_eq!(logger.data(), &json!({"message": "Test"}));
546        assert_eq!(logger.format_message(), "{\"message\":\"Test\"}");
547
548        // Test HasLogTransport
549        assert_eq!(logger.batch_size(), Some(10));
550        assert!(logger.should_deliver(LoggingLevel::Debug));
551
552        // Test LoggerDefinition (auto-implemented)
553        let message_notification = logger.to_message_notification();
554        assert_eq!(message_notification.method, "notifications/message");
555        assert_eq!(message_notification.params.level, LoggingLevel::Info);
556
557        let set_level_request = logger.to_set_level_request();
558        assert_eq!(set_level_request.method, "logging/setLevel");
559        assert_eq!(set_level_request.params.level, LoggingLevel::Info);
560    }
561
562    #[test]
563    fn test_log_level_utilities() {
564        // Test parsing
565        assert_eq!(LogLevel::parse("info").unwrap(), LoggingLevel::Info);
566        assert_eq!(LogLevel::parse("WARNING").unwrap(), LoggingLevel::Warning);
567        assert_eq!(LogLevel::parse("warn").unwrap(), LoggingLevel::Warning);
568        assert!(LogLevel::parse("invalid").is_err());
569
570        // Test to_string
571        assert_eq!(LogLevel::to_string(LoggingLevel::Debug), "debug");
572        assert_eq!(LogLevel::to_string(LoggingLevel::Emergency), "emergency");
573
574        // Test all levels
575        let all_levels = LogLevel::all();
576        assert_eq!(all_levels.len(), 8);
577        assert!(all_levels.contains(&LoggingLevel::Debug));
578        assert!(all_levels.contains(&LoggingLevel::Emergency));
579    }
580
581    #[test]
582    fn test_log_format_with_string_data() {
583        let logger =
584            LoggingBuilder::text(LoggingLevel::Info, "Simple string message").build_dynamic();
585
586        assert_eq!(logger.format_message(), "Simple string message");
587    }
588
589    #[test]
590    fn test_log_format_with_object_data() {
591        let data = json!({"key": "value", "number": 42});
592        let logger = LoggingBuilder::new(LoggingLevel::Info, data).build_dynamic();
593
594        let formatted = logger.format_message();
595        assert!(formatted.contains("key"));
596        assert!(formatted.contains("value"));
597        assert!(formatted.contains("42"));
598    }
599}