Skip to main content

code_analyze_mcp/
logging.rs

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