Skip to main content

vtcode_core/metrics/
mod.rs

1// Metrics collection and observability for MCP execution system
2//
3// Tracks performance, effectiveness, and security across all execution steps:
4// - Tool discovery (hit rate, response time)
5// - Code execution (duration, success rate, memory)
6// - SDK generation (overhead, caching)
7// - Data filtering (reduction ratio, token savings)
8// - Skill usage (adoption, reuse patterns)
9// - PII detection (pattern matches, audit trail)
10
11use chrono::{DateTime, Utc};
12use parking_lot::Mutex;
13use serde::{Deserialize, Serialize};
14use std::fmt::Write;
15use std::sync::Arc;
16use std::time::Instant;
17
18pub mod discovery_metrics;
19pub mod execution_metrics;
20pub mod filtering_metrics;
21pub mod sdk_metrics;
22pub mod security_metrics;
23pub mod skill_metrics;
24
25pub use discovery_metrics::DiscoveryMetrics;
26pub use execution_metrics::ExecutionMetrics;
27pub use filtering_metrics::FilteringMetrics;
28pub use sdk_metrics::SdkMetrics;
29pub use security_metrics::SecurityMetrics;
30pub use skill_metrics::SkillMetrics;
31
32/// Central metrics collector for all MCP execution activities
33#[derive(Clone)]
34pub struct MetricsCollector {
35    discovery: Arc<Mutex<DiscoveryMetrics>>,
36    execution: Arc<Mutex<ExecutionMetrics>>,
37    sdk: Arc<Mutex<SdkMetrics>>,
38    filtering: Arc<Mutex<FilteringMetrics>>,
39    skills: Arc<Mutex<SkillMetrics>>,
40    security: Arc<Mutex<SecurityMetrics>>,
41    start_time: Instant,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct MetricsSummary {
46    pub timestamp: DateTime<Utc>,
47    pub session_duration_ms: u64,
48    pub discovery: DiscoveryMetrics,
49    pub execution: ExecutionMetrics,
50    pub sdk: SdkMetrics,
51    pub filtering: FilteringMetrics,
52    pub skills: SkillMetrics,
53    pub security: SecurityMetrics,
54}
55
56impl MetricsCollector {
57    /// Create a new metrics collector
58    pub fn new() -> Self {
59        Self {
60            discovery: Arc::new(Mutex::new(DiscoveryMetrics::new())),
61            execution: Arc::new(Mutex::new(ExecutionMetrics::new())),
62            sdk: Arc::new(Mutex::new(SdkMetrics::new())),
63            filtering: Arc::new(Mutex::new(FilteringMetrics::new())),
64            skills: Arc::new(Mutex::new(SkillMetrics::new())),
65            security: Arc::new(Mutex::new(SecurityMetrics::new())),
66            start_time: Instant::now(),
67        }
68    }
69
70    // ========== Discovery Metrics ==========
71
72    /// Record a tool discovery query
73    pub fn record_discovery_query(&self, keyword: String, result_count: u64, response_time_ms: u64) {
74        let mut metrics = self.discovery.lock();
75        metrics.record_query(keyword, result_count, response_time_ms);
76    }
77
78    /// Record a failed discovery query
79    pub fn record_discovery_failure(&self, keyword: String) {
80        let mut metrics = self.discovery.lock();
81        metrics.record_failure(keyword);
82    }
83
84    /// Record a discovery cache hit
85    pub fn record_discovery_cache_hit(&self) {
86        let mut metrics = self.discovery.lock();
87        metrics.record_cache_hit();
88    }
89
90    // ========== Execution Metrics ==========
91
92    /// Record the start of a code execution
93    pub fn record_execution_start(&self, language: String) {
94        let mut metrics = self.execution.lock();
95        metrics.record_start(language);
96    }
97
98    /// Record successful execution completion
99    pub fn record_execution_complete(&self, language: String, duration_ms: u64, memory_mb: u64) {
100        let mut metrics = self.execution.lock();
101        metrics.record_complete(language, duration_ms, memory_mb, true);
102    }
103
104    /// Record failed execution
105    pub fn record_execution_failure(&self, language: String, duration_ms: u64) {
106        let mut metrics = self.execution.lock();
107        metrics.record_failure(language, duration_ms);
108    }
109
110    /// Record execution timeout
111    pub fn record_execution_timeout(&self, language: String, duration_ms: u64) {
112        let mut metrics = self.execution.lock();
113        metrics.record_timeout(language, duration_ms);
114    }
115
116    /// Record a retry attempt for execution-related workflows.
117    pub fn record_retry_attempt(&self) {
118        let mut metrics = self.execution.lock();
119        metrics.record_retry_attempt();
120    }
121
122    /// Record a successful retry that eventually recovered.
123    pub fn record_retry_success(&self) {
124        let mut metrics = self.execution.lock();
125        metrics.record_retry_success();
126    }
127
128    /// Record an execution that exhausted all retry attempts.
129    pub fn record_retry_exhausted(&self) {
130        let mut metrics = self.execution.lock();
131        metrics.record_retry_exhausted();
132    }
133
134    /// Record that a circuit breaker entered the open state.
135    pub fn record_circuit_open(&self) {
136        let mut metrics = self.execution.lock();
137        metrics.record_circuit_open();
138    }
139
140    /// Record that a circuit breaker transitioned to half-open.
141    pub fn record_half_open(&self) {
142        let mut metrics = self.execution.lock();
143        metrics.record_half_open();
144    }
145
146    /// Record a denied request caused by an open circuit breaker.
147    pub fn record_breaker_denial(&self) {
148        let mut metrics = self.execution.lock();
149        metrics.record_breaker_denial();
150    }
151
152    // ========== Circuit Breaker Helper Methods ==========
153
154    /// Record circuit breaker metrics for an optional collector.
155    ///
156    /// This helper method is used by both `McpCircuitBreaker` and `ToolCircuitBreaker`
157    /// to avoid code duplication.
158    pub fn record_circuit_breaker_metrics(
159        metrics: &Option<Arc<MetricsCollector>>,
160        half_open: bool,
161        denial: bool,
162        circuit_open: bool,
163    ) {
164        if let Some(metrics) = metrics {
165            if half_open {
166                metrics.record_half_open();
167            }
168            if denial {
169                metrics.record_breaker_denial();
170            }
171            if circuit_open {
172                metrics.record_circuit_open();
173            }
174        }
175    }
176
177    /// Record result size for filtering calculation
178    pub fn record_result_size(&self, size_bytes: usize) {
179        let mut metrics = self.execution.lock();
180        metrics.record_result_size(size_bytes);
181    }
182
183    // ========== SDK Metrics ==========
184
185    /// Record SDK generation
186    pub fn record_sdk_generation(&self, generation_time_ms: u64, tools_count: u64) {
187        let mut metrics = self.sdk.lock();
188        metrics.record_generation(generation_time_ms, tools_count);
189    }
190
191    /// Record SDK cache utilization
192    pub fn record_sdk_cache_hit(&self) {
193        let mut metrics = self.sdk.lock();
194        metrics.record_cache_hit();
195    }
196
197    /// Record the estimated token overhead of the tool definitions sent in a
198    /// request, accumulated across the session for transparency reporting.
199    pub fn record_sdk_tool_definition_tokens(&self, tokens: u64) {
200        let mut metrics = self.sdk.lock();
201        metrics.record_tool_definition_tokens(tokens);
202    }
203
204    // ========== Filtering Metrics ==========
205
206    /// Record a filtering operation
207    pub fn record_filtering_operation(
208        &self,
209        operation_type: String,
210        input_size: u64,
211        output_size: u64,
212        duration_ms: u64,
213    ) {
214        let mut metrics = self.filtering.lock();
215        metrics.record_operation(operation_type, input_size, output_size, duration_ms);
216    }
217
218    // ========== Skill Metrics ==========
219
220    /// Record skill execution
221    pub fn record_skill_execution(&self, skill_name: String, duration_ms: u64, success: bool) {
222        let mut metrics = self.skills.lock();
223        metrics.record_execution(skill_name, duration_ms, success);
224    }
225
226    /// Record skill creation
227    pub fn record_skill_created(&self, skill_name: String, language: String) {
228        let mut metrics = self.skills.lock();
229        metrics.record_created(skill_name, language);
230    }
231
232    /// Record skill deletion
233    pub fn record_skill_deleted(&self, skill_name: String) {
234        let mut metrics = self.skills.lock();
235        metrics.record_deleted(skill_name);
236    }
237
238    // ========== Security Metrics ==========
239
240    /// Record PII pattern detection
241    pub fn record_pii_detection(&self, pattern_type: String) {
242        let mut metrics = self.security.lock();
243        metrics.record_detection(pattern_type);
244    }
245
246    /// Record tokenization
247    pub fn record_pii_tokenization(&self, token_count: usize) {
248        let mut metrics = self.security.lock();
249        metrics.record_tokenization(token_count);
250    }
251
252    /// Record audit event
253    pub fn record_audit_event(&self, event_type: String, severity: String) {
254        let mut metrics = self.security.lock();
255        metrics.record_audit_event(event_type, severity);
256    }
257
258    // ========== Queries ==========
259
260    /// Get current discovery metrics snapshot
261    pub fn get_discovery_metrics(&self) -> DiscoveryMetrics {
262        self.discovery.lock().clone()
263    }
264
265    /// Get current execution metrics snapshot
266    pub fn get_execution_metrics(&self) -> ExecutionMetrics {
267        self.execution.lock().clone()
268    }
269
270    /// Get current SDK metrics snapshot
271    pub fn get_sdk_metrics(&self) -> SdkMetrics {
272        self.sdk.lock().clone()
273    }
274
275    /// Get current filtering metrics snapshot
276    pub fn get_filtering_metrics(&self) -> FilteringMetrics {
277        self.filtering.lock().clone()
278    }
279
280    /// Get current skill metrics snapshot
281    pub fn get_skill_metrics(&self) -> SkillMetrics {
282        self.skills.lock().clone()
283    }
284
285    /// Get current security metrics snapshot
286    pub fn get_security_metrics(&self) -> SecurityMetrics {
287        self.security.lock().clone()
288    }
289
290    /// Get comprehensive summary of all metrics
291    pub fn get_summary(&self) -> MetricsSummary {
292        MetricsSummary {
293            timestamp: Utc::now(),
294            session_duration_ms: self.start_time.elapsed().as_millis().min(u64::MAX as u128) as u64,
295            discovery: self.get_discovery_metrics(),
296            execution: self.get_execution_metrics(),
297            sdk: self.get_sdk_metrics(),
298            filtering: self.get_filtering_metrics(),
299            skills: self.get_skill_metrics(),
300            security: self.get_security_metrics(),
301        }
302    }
303
304    // ========== Export ==========
305
306    /// Export metrics as JSON
307    pub fn export_json(&self) -> anyhow::Result<serde_json::Value> {
308        let summary = self.get_summary();
309        Ok(serde_json::to_value(summary)?)
310    }
311
312    /// Export metrics in Prometheus format
313    pub fn export_prometheus(&self) -> String {
314        let discovery = self.get_discovery_metrics();
315        let execution = self.get_execution_metrics();
316        let filtering = self.get_filtering_metrics();
317        let skills = self.get_skill_metrics();
318        let security = self.get_security_metrics();
319
320        let mut output = String::new();
321
322        // Discovery metrics
323        let _ = write!(
324            output,
325            "# HELP vtcode_discovery_queries_total Total tool discovery queries\n\
326             # TYPE vtcode_discovery_queries_total counter\n\
327             vtcode_discovery_queries_total {}\n\n",
328            discovery.total_queries
329        );
330
331        let _ = write!(
332            output,
333            "# HELP vtcode_discovery_hit_rate Hit rate of discovery queries\n\
334             # TYPE vtcode_discovery_hit_rate gauge\n\
335             vtcode_discovery_hit_rate {}\n\n",
336            discovery.hit_rate()
337        );
338
339        // Execution metrics
340        let _ = write!(
341            output,
342            "# HELP vtcode_execution_total Total code executions\n\
343             # TYPE vtcode_execution_total counter\n\
344             vtcode_execution_total {}\n\n",
345            execution.total_executions
346        );
347
348        let _ = write!(
349            output,
350            "# HELP vtcode_execution_duration_ms Code execution average duration\n\
351             # TYPE vtcode_execution_duration_ms gauge\n\
352             vtcode_execution_duration_ms {}\n\n",
353            execution.avg_duration_ms()
354        );
355
356        let _ = write!(
357            output,
358            "# HELP vtcode_retry_attempts_total Total retry attempts\n\
359             # TYPE vtcode_retry_attempts_total counter\n\
360             vtcode_retry_attempts_total {}\n\n",
361            execution.retry_attempts
362        );
363
364        let _ = write!(
365            output,
366            "# HELP vtcode_retry_successes_total Total retries that later succeeded\n\
367             # TYPE vtcode_retry_successes_total counter\n\
368             vtcode_retry_successes_total {}\n\n",
369            execution.retry_successes
370        );
371
372        let _ = write!(
373            output,
374            "# HELP vtcode_retry_exhausted_total Total operations that exhausted retries\n\
375             # TYPE vtcode_retry_exhausted_total counter\n\
376             vtcode_retry_exhausted_total {}\n\n",
377            execution.retry_exhausted
378        );
379
380        let _ = write!(
381            output,
382            "# HELP vtcode_circuit_open_total Total circuit breaker open transitions\n\
383             # TYPE vtcode_circuit_open_total counter\n\
384             vtcode_circuit_open_total {}\n\n",
385            execution.circuit_open_events
386        );
387
388        let _ = write!(
389            output,
390            "# HELP vtcode_circuit_half_open_total Total circuit breaker half-open transitions\n\
391             # TYPE vtcode_circuit_half_open_total counter\n\
392             vtcode_circuit_half_open_total {}\n\n",
393            execution.half_open_events
394        );
395
396        let _ = write!(
397            output,
398            "# HELP vtcode_circuit_breaker_denials_total Total circuit breaker denials\n\
399             # TYPE vtcode_circuit_breaker_denials_total counter\n\
400             vtcode_circuit_breaker_denials_total {}\n\n",
401            execution.breaker_denials
402        );
403
404        // Filtering metrics
405        let _ = write!(
406            output,
407            "# HELP vtcode_filtering_operations_total Total filtering operations\n\
408             # TYPE vtcode_filtering_operations_total counter\n\
409             vtcode_filtering_operations_total {}\n\n",
410            filtering.total_operations
411        );
412
413        let _ = write!(
414            output,
415            "# HELP vtcode_context_tokens_saved Estimated tokens saved by filtering\n\
416             # TYPE vtcode_context_tokens_saved counter\n\
417             vtcode_context_tokens_saved {}\n\n",
418            filtering.estimated_tokens_saved()
419        );
420
421        // Skills metrics
422        let _ = write!(
423            output,
424            "# HELP vtcode_skills_total Total saved skills\n\
425             # TYPE vtcode_skills_total gauge\n\
426             vtcode_skills_total {}\n\n",
427            skills.total_skills
428        );
429
430        let _ = write!(
431            output,
432            "# HELP vtcode_skill_reuse_ratio Ratio of skill reuse\n\
433             # TYPE vtcode_skill_reuse_ratio gauge\n\
434             vtcode_skill_reuse_ratio {}\n\n",
435            skills.reuse_ratio()
436        );
437
438        // Security metrics
439        let _ = write!(
440            output,
441            "# HELP vtcode_pii_detections_total Total PII patterns detected\n\
442             # TYPE vtcode_pii_detections_total counter\n\
443             vtcode_pii_detections_total {}\n\n",
444            security.pii_detections
445        );
446
447        let _ = write!(
448            output,
449            "# HELP vtcode_tokens_created_total Total PII tokens created\n\
450             # TYPE vtcode_tokens_created_total counter\n\
451             vtcode_tokens_created_total {}\n\n",
452            security.tokens_created
453        );
454
455        output
456    }
457}
458
459impl Default for MetricsCollector {
460    fn default() -> Self {
461        Self::new()
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    #[test]
470    fn test_metrics_collector_creation() {
471        let collector = MetricsCollector::new();
472        let summary = collector.get_summary();
473        assert_eq!(summary.discovery.total_queries, 0);
474        assert_eq!(summary.execution.total_executions, 0);
475    }
476
477    #[test]
478    fn test_discovery_metrics_recording() {
479        let collector = MetricsCollector::new();
480        collector.record_discovery_query("file".to_owned(), 5, 50);
481
482        let metrics = collector.get_discovery_metrics();
483        assert_eq!(metrics.total_queries, 1);
484        assert!(metrics.avg_response_time_ms() > 0);
485    }
486
487    #[test]
488    fn test_execution_metrics_recording() {
489        let collector = MetricsCollector::new();
490        collector.record_execution_start("python3".to_owned());
491        collector.record_execution_complete("python3".to_owned(), 1000, 50);
492
493        let metrics = collector.get_execution_metrics();
494        assert_eq!(metrics.total_executions, 1);
495        assert_eq!(metrics.successful_executions, 1);
496        assert_eq!(metrics.avg_duration_ms(), 1000);
497    }
498
499    #[test]
500    fn test_metrics_summary_export() {
501        let collector = MetricsCollector::new();
502        collector.record_discovery_query("test".to_owned(), 3, 30);
503        collector.record_pii_detection("email".to_owned());
504
505        let summary = collector.get_summary();
506        assert_eq!(summary.discovery.total_queries, 1);
507        assert_eq!(summary.security.pii_detections, 1);
508    }
509
510    #[test]
511    fn test_reliability_metrics_recording() {
512        let collector = MetricsCollector::new();
513        collector.record_retry_attempt();
514        collector.record_retry_success();
515        collector.record_circuit_open();
516        collector.record_half_open();
517        collector.record_breaker_denial();
518
519        let metrics = collector.get_execution_metrics();
520        assert_eq!(metrics.retry_attempts, 1);
521        assert_eq!(metrics.retry_successes, 1);
522        assert_eq!(metrics.circuit_open_events, 1);
523        assert_eq!(metrics.half_open_events, 1);
524        assert_eq!(metrics.breaker_denials, 1);
525    }
526
527    #[test]
528    fn test_prometheus_export() {
529        let collector = MetricsCollector::new();
530        collector.record_execution_complete("python3".to_owned(), 500, 40);
531
532        let prometheus = collector.export_prometheus();
533        assert!(prometheus.contains("vtcode_execution_total"));
534        assert!(prometheus.contains("vtcode_execution_duration_ms"));
535        assert!(prometheus.contains("vtcode_retry_attempts_total"));
536    }
537
538    #[test]
539    fn test_json_export() {
540        let collector = MetricsCollector::new();
541        collector.record_discovery_query("test".to_owned(), 2, 25);
542
543        let json = collector.export_json().unwrap();
544        assert!(json.get("timestamp").is_some());
545        assert!(json.get("discovery").is_some());
546    }
547}