prism_mcp_rs/core/
logging.rs

1// ! Structured logging for the MCP SDK
2// !
3// ! Module provides structured error logging with categorization,
4// ! context preservation, and integration with the metrics system.
5
6use serde_json::{json, Value};
7use std::collections::HashMap;
8use tracing::{error, info, span, warn, Level};
9
10use crate::core::error::McpError;
11use crate::core::metrics::global_metrics;
12
13/// Log level for error reporting
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ErrorLogLevel {
16    /// Critical errors that require immediate attention
17    Critical,
18    /// Errors that affect functionality but system can continue
19    Error,
20    /// Warnings about potential issues
21    Warning,
22    /// Informational error context
23    Info,
24}
25
26impl From<&McpError> for ErrorLogLevel {
27    fn from(error: &McpError) -> Self {
28        match error {
29            // Critical system errors
30            McpError::Internal(_) => ErrorLogLevel::Critical,
31
32            // Errors that break functionality
33            McpError::Transport(_)
34            | McpError::Protocol(_)
35            | McpError::Serialization(_)
36            | McpError::Authentication(_) => ErrorLogLevel::Error,
37
38            // Recoverable errors
39            McpError::Connection(_) | McpError::Timeout(_) | McpError::Io(_) => {
40                ErrorLogLevel::Warning
41            }
42
43            // Client errors (user/input issues)
44            McpError::Validation(_)
45            | McpError::ToolNotFound(_)
46            | McpError::ResourceNotFound(_)
47            | McpError::PromptNotFound(_)
48            | McpError::MethodNotFound(_)
49            | McpError::InvalidParams(_)
50            | McpError::InvalidUri(_)
51            | McpError::Url(_) => ErrorLogLevel::Info,
52
53            // Transport-specific errors
54            #[cfg(feature = "http")]
55            McpError::Http(_) => ErrorLogLevel::Warning,
56
57            #[cfg(feature = "websocket")]
58            McpError::WebSocket(_) => ErrorLogLevel::Warning,
59
60            McpError::SchemaValidation(_) => ErrorLogLevel::Info,
61
62            // Cancellation is informational
63            McpError::Auth(_) => ErrorLogLevel::Warning,
64            McpError::Cancelled(_) => ErrorLogLevel::Info,
65        }
66    }
67}
68
69/// Extended error context for logging
70#[derive(Debug, Clone)]
71pub struct ErrorContext {
72    /// Operation being performed when error occurred
73    pub operation: String,
74    /// Transport type (stdio, http, websocket)
75    pub transport: Option<String>,
76    /// Request method if applicable
77    pub method: Option<String>,
78    /// Client/server identifier
79    pub component: Option<String>,
80    /// Session or connection ID
81    pub session_id: Option<String>,
82    /// Additional context data
83    pub extra: HashMap<String, Value>,
84}
85
86impl Default for ErrorContext {
87    fn default() -> Self {
88        Self {
89            operation: "unknown".to_string(),
90            transport: None,
91            method: None,
92            component: None,
93            session_id: None,
94            extra: HashMap::new(),
95        }
96    }
97}
98
99impl ErrorContext {
100    /// Create a new error context
101    pub fn new(operation: impl Into<String>) -> Self {
102        Self {
103            operation: operation.into(),
104            ..Default::default()
105        }
106    }
107
108    /// Set transport type
109    pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
110        self.transport = Some(transport.into());
111        self
112    }
113
114    /// Set method name
115    pub fn with_method(mut self, method: impl Into<String>) -> Self {
116        self.method = Some(method.into());
117        self
118    }
119
120    /// Set component identifier
121    pub fn with_component(mut self, component: impl Into<String>) -> Self {
122        self.component = Some(component.into());
123        self
124    }
125
126    /// Set session ID
127    pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
128        self.session_id = Some(session_id.into());
129        self
130    }
131
132    /// Add extra context data
133    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
134        self.extra.insert(key.into(), value.into());
135        self
136    }
137}
138
139/// improved error logging with metrics integration
140pub struct ErrorLogger;
141
142impl ErrorLogger {
143    /// Log an error with full context and metrics
144    pub async fn log_error(error: &McpError, context: ErrorContext) {
145        let category = error.category();
146        let recoverable = error.is_recoverable();
147        let log_level = ErrorLogLevel::from(error);
148
149        // Record metrics
150        let metrics = global_metrics();
151        metrics.record_error(error, &context.operation).await;
152
153        // Create structured log entry
154        let log_data = json!({
155            "error_category": category,
156            "error_recoverable": recoverable,
157            "error_message": error.to_string(),
158            "operation": context.operation,
159            "transport": context.transport,
160            "method": context.method,
161            "component": context.component,
162            "session_id": context.session_id,
163            "extra_context": context.extra,
164        });
165
166        // Log at appropriate level
167        match log_level {
168            ErrorLogLevel::Critical => {
169                error!(
170                    target: "mcp_errors",
171                    error_category = category,
172                    error_recoverable = recoverable,
173                    operation = context.operation.as_str(),
174                    "CRITICAL MCP Error: {} - {}",
175                    error,
176                    serde_json::to_string(&log_data).unwrap_or_default()
177                );
178            }
179            ErrorLogLevel::Error => {
180                error!(
181                    target: "mcp_errors",
182                    error_category = category,
183                    error_recoverable = recoverable,
184                    operation = context.operation.as_str(),
185                    "MCP Error: {} - {}",
186                    error,
187                    serde_json::to_string(&log_data).unwrap_or_default()
188                );
189            }
190            ErrorLogLevel::Warning => {
191                warn!(
192                    target: "mcp_errors",
193                    error_category = category,
194                    error_recoverable = recoverable,
195                    operation = context.operation.as_str(),
196                    "MCP Warning: {} - {}",
197                    error,
198                    serde_json::to_string(&log_data).unwrap_or_default()
199                );
200            }
201            ErrorLogLevel::Info => {
202                info!(
203                    target: "mcp_errors",
204                    error_category = category,
205                    error_recoverable = recoverable,
206                    operation = context.operation.as_str(),
207                    "MCP Info: {} - {}",
208                    error,
209                    serde_json::to_string(&log_data).unwrap_or_default()
210                );
211            }
212        }
213    }
214
215    /// Log a retry attempt with context
216    pub async fn log_retry_attempt(
217        error: &McpError,
218        attempt: u32,
219        max_attempts: u32,
220        will_retry: bool,
221        context: ErrorContext,
222    ) {
223        let category = error.category();
224        let recoverable = error.is_recoverable();
225
226        // Record retry metrics
227        let metrics = global_metrics();
228        metrics
229            .record_retry_attempt(&context.operation, attempt, category, will_retry)
230            .await;
231
232        let log_data = json!({
233            "error_category": category,
234            "error_recoverable": recoverable,
235            "error_message": error.to_string(),
236            "retry_attempt": attempt,
237            "max_attempts": max_attempts,
238            "will_retry_again": will_retry,
239            "operation": context.operation,
240            "transport": context.transport,
241            "method": context.method,
242            "component": context.component,
243            "session_id": context.session_id,
244            "extra_context": context.extra,
245        });
246
247        if will_retry {
248            warn!(
249                target: "mcp_retries",
250                error_category = category,
251                retry_attempt = attempt,
252                max_attempts = max_attempts,
253                operation = context.operation.as_str(),
254                "MCP Retry Attempt {}/{}: {} - {}",
255                attempt,
256                max_attempts,
257                error,
258                serde_json::to_string(&log_data).unwrap_or_default()
259            );
260        } else {
261            error!(
262                target: "mcp_retries",
263                error_category = category,
264                retry_attempt = attempt,
265                max_attempts = max_attempts,
266                operation = context.operation.as_str(),
267                "MCP Retry Failed (Final): {} - {}",
268                error,
269                serde_json::to_string(&log_data).unwrap_or_default()
270            );
271        }
272    }
273
274    /// Log successful recovery after retries
275    pub async fn log_retry_success(operation: &str, total_attempts: u32, context: ErrorContext) {
276        let metrics = global_metrics();
277        metrics
278            .record_retry_attempt(operation, total_attempts, "success", false)
279            .await;
280
281        let log_data = json!({
282            "operation": operation,
283            "total_attempts": total_attempts,
284            "transport": context.transport,
285            "method": context.method,
286            "component": context.component,
287            "session_id": context.session_id,
288            "extra_context": context.extra,
289        });
290
291        info!(
292            target: "mcp_retries",
293            operation = operation,
294            total_attempts = total_attempts,
295            "MCP Retry Success: Operation '{}' succeeded after {} attempts - {}",
296            operation,
297            total_attempts,
298            serde_json::to_string(&log_data).unwrap_or_default()
299        );
300    }
301
302    /// Create a logging span for an operation
303    pub fn create_operation_span(operation: &str, context: &ErrorContext) -> tracing::Span {
304        span!(
305            Level::INFO,
306            "mcp_operation",
307            operation = operation,
308            transport = context.transport.as_deref(),
309            method = context.method.as_deref(),
310            component = context.component.as_deref(),
311            session_id = context.session_id.as_deref(),
312        )
313    }
314}
315
316impl McpError {
317    /// Log this error with structured context
318    pub async fn log_with_context(&self, context: ErrorContext) {
319        ErrorLogger::log_error(self, context).await;
320    }
321
322    /// Log this error with basic context
323    pub async fn log_error(&self, operation: &str) {
324        let context = ErrorContext::new(operation);
325        ErrorLogger::log_error(self, context).await;
326    }
327}
328
329/// Helper macro for logging errors with automatic context
330#[macro_export]
331macro_rules! log_mcp_error {
332    ($error:expr, $operation:expr) => {
333        $error.log_error($operation).await;
334    };
335    ($error:expr, $context:expr) => {
336        $error.log_with_context($context).await;
337    };
338}
339
340/// Helper macro for logging retry attempts
341#[macro_export]
342macro_rules! log_mcp_retry {
343    ($error:expr, $attempt:expr, $max:expr, $will_retry:expr, $context:expr) => {
344        $crate::core::logging::ErrorLogger::log_retry_attempt(
345            $error,
346            $attempt,
347            $max,
348            $will_retry,
349            $context,
350        )
351        .await;
352    };
353}
354
355/// Helper macro for logging successful retries
356#[macro_export]
357macro_rules! log_mcp_retry_success {
358    ($operation:expr, $attempts:expr, $context:expr) => {
359        $crate::core::logging::ErrorLogger::log_retry_success($operation, $attempts, $context)
360            .await;
361    };
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use crate::core::error::McpError;
368
369    #[test]
370    fn test_error_log_levels() {
371        assert_eq!(
372            ErrorLogLevel::from(&McpError::internal("test")),
373            ErrorLogLevel::Critical
374        );
375        assert_eq!(
376            ErrorLogLevel::from(&McpError::protocol("test")),
377            ErrorLogLevel::Error
378        );
379        assert_eq!(
380            ErrorLogLevel::from(&McpError::connection("test")),
381            ErrorLogLevel::Warning
382        );
383        assert_eq!(
384            ErrorLogLevel::from(&McpError::validation("test")),
385            ErrorLogLevel::Info
386        );
387    }
388
389    #[test]
390    fn test_error_context_builder() {
391        let context = ErrorContext::new("test_operation")
392            .with_transport("http")
393            .with_method("tools/list")
394            .with_component("client")
395            .with_session_id("sess_123")
396            .with_extra("user_id", json!("user_456"));
397
398        assert_eq!(context.operation, "test_operation");
399        assert_eq!(context.transport, Some("http".to_string()));
400        assert_eq!(context.method, Some("tools/list".to_string()));
401        assert_eq!(context.component, Some("client".to_string()));
402        assert_eq!(context.session_id, Some("sess_123".to_string()));
403        assert_eq!(context.extra.get("user_id"), Some(&json!("user_456")));
404    }
405
406    #[tokio::test]
407    async fn test_error_logging() {
408        let error = McpError::connection("Test connection error");
409        let context = ErrorContext::new("connect")
410            .with_transport("websocket")
411            .with_component("client");
412
413        // This test mainly ensures the logging doesn't panic
414        ErrorLogger::log_error(&error, context).await;
415    }
416
417    #[tokio::test]
418    async fn test_retry_logging() {
419        let error = McpError::timeout("Request timeout");
420        let context = ErrorContext::new("send_request")
421            .with_transport("http")
422            .with_method("tools/call");
423
424        // Test retry attempt logging
425        ErrorLogger::log_retry_attempt(&error, 1, 3, true, context.clone()).await;
426
427        // Test final retry failure
428        ErrorLogger::log_retry_attempt(&error, 3, 3, false, context.clone()).await;
429
430        // Test retry success
431        ErrorLogger::log_retry_success("send_request", 2, context).await;
432    }
433
434    #[tokio::test]
435    async fn test_error_extension_methods() {
436        let error = McpError::validation("Invalid input");
437
438        // Test basic error logging
439        error.log_error("validate_input").await;
440
441        // Test error logging with context
442        let context = ErrorContext::new("validate_request")
443            .with_method("tools/call")
444            .with_extra("input_size", json!(1024));
445        error.log_with_context(context).await;
446    }
447
448    #[test]
449    #[cfg(feature = "tracing-subscriber")]
450    fn test_operation_span_creation() {
451        // Initialize a tracing subscriber for testing
452        let _subscriber = tracing_subscriber::fmt()
453            .with_max_level(tracing::Level::INFO)
454            .try_init();
455
456        let context = ErrorContext::new("test_op")
457            .with_transport("stdio")
458            .with_component("server");
459
460        let span = ErrorLogger::create_operation_span("test_operation", &context);
461        // In test environments, spans may be disabled based on configuration
462        // We just verify the span was created without errors
463        let _span_guard = span.enter();
464        // If this executes without panic, the span creation is working
465    }
466}