Skip to main content

prism_mcp_rs/core/
metrics.rs

1//! Metrics collection for the MCP SDK
2//!
3//! Module provides structured metrics collection for error tracking,
4//! performance monitoring, and operational insights.
5
6use 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/// Metrics collector for MCP operations
15#[derive(Debug, Clone)]
16pub struct MetricsCollector {
17    /// Error counters by category and recoverability
18    error_counters: Arc<RwLock<HashMap<String, AtomicU64>>>,
19    /// Request counters by method
20    request_counters: Arc<RwLock<HashMap<String, AtomicU64>>>,
21    /// Connection attempt counters
22    connection_counters: Arc<RwLock<HashMap<String, AtomicU64>>>,
23    /// Retry attempt counters
24    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    /// Create a new metrics collector
35    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    /// Record an error occurrence
45    pub async fn record_error(&self, error: &McpError, context: &str) {
46        let category = error.category();
47        let recoverable = error.is_recoverable();
48
49        // Create metric key
50        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        // Also record by category only
57        let category_key = format!("mcp_errors_by_category:{category}");
58        self.increment_counter(&self.error_counters, &category_key)
59            .await;
60
61        // Log the metric for external systems
62        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    /// Record a request
72    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    /// Record a connection attempt
85    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    /// Record a retry attempt
99    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    /// Get current error metrics
122    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    /// Get current request metrics
131    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    /// Get current connection metrics
140    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    /// Get current retry metrics
149    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    /// Get all metrics
158    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    /// Reset all metrics (useful for testing)
168    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    /// Internal helper to increment a counter
178    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/// Summary of all metrics
192#[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
200/// Global metrics collector instance
201static GLOBAL_METRICS: once_cell::sync::Lazy<MetricsCollector> =
202    once_cell::sync::Lazy::new(MetricsCollector::new);
203
204/// Get the global metrics collector
205pub fn global_metrics() -> &'static MetricsCollector {
206    &GLOBAL_METRICS
207}
208
209/// Helper macro for recording errors with metrics
210#[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/// Helper macro for recording requests with metrics
219#[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/// Helper macro for recording connection attempts with metrics
228#[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/// Helper macro for recording retry attempts with metrics
239#[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        // Should have both specific and category metrics
276        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}