1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ErrorLogLevel {
16 Critical,
18 Error,
20 Warning,
22 Info,
24}
25
26impl From<&McpError> for ErrorLogLevel {
27 fn from(error: &McpError) -> Self {
28 match error {
29 McpError::Internal(_) => ErrorLogLevel::Critical,
31
32 McpError::Transport(_)
34 | McpError::Protocol(_)
35 | McpError::Serialization(_)
36 | McpError::Authentication(_) => ErrorLogLevel::Error,
37
38 McpError::Connection(_) | McpError::Timeout(_) | McpError::Io(_) => {
40 ErrorLogLevel::Warning
41 }
42
43 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 #[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 McpError::Auth(_) | McpError::Forbidden(_) | McpError::RateLimited { .. } => {
67 ErrorLogLevel::Warning
68 }
69 McpError::Cancelled(_) => ErrorLogLevel::Info,
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct ErrorContext {
77 pub operation: String,
79 pub transport: Option<String>,
81 pub method: Option<String>,
83 pub component: Option<String>,
85 pub session_id: Option<String>,
87 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 pub fn new(operation: impl Into<String>) -> Self {
107 Self {
108 operation: operation.into(),
109 ..Default::default()
110 }
111 }
112
113 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
115 self.transport = Some(transport.into());
116 self
117 }
118
119 pub fn with_method(mut self, method: impl Into<String>) -> Self {
121 self.method = Some(method.into());
122 self
123 }
124
125 pub fn with_component(mut self, component: impl Into<String>) -> Self {
127 self.component = Some(component.into());
128 self
129 }
130
131 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 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
144pub struct ErrorLogger;
146
147impl ErrorLogger {
148 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 let metrics = global_metrics();
156 metrics.record_error(error, &context.operation).await;
157
158 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 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 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 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 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 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 pub async fn log_with_context(&self, context: ErrorContext) {
324 ErrorLogger::log_error(self, context).await;
325 }
326
327 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#[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#[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#[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 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 ErrorLogger::log_retry_attempt(&error, 1, 3, true, context.clone()).await;
431
432 ErrorLogger::log_retry_attempt(&error, 3, 3, false, context.clone()).await;
434
435 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 error.log_error("validate_input").await;
445
446 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 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 let _span_guard = span.enter();
469 }
471}