mz_environmentd/http/
mcp_metrics.rs1use mz_ore::metric;
19use mz_ore::metrics::MetricsRegistry;
20use mz_ore::stats::histogram_seconds_buckets;
21use prometheus::{HistogramTimer, HistogramVec, IntCounterVec};
22
23#[derive(Debug, Clone)]
28pub struct McpMetrics {
29 pub requests: IntCounterVec,
31 pub tool_calls: IntCounterVec,
33 pub tool_call_duration: HistogramVec,
35}
36
37pub struct ToolCallGuard<'a> {
44 metrics: &'a McpMetrics,
45 endpoint_label: &'static str,
46 tool_label: String,
47 status: &'static str,
48 _timer: HistogramTimer,
52}
53
54impl<'a> ToolCallGuard<'a> {
55 pub fn new(metrics: &'a McpMetrics, endpoint_label: &'static str, tool_label: String) -> Self {
58 let timer = metrics
59 .tool_call_duration
60 .with_label_values(&[endpoint_label, &tool_label])
61 .start_timer();
62 Self {
63 metrics,
64 endpoint_label,
65 tool_label,
66 status: "cancelled",
67 _timer: timer,
68 }
69 }
70
71 pub fn set_status(&mut self, status: &'static str) {
74 self.status = status;
75 }
76}
77
78impl Drop for ToolCallGuard<'_> {
79 fn drop(&mut self) {
80 self.metrics
81 .tool_calls
82 .with_label_values(&[self.endpoint_label, &self.tool_label, self.status])
83 .inc();
84 }
85}
86
87impl McpMetrics {
88 pub fn register_into(registry: &MetricsRegistry) -> Self {
89 Self {
90 requests: registry.register(metric!(
91 name: "mz_mcp_requests_total",
92 help: "Total number of MCP requests received.",
93 var_labels: ["endpoint_type", "method", "status"],
94 )),
95 tool_calls: registry.register(metric!(
96 name: "mz_mcp_tool_calls_total",
97 help: "Total number of MCP tools/call invocations.",
98 var_labels: ["endpoint_type", "tool_name", "status"],
99 )),
100 tool_call_duration: registry.register(metric!(
101 name: "mz_mcp_tool_call_duration_seconds",
102 help: "Duration of MCP tools/call invocations in seconds.",
103 var_labels: ["endpoint_type", "tool_name"],
104 buckets: histogram_seconds_buckets(0.000_128, 8.0),
105 )),
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::McpMetrics;
113 use mz_ore::metrics::MetricsRegistry;
114
115 #[mz_ore::test]
120 fn test_register_into() {
121 let registry = MetricsRegistry::new();
122 let metrics = McpMetrics::register_into(®istry);
123
124 metrics
125 .requests
126 .with_label_values(&["agent", "initialize", "ok"])
127 .inc_by(0);
128 metrics
129 .tool_calls
130 .with_label_values(&["agent", "read_data_product", "ok"])
131 .inc_by(0);
132 metrics
133 .tool_call_duration
134 .with_label_values(&["agent", "read_data_product"])
135 .observe(0.0);
136
137 let names: Vec<String> = registry
138 .gather()
139 .iter()
140 .map(|m| m.name().to_string())
141 .collect();
142
143 assert!(
144 names.iter().any(|n| n == "mz_mcp_requests_total"),
145 "mz_mcp_requests_total should be registered, got: {names:?}",
146 );
147 assert!(
148 names.iter().any(|n| n == "mz_mcp_tool_calls_total"),
149 "mz_mcp_tool_calls_total should be registered, got: {names:?}",
150 );
151 assert!(
152 names
153 .iter()
154 .any(|n| n == "mz_mcp_tool_call_duration_seconds"),
155 "mz_mcp_tool_call_duration_seconds should be registered, got: {names:?}",
156 );
157 }
158
159 #[mz_ore::test]
162 fn test_record_metrics() {
163 let registry = MetricsRegistry::new();
164 let metrics = McpMetrics::register_into(®istry);
165
166 metrics
167 .requests
168 .with_label_values(&["agent", "tools/call", "ok"])
169 .inc();
170 metrics
171 .requests
172 .with_label_values(&["agent", "tools/call", "ok"])
173 .inc();
174 metrics
175 .requests
176 .with_label_values(&["developer", "initialize", "ok"])
177 .inc();
178
179 metrics
180 .tool_calls
181 .with_label_values(&["agent", "read_data_product", "ok"])
182 .inc();
183 metrics
184 .tool_calls
185 .with_label_values(&["agent", "read_data_product", "DataProductNotFound"])
186 .inc();
187
188 metrics
189 .tool_call_duration
190 .with_label_values(&["agent", "read_data_product"])
191 .observe(0.123);
192
193 let gathered = registry.gather();
194
195 let requests = gathered
198 .iter()
199 .find(|m| m.name() == "mz_mcp_requests_total")
200 .expect("requests metric present");
201 assert_eq!(requests.get_metric().len(), 2);
202
203 let tool_calls = gathered
205 .iter()
206 .find(|m| m.name() == "mz_mcp_tool_calls_total")
207 .expect("tool_calls metric present");
208 assert_eq!(tool_calls.get_metric().len(), 2);
209
210 let duration = gathered
212 .iter()
213 .find(|m| m.name() == "mz_mcp_tool_call_duration_seconds")
214 .expect("tool_call_duration metric present");
215 assert_eq!(
216 duration.get_metric()[0].get_histogram().get_sample_count(),
217 1
218 );
219 }
220}