use rmcp::model::LoggingLevel;
use serde_json::{Map, Value};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use tracing::subscriber::Interest;
use tracing::{Level, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::layer::Context;
#[must_use]
pub fn level_to_mcp(level: &Level) -> LoggingLevel {
match *level {
Level::TRACE | Level::DEBUG => LoggingLevel::Debug,
Level::INFO => LoggingLevel::Info,
Level::WARN => LoggingLevel::Warning,
Level::ERROR => LoggingLevel::Error,
}
}
#[derive(Clone, Debug)]
pub struct LogEvent {
pub level: LoggingLevel,
pub logger: String,
pub data: Value,
}
pub struct McpLoggingLayer {
event_tx: mpsc::UnboundedSender<LogEvent>,
log_level_filter: Arc<Mutex<LevelFilter>>,
}
impl McpLoggingLayer {
pub fn new(
event_tx: mpsc::UnboundedSender<LogEvent>,
log_level_filter: Arc<Mutex<LevelFilter>>,
) -> Self {
Self {
event_tx,
log_level_filter,
}
}
}
impl<S> Layer<S> for McpLoggingLayer
where
S: Subscriber,
{
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
let metadata = event.metadata();
let level = *metadata.level();
let target = metadata.target();
let filter_level = self
.log_level_filter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if level > *filter_level {
return;
}
drop(filter_level);
let mut fields = Map::new();
let mut visitor = MessageVisitor(&mut fields);
event.record(&mut visitor);
let mcp_level = level_to_mcp(&level);
let logger = target.to_string();
let data = Value::Object(fields);
let log_event = LogEvent {
level: mcp_level,
logger,
data,
};
let _ = self.event_tx.send(log_event);
}
fn register_callsite(&self, metadata: &'static tracing::Metadata<'static>) -> Interest {
let filter_level = self
.log_level_filter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if *metadata.level() <= *filter_level {
Interest::always()
} else {
Interest::never()
}
}
fn enabled(&self, metadata: &tracing::Metadata<'_>, _ctx: Context<'_, S>) -> bool {
let filter_level = self
.log_level_filter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*metadata.level() <= *filter_level
}
}
struct MessageVisitor<'a>(&'a mut Map<String, Value>);
impl tracing::field::Visit for MessageVisitor<'_> {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0.insert(
field.name().to_string(),
Value::String(format!("{value:?}")),
);
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
self.0
.insert(field.name().to_string(), Value::String(value.to_string()));
}
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
self.0
.insert(field.name().to_string(), Value::Number(value.into()));
}
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
self.0
.insert(field.name().to_string(), Value::Number(value.into()));
}
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
self.0.insert(field.name().to_string(), Value::Bool(value));
}
}