Skip to main content

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