1use std::collections::HashMap;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use tracing::{info, warn};
11
12use crate::core::error::McpError;
13
14#[derive(Debug, Clone)]
16pub struct MetricsCollector {
17 error_counters: Arc<RwLock<HashMap<String, AtomicU64>>>,
19 request_counters: Arc<RwLock<HashMap<String, AtomicU64>>>,
21 connection_counters: Arc<RwLock<HashMap<String, AtomicU64>>>,
23 retry_counters: Arc<RwLock<HashMap<String, AtomicU64>>>,
25}
26
27impl Default for MetricsCollector {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl MetricsCollector {
34 pub fn new() -> Self {
36 Self {
37 error_counters: Arc::new(RwLock::new(HashMap::new())),
38 request_counters: Arc::new(RwLock::new(HashMap::new())),
39 connection_counters: Arc::new(RwLock::new(HashMap::new())),
40 retry_counters: Arc::new(RwLock::new(HashMap::new())),
41 }
42 }
43
44 pub async fn record_error(&self, error: &McpError, context: &str) {
46 let category = error.category();
47 let recoverable = error.is_recoverable();
48
49 let key = format!(
51 "mcp_errors_total:category={category}:recoverable={recoverable}:context={context}"
52 );
53
54 self.increment_counter(&self.error_counters, &key).await;
55
56 let category_key = format!("mcp_errors_by_category:{category}");
58 self.increment_counter(&self.error_counters, &category_key)
59 .await;
60
61 info!(
63 target: "mcp_metrics",
64 error_category = category,
65 error_recoverable = recoverable,
66 error_context = context,
67 "Error recorded in metrics"
68 );
69 }
70
71 pub async fn record_request(&self, method: &str, transport: &str) {
73 let key = format!("mcp_requests_total:method={method}:transport={transport}");
74 self.increment_counter(&self.request_counters, &key).await;
75
76 info!(
77 target: "mcp_metrics",
78 request_method = method,
79 transport_type = transport,
80 "Request recorded in metrics"
81 );
82 }
83
84 pub async fn record_connection_attempt(&self, transport: &str, success: bool) {
86 let key = format!("mcp_connections_total:transport={transport}:success={success}");
87 self.increment_counter(&self.connection_counters, &key)
88 .await;
89
90 info!(
91 target: "mcp_metrics",
92 transport_type = transport,
93 connection_success = success,
94 "Connection attempt recorded in metrics"
95 );
96 }
97
98 pub async fn record_retry_attempt(
100 &self,
101 operation: &str,
102 attempt: u32,
103 error_category: &str,
104 will_retry: bool,
105 ) {
106 let key = format!(
107 "mcp_retries_total:operation={operation}:attempt={attempt}:error_category={error_category}:will_retry={will_retry}"
108 );
109 self.increment_counter(&self.retry_counters, &key).await;
110
111 info!(
112 target: "mcp_metrics",
113 retry_operation = operation,
114 retry_attempt = attempt,
115 error_category = error_category,
116 will_retry_again = will_retry,
117 "Retry attempt recorded in metrics"
118 );
119 }
120
121 pub async fn get_error_metrics(&self) -> HashMap<String, u64> {
123 let counters = self.error_counters.read().await;
124 counters
125 .iter()
126 .map(|(k, v)| (k.clone(), v.load(Ordering::Relaxed)))
127 .collect()
128 }
129
130 pub async fn get_request_metrics(&self) -> HashMap<String, u64> {
132 let counters = self.request_counters.read().await;
133 counters
134 .iter()
135 .map(|(k, v)| (k.clone(), v.load(Ordering::Relaxed)))
136 .collect()
137 }
138
139 pub async fn get_connection_metrics(&self) -> HashMap<String, u64> {
141 let counters = self.connection_counters.read().await;
142 counters
143 .iter()
144 .map(|(k, v)| (k.clone(), v.load(Ordering::Relaxed)))
145 .collect()
146 }
147
148 pub async fn get_retry_metrics(&self) -> HashMap<String, u64> {
150 let counters = self.retry_counters.read().await;
151 counters
152 .iter()
153 .map(|(k, v)| (k.clone(), v.load(Ordering::Relaxed)))
154 .collect()
155 }
156
157 pub async fn get_all_metrics(&self) -> MetricsSummary {
159 MetricsSummary {
160 errors: self.get_error_metrics().await,
161 requests: self.get_request_metrics().await,
162 connections: self.get_connection_metrics().await,
163 retries: self.get_retry_metrics().await,
164 }
165 }
166
167 pub async fn reset(&self) {
169 self.error_counters.write().await.clear();
170 self.request_counters.write().await.clear();
171 self.connection_counters.write().await.clear();
172 self.retry_counters.write().await.clear();
173
174 warn!(target: "mcp_metrics", "Metrics collector reset");
175 }
176
177 async fn increment_counter(
179 &self,
180 counters: &Arc<RwLock<HashMap<String, AtomicU64>>>,
181 key: &str,
182 ) {
183 let mut counters_guard = counters.write().await;
184 let counter = counters_guard
185 .entry(key.to_string())
186 .or_insert_with(|| AtomicU64::new(0));
187 counter.fetch_add(1, Ordering::Relaxed);
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct MetricsSummary {
194 pub errors: HashMap<String, u64>,
195 pub requests: HashMap<String, u64>,
196 pub connections: HashMap<String, u64>,
197 pub retries: HashMap<String, u64>,
198}
199
200static GLOBAL_METRICS: once_cell::sync::Lazy<MetricsCollector> =
202 once_cell::sync::Lazy::new(MetricsCollector::new);
203
204pub fn global_metrics() -> &'static MetricsCollector {
206 &GLOBAL_METRICS
207}
208
209#[macro_export]
211macro_rules! record_error_metric {
212 ($error:expr, $context:expr) => {
213 let metrics = $crate::core::metrics::global_metrics();
214 metrics.record_error($error, $context).await;
215 };
216}
217
218#[macro_export]
220macro_rules! record_request_metric {
221 ($method:expr, $transport:expr) => {
222 let metrics = $crate::core::metrics::global_metrics();
223 metrics.record_request($method, $transport).await;
224 };
225}
226
227#[macro_export]
229macro_rules! record_connection_metric {
230 ($transport:expr, $success:expr) => {
231 let metrics = $crate::core::metrics::global_metrics();
232 metrics
233 .record_connection_attempt($transport, $success)
234 .await;
235 };
236}
237
238#[macro_export]
240macro_rules! record_retry_metric {
241 ($operation:expr, $attempt:expr, $error_category:expr, $will_retry:expr) => {
242 let metrics = $crate::core::metrics::global_metrics();
243 metrics
244 .record_retry_attempt($operation, $attempt, $error_category, $will_retry)
245 .await;
246 };
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::core::error::McpError;
253
254 #[tokio::test]
255 async fn test_metrics_collector_creation() {
256 let metrics = MetricsCollector::new();
257 let summary = metrics.get_all_metrics().await;
258
259 assert!(summary.errors.is_empty());
260 assert!(summary.requests.is_empty());
261 assert!(summary.connections.is_empty());
262 assert!(summary.retries.is_empty());
263 }
264
265 #[tokio::test]
266 async fn test_error_recording() {
267 let metrics = MetricsCollector::new();
268 let error = McpError::connection("test error");
269
270 metrics.record_error(&error, "test_context").await;
271
272 let error_metrics = metrics.get_error_metrics().await;
273 assert!(!error_metrics.is_empty());
274
275 let has_specific = error_metrics.keys().any(|k| k.contains("test_context"));
277 let has_category = error_metrics.keys().any(|k| k.contains("connection"));
278
279 assert!(has_specific);
280 assert!(has_category);
281 }
282
283 #[tokio::test]
284 async fn test_request_recording() {
285 let metrics = MetricsCollector::new();
286
287 metrics.record_request("tools/list", "http").await;
288
289 let request_metrics = metrics.get_request_metrics().await;
290 assert!(!request_metrics.is_empty());
291
292 let key_exists = request_metrics
293 .keys()
294 .any(|k| k.contains("tools/list") && k.contains("http"));
295 assert!(key_exists);
296 }
297
298 #[tokio::test]
299 async fn test_connection_recording() {
300 let metrics = MetricsCollector::new();
301
302 metrics.record_connection_attempt("websocket", true).await;
303 metrics.record_connection_attempt("websocket", false).await;
304
305 let connection_metrics = metrics.get_connection_metrics().await;
306 assert!(!connection_metrics.is_empty());
307
308 let success_key_exists = connection_metrics
309 .keys()
310 .any(|k| k.contains("websocket") && k.contains("success=true"));
311 let failure_key_exists = connection_metrics
312 .keys()
313 .any(|k| k.contains("websocket") && k.contains("success=false"));
314
315 assert!(success_key_exists);
316 assert!(failure_key_exists);
317 }
318
319 #[tokio::test]
320 async fn test_retry_recording() {
321 let metrics = MetricsCollector::new();
322
323 metrics
324 .record_retry_attempt("send_request", 1, "connection", true)
325 .await;
326
327 let retry_metrics = metrics.get_retry_metrics().await;
328 assert!(!retry_metrics.is_empty());
329
330 let key_exists = retry_metrics.keys().any(|k| {
331 k.contains("send_request") && k.contains("attempt=1") && k.contains("connection")
332 });
333 assert!(key_exists);
334 }
335
336 #[tokio::test]
337 async fn test_metrics_reset() {
338 let metrics = MetricsCollector::new();
339 let error = McpError::timeout("test");
340
341 metrics.record_error(&error, "test").await;
342 assert!(!metrics.get_error_metrics().await.is_empty());
343
344 metrics.reset().await;
345 assert!(metrics.get_error_metrics().await.is_empty());
346 }
347
348 #[tokio::test]
349 async fn test_global_metrics() {
350 let metrics = global_metrics();
351 let error = McpError::validation("test global metrics");
352
353 metrics.record_error(&error, "global_test").await;
354
355 let error_metrics = metrics.get_error_metrics().await;
356 let has_global_test = error_metrics.keys().any(|k| k.contains("global_test"));
357 assert!(has_global_test);
358 }
359}