zagens_runtime_adapters/mcp/
observability.rs1use std::collections::VecDeque;
4use std::sync::{Mutex, OnceLock};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7const MAX_RECENT_CALLS: usize = 64;
8
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
11pub struct McpCallRecord {
12 pub timestamp_ms: u128,
13 pub server: String,
14 pub method: String,
15 pub duration_ms: u64,
16 pub success: bool,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub error: Option<String>,
19 pub result_bytes: usize,
20}
21
22struct CallLog {
23 entries: VecDeque<McpCallRecord>,
24}
25
26impl CallLog {
27 fn push(&mut self, record: McpCallRecord) {
28 if self.entries.len() >= MAX_RECENT_CALLS {
29 self.entries.pop_front();
30 }
31 self.entries.push_back(record);
32 }
33}
34
35static CALL_LOG: OnceLock<Mutex<CallLog>> = OnceLock::new();
36
37fn call_log() -> &'static Mutex<CallLog> {
38 CALL_LOG.get_or_init(|| {
39 Mutex::new(CallLog {
40 entries: VecDeque::new(),
41 })
42 })
43}
44
45fn now_ms() -> u128 {
46 SystemTime::now()
47 .duration_since(UNIX_EPOCH)
48 .map_or(0, |d| d.as_millis())
49}
50
51pub fn record_mcp_call(
53 server: impl Into<String>,
54 method: impl Into<String>,
55 duration_ms: u64,
56 success: bool,
57 error: Option<String>,
58 result_bytes: usize,
59) {
60 let record = McpCallRecord {
61 timestamp_ms: now_ms(),
62 server: server.into(),
63 method: method.into(),
64 duration_ms,
65 success,
66 error: error.map(|e| super::diagnostics::redact_body_preview(&e)),
67 result_bytes,
68 };
69 if let Ok(mut log) = call_log().lock() {
70 log.push(record);
71 }
72}
73
74#[must_use]
76pub fn recent_mcp_calls() -> Vec<McpCallRecord> {
77 call_log()
78 .lock()
79 .ok()
80 .map(|log| log.entries.iter().cloned().collect())
81 .unwrap_or_default()
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn recent_calls_ring_buffer() {
90 for i in 0..70 {
91 record_mcp_call("srv", format!("tool_{i}"), 1, true, None, 10);
92 }
93 let calls = recent_mcp_calls();
94 assert!(calls.len() <= MAX_RECENT_CALLS);
95 assert_eq!(calls.last().map(|c| c.method.as_str()), Some("tool_69"));
96 }
97}