1use std::convert::Infallible;
38use std::future::Future;
39use std::pin::Pin;
40use std::task::{Context, Poll};
41use std::time::Instant;
42
43use tower::Layer;
44use tower_service::Service;
45use tracing::{Instrument, Level, Span};
46
47use crate::protocol::McpRequest;
48use crate::router::{RouterRequest, RouterResponse};
49
50#[derive(Debug, Clone, Copy)]
75pub struct McpTracingLayer {
76 level: Level,
77}
78
79impl Default for McpTracingLayer {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl McpTracingLayer {
86 pub fn new() -> Self {
88 Self { level: Level::INFO }
89 }
90
91 pub fn level(mut self, level: Level) -> Self {
95 self.level = level;
96 self
97 }
98}
99
100impl<S> Layer<S> for McpTracingLayer {
101 type Service = McpTracingService<S>;
102
103 fn layer(&self, inner: S) -> Self::Service {
104 McpTracingService {
105 inner,
106 level: self.level,
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
115pub struct McpTracingService<S> {
116 inner: S,
117 level: Level,
118}
119
120impl<S> Service<RouterRequest> for McpTracingService<S>
121where
122 S: Service<RouterRequest, Response = RouterResponse, Error = Infallible>
123 + Clone
124 + Send
125 + 'static,
126 S::Future: Send,
127{
128 type Response = RouterResponse;
129 type Error = Infallible;
130 type Future = Pin<Box<dyn Future<Output = Result<RouterResponse, Infallible>> + Send>>;
131
132 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
133 self.inner.poll_ready(cx)
134 }
135
136 fn call(&mut self, req: RouterRequest) -> Self::Future {
137 let method = req.inner.method_name().to_string();
138 let request_id = format!("{:?}", req.id);
139
140 let (operation_name, operation_target) = extract_operation_details(&req.inner);
142
143 let span = create_span(
145 self.level,
146 &method,
147 &request_id,
148 operation_name,
149 operation_target,
150 );
151
152 let start = Instant::now();
153 let fut = self.inner.call(req);
154 let level = self.level;
155
156 Box::pin(
157 async move {
158 let result = fut.await;
159 let duration = start.elapsed();
160
161 match &result {
162 Ok(response) => {
163 let duration_ms = duration.as_secs_f64() * 1000.0;
164 match &response.inner {
165 Ok(_) => {
166 log_success(level, &method, duration_ms);
167 }
168 Err(err) => {
169 tracing::warn!(
170 method = %method,
171 error_code = err.code,
172 error_message = %err.message,
173 duration_ms = duration_ms,
174 "MCP request failed"
175 );
176 }
177 }
178 }
179 Err(_) => {
180 tracing::error!(method = %method, "MCP request error (infallible)");
182 }
183 }
184
185 result
186 }
187 .instrument(span),
188 )
189 }
190}
191
192pub(crate) fn extract_operation_details(
194 req: &McpRequest,
195) -> (Option<&'static str>, Option<String>) {
196 match req {
197 McpRequest::CallTool(params) => (Some("tool"), Some(params.name.clone())),
198 McpRequest::ReadResource(params) => (Some("resource"), Some(params.uri.clone())),
199 McpRequest::GetPrompt(params) => (Some("prompt"), Some(params.name.clone())),
200 McpRequest::ListTools(_) => (Some("list"), Some("tools".to_string())),
201 McpRequest::ListResources(_) => (Some("list"), Some("resources".to_string())),
202 McpRequest::ListResourceTemplates(_) => {
203 (Some("list"), Some("resource_templates".to_string()))
204 }
205 McpRequest::ListPrompts(_) => (Some("list"), Some("prompts".to_string())),
206 McpRequest::SubscribeResource(params) => (Some("subscribe"), Some(params.uri.clone())),
207 McpRequest::UnsubscribeResource(params) => (Some("unsubscribe"), Some(params.uri.clone())),
208 McpRequest::GetTaskInfo(params) => (Some("task"), Some(params.task_id.clone())),
209 McpRequest::CancelTask(params) => (Some("cancel"), Some(params.task_id.clone())),
210 McpRequest::Complete(params) => {
211 let ref_type = match ¶ms.reference {
212 crate::protocol::CompletionReference::Resource { uri } => {
213 format!("resource:{}", uri)
214 }
215 crate::protocol::CompletionReference::Prompt { name } => {
216 format!("prompt:{}", name)
217 }
218 _ => "unknown".to_string(),
219 };
220 (Some("complete"), Some(ref_type))
221 }
222 McpRequest::SetLoggingLevel(params) => {
223 (Some("logging"), Some(format!("{:?}", params.level)))
224 }
225 McpRequest::Initialize(_) => (Some("init"), None),
226 McpRequest::Ping => (Some("ping"), None),
227 McpRequest::Unknown { method, .. } => (Some("unknown"), Some(method.clone())),
228 _ => (Some("unknown"), None),
229 }
230}
231
232fn create_span(
234 level: Level,
235 method: &str,
236 request_id: &str,
237 operation_name: Option<&str>,
238 operation_target: Option<String>,
239) -> Span {
240 match level {
241 Level::TRACE => tracing::trace_span!(
242 "mcp_request",
243 method = %method,
244 request_id = %request_id,
245 operation = operation_name,
246 target = operation_target.as_deref(),
247 ),
248 Level::DEBUG => tracing::debug_span!(
249 "mcp_request",
250 method = %method,
251 request_id = %request_id,
252 operation = operation_name,
253 target = operation_target.as_deref(),
254 ),
255 Level::INFO => tracing::info_span!(
256 "mcp_request",
257 method = %method,
258 request_id = %request_id,
259 operation = operation_name,
260 target = operation_target.as_deref(),
261 ),
262 Level::WARN => tracing::warn_span!(
263 "mcp_request",
264 method = %method,
265 request_id = %request_id,
266 operation = operation_name,
267 target = operation_target.as_deref(),
268 ),
269 Level::ERROR => tracing::error_span!(
270 "mcp_request",
271 method = %method,
272 request_id = %request_id,
273 operation = operation_name,
274 target = operation_target.as_deref(),
275 ),
276 }
277}
278
279fn log_success(level: Level, method: &str, duration_ms: f64) {
281 match level {
282 Level::TRACE => {
283 tracing::trace!(method = %method, duration_ms = duration_ms, "MCP request completed")
284 }
285 Level::DEBUG => {
286 tracing::debug!(method = %method, duration_ms = duration_ms, "MCP request completed")
287 }
288 Level::INFO => {
289 tracing::info!(method = %method, duration_ms = duration_ms, "MCP request completed")
290 }
291 Level::WARN => {
292 tracing::warn!(method = %method, duration_ms = duration_ms, "MCP request completed")
293 }
294 Level::ERROR => {
295 tracing::error!(method = %method, duration_ms = duration_ms, "MCP request completed")
296 }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_layer_creation() {
306 let layer = McpTracingLayer::new();
307 assert_eq!(layer.level, Level::INFO);
308
309 let layer = McpTracingLayer::new().level(Level::DEBUG);
310 assert_eq!(layer.level, Level::DEBUG);
311 }
312
313 #[test]
314 fn test_extract_operation_details() {
315 use crate::protocol::{CallToolParams, GetPromptParams, ReadResourceParams};
316 use serde_json::Value;
317 use std::collections::HashMap;
318
319 let req = McpRequest::CallTool(CallToolParams {
321 input_responses: None,
322 request_state: None,
323 name: "my_tool".to_string(),
324 arguments: Value::Null,
325 meta: None,
326 task: None,
327 });
328 let (name, target) = extract_operation_details(&req);
329 assert_eq!(name, Some("tool"));
330 assert_eq!(target, Some("my_tool".to_string()));
331
332 let req = McpRequest::ReadResource(ReadResourceParams {
334 input_responses: None,
335 request_state: None,
336 uri: "file:///test.txt".to_string(),
337 meta: None,
338 });
339 let (name, target) = extract_operation_details(&req);
340 assert_eq!(name, Some("resource"));
341 assert_eq!(target, Some("file:///test.txt".to_string()));
342
343 let req = McpRequest::GetPrompt(GetPromptParams {
345 input_responses: None,
346 request_state: None,
347 name: "my_prompt".to_string(),
348 arguments: HashMap::new(),
349 meta: None,
350 });
351 let (name, target) = extract_operation_details(&req);
352 assert_eq!(name, Some("prompt"));
353 assert_eq!(target, Some("my_prompt".to_string()));
354
355 let req = McpRequest::Ping;
357 let (name, target) = extract_operation_details(&req);
358 assert_eq!(name, Some("ping"));
359 assert_eq!(target, None);
360 }
361
362 #[test]
363 fn test_extract_operation_details_list_operations() {
364 use crate::protocol::{
365 ListPromptsParams, ListResourceTemplatesParams, ListResourcesParams, ListToolsParams,
366 };
367
368 let (name, target) = extract_operation_details(&McpRequest::ListTools(ListToolsParams {
369 cursor: None,
370 meta: None,
371 }));
372 assert_eq!(name, Some("list"));
373 assert_eq!(target, Some("tools".to_string()));
374
375 let (name, target) =
376 extract_operation_details(&McpRequest::ListResources(ListResourcesParams {
377 cursor: None,
378 meta: None,
379 }));
380 assert_eq!(name, Some("list"));
381 assert_eq!(target, Some("resources".to_string()));
382
383 let (name, target) = extract_operation_details(&McpRequest::ListResourceTemplates(
384 ListResourceTemplatesParams {
385 cursor: None,
386 meta: None,
387 },
388 ));
389 assert_eq!(name, Some("list"));
390 assert_eq!(target, Some("resource_templates".to_string()));
391
392 let (name, target) =
393 extract_operation_details(&McpRequest::ListPrompts(ListPromptsParams {
394 cursor: None,
395 meta: None,
396 }));
397 assert_eq!(name, Some("list"));
398 assert_eq!(target, Some("prompts".to_string()));
399 }
400
401 #[test]
402 fn test_extract_operation_details_initialize() {
403 use crate::protocol::{ClientCapabilities, Implementation, InitializeParams};
404
405 let req = McpRequest::Initialize(InitializeParams {
406 protocol_version: "2025-11-25".to_string(),
407 capabilities: ClientCapabilities::default(),
408 client_info: Implementation {
409 name: "test".to_string(),
410 version: "1.0".to_string(),
411 ..Default::default()
412 },
413 meta: None,
414 });
415 let (name, target) = extract_operation_details(&req);
416 assert_eq!(name, Some("init"));
417 assert_eq!(target, None);
418 }
419
420 #[test]
421 fn test_extract_operation_details_subscribe() {
422 use crate::protocol::SubscribeResourceParams;
423
424 let req = McpRequest::SubscribeResource(SubscribeResourceParams {
425 uri: "file:///watched.txt".to_string(),
426 meta: None,
427 });
428 let (name, target) = extract_operation_details(&req);
429 assert_eq!(name, Some("subscribe"));
430 assert_eq!(target, Some("file:///watched.txt".to_string()));
431 }
432
433 #[test]
434 fn test_extract_operation_details_logging_level() {
435 use crate::protocol::{LogLevel, SetLogLevelParams};
436
437 let req = McpRequest::SetLoggingLevel(SetLogLevelParams {
438 level: LogLevel::Debug,
439 meta: None,
440 });
441 let (name, target) = extract_operation_details(&req);
442 assert_eq!(name, Some("logging"));
443 assert!(target.is_some());
444 }
445
446 #[test]
447 fn test_extract_operation_details_completion() {
448 use crate::protocol::{CompleteParams, CompletionArgument, CompletionReference};
449
450 let req = McpRequest::Complete(CompleteParams {
451 reference: CompletionReference::Prompt {
452 name: "my-prompt".to_string(),
453 },
454 argument: CompletionArgument::new("arg1", "val"),
455 context: None,
456 meta: None,
457 });
458 let (name, target) = extract_operation_details(&req);
459 assert_eq!(name, Some("complete"));
460 assert_eq!(target, Some("prompt:my-prompt".to_string()));
461
462 let req = McpRequest::Complete(CompleteParams {
463 reference: CompletionReference::Resource {
464 uri: "file:///test".to_string(),
465 },
466 argument: CompletionArgument::new("arg1", "val"),
467 context: None,
468 meta: None,
469 });
470 let (_, target) = extract_operation_details(&req);
471 assert_eq!(target, Some("resource:file:///test".to_string()));
472 }
473
474 #[test]
475 fn test_extract_operation_details_unknown_method() {
476 let req = McpRequest::Unknown {
477 method: "custom/method".to_string(),
478 params: None,
479 };
480 let (name, target) = extract_operation_details(&req);
481 assert_eq!(name, Some("unknown"));
482 assert_eq!(target, Some("custom/method".to_string()));
483 }
484
485 #[test]
486 fn test_layer_level_configuration() {
487 let layer = McpTracingLayer::new().level(Level::TRACE);
488 assert_eq!(layer.level, Level::TRACE);
489
490 let layer = McpTracingLayer::new().level(Level::ERROR);
491 assert_eq!(layer.level, Level::ERROR);
492
493 let layer = McpTracingLayer::new().level(Level::WARN);
494 assert_eq!(layer.level, Level::WARN);
495 }
496}