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::InvalidUri(_)
51 | McpError::Url(_) => ErrorLogLevel::Info,
52
53 #[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 McpError::Auth(_) => ErrorLogLevel::Warning,
64 McpError::Cancelled(_) => ErrorLogLevel::Info,
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct ErrorContext {
72 pub operation: String,
74 pub transport: Option<String>,
76 pub method: Option<String>,
78 pub component: Option<String>,
80 pub session_id: Option<String>,
82 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 pub fn new(operation: impl Into<String>) -> Self {
102 Self {
103 operation: operation.into(),
104 ..Default::default()
105 }
106 }
107
108 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
110 self.transport = Some(transport.into());
111 self
112 }
113
114 pub fn with_method(mut self, method: impl Into<String>) -> Self {
116 self.method = Some(method.into());
117 self
118 }
119
120 pub fn with_component(mut self, component: impl Into<String>) -> Self {
122 self.component = Some(component.into());
123 self
124 }
125
126 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 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
139pub struct ErrorLogger;
141
142impl ErrorLogger {
143 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 let metrics = global_metrics();
151 metrics.record_error(error, &context.operation).await;
152
153 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 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 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 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 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 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 pub async fn log_with_context(&self, context: ErrorContext) {
319 ErrorLogger::log_error(self, context).await;
320 }
321
322 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#[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#[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#[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 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 ErrorLogger::log_retry_attempt(&error, 1, 3, true, context.clone()).await;
426
427 ErrorLogger::log_retry_attempt(&error, 3, 3, false, context.clone()).await;
429
430 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 error.log_error("validate_input").await;
440
441 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 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 let _span_guard = span.enter();
464 }
466}