Skip to main content

code_analyze_mcp/
logging.rs

1//! MCP logging integration via tracing.
2//!
3//! Provides a custom tracing subscriber that forwards log events to MCP clients.
4//! Maps Rust tracing levels to `MCP` [`LoggingLevel`].
5
6use rmcp::model::LoggingLevel;
7use serde_json::{Map, Value};
8use std::sync::{Arc, Mutex};
9use tokio::sync::mpsc;
10use tracing::subscriber::Interest;
11use tracing::{Level, Subscriber};
12use tracing_subscriber::Layer;
13use tracing_subscriber::filter::LevelFilter;
14use tracing_subscriber::layer::Context;
15
16/// Maps `tracing::Level` to `MCP` [`LoggingLevel`].
17#[must_use]
18pub fn level_to_mcp(level: &Level) -> LoggingLevel {
19    match *level {
20        Level::TRACE | Level::DEBUG => LoggingLevel::Debug,
21        Level::INFO => LoggingLevel::Info,
22        Level::WARN => LoggingLevel::Warning,
23        Level::ERROR => LoggingLevel::Error,
24    }
25}
26
27/// Lightweight event sent from `McpLoggingLayer` to consumer task via unbounded channel.
28#[derive(Clone, Debug)]
29pub struct LogEvent {
30    pub level: LoggingLevel,
31    pub logger: String,
32    pub data: Value,
33}
34
35/// Custom tracing Layer that bridges tracing events to `MCP` client via unbounded channel.
36/// Sends lightweight [`LogEvent`] to channel; consumer task in `on_initialized` drains with `recv_many`.
37pub struct McpLoggingLayer {
38    event_tx: mpsc::UnboundedSender<LogEvent>,
39    log_level_filter: Arc<Mutex<LevelFilter>>,
40}
41
42impl McpLoggingLayer {
43    pub fn new(
44        event_tx: mpsc::UnboundedSender<LogEvent>,
45        log_level_filter: Arc<Mutex<LevelFilter>>,
46    ) -> Self {
47        Self {
48            event_tx,
49            log_level_filter,
50        }
51    }
52}
53
54impl<S> Layer<S> for McpLoggingLayer
55where
56    S: Subscriber,
57{
58    fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
59        let metadata = event.metadata();
60        let level = *metadata.level();
61        let target = metadata.target();
62
63        // Check if event level passes the current filter before processing
64        let filter_level = self
65            .log_level_filter
66            .lock()
67            .unwrap_or_else(std::sync::PoisonError::into_inner);
68        if level > *filter_level {
69            return;
70        }
71        drop(filter_level);
72
73        // Extract fields from the event using a visitor that collects into a Map.
74        let mut fields = Map::new();
75        let mut visitor = MessageVisitor(&mut fields);
76        event.record(&mut visitor);
77
78        let mcp_level = level_to_mcp(&level);
79        let logger = target.to_string();
80        let data = Value::Object(fields);
81
82        // Send LogEvent to channel without blocking on_event.
83        let log_event = LogEvent {
84            level: mcp_level,
85            logger,
86            data,
87        };
88
89        // Ignore send error if receiver is dropped (channel closed).
90        let _ = self.event_tx.send(log_event);
91    }
92
93    fn register_callsite(&self, metadata: &'static tracing::Metadata<'static>) -> Interest {
94        let filter_level = self
95            .log_level_filter
96            .lock()
97            .unwrap_or_else(std::sync::PoisonError::into_inner);
98        if *metadata.level() <= *filter_level {
99            Interest::always()
100        } else {
101            Interest::never()
102        }
103    }
104
105    fn enabled(&self, metadata: &tracing::Metadata<'_>, _ctx: Context<'_, S>) -> bool {
106        let filter_level = self
107            .log_level_filter
108            .lock()
109            .unwrap_or_else(std::sync::PoisonError::into_inner);
110        *metadata.level() <= *filter_level
111    }
112}
113
114/// Visitor to extract fields from tracing event into a JSON Map.
115struct MessageVisitor<'a>(&'a mut Map<String, Value>);
116
117impl tracing::field::Visit for MessageVisitor<'_> {
118    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
119        self.0.insert(
120            field.name().to_string(),
121            Value::String(format!("{value:?}")),
122        );
123    }
124
125    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
126        self.0
127            .insert(field.name().to_string(), Value::String(value.to_string()));
128    }
129
130    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
131        self.0
132            .insert(field.name().to_string(), Value::Number(value.into()));
133    }
134
135    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
136        self.0
137            .insert(field.name().to_string(), Value::Number(value.into()));
138    }
139
140    fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
141        self.0.insert(field.name().to_string(), Value::Bool(value));
142    }
143}