Skip to main content

ucp_agent/
metrics.rs

1//! Metrics and observability for agent sessions.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
6use std::time::{Duration, Instant};
7
8/// Metrics for an agent session.
9#[derive(Debug, Default)]
10pub struct SessionMetrics {
11    /// Total number of navigation operations.
12    pub navigation_count: AtomicUsize,
13    /// Total number of expansion operations.
14    pub expansion_count: AtomicUsize,
15    /// Total number of search operations.
16    pub search_count: AtomicUsize,
17    /// Total number of context additions.
18    pub context_add_count: AtomicUsize,
19    /// Total number of context removals.
20    pub context_remove_count: AtomicUsize,
21    /// Total blocks visited.
22    pub blocks_visited: AtomicUsize,
23    /// Total edges followed.
24    pub edges_followed: AtomicUsize,
25    /// Total execution time in microseconds.
26    pub total_execution_time_us: AtomicU64,
27    /// Number of errors encountered.
28    pub error_count: AtomicUsize,
29    /// Number of budget warnings.
30    pub budget_warnings: AtomicUsize,
31}
32
33impl SessionMetrics {
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    pub fn record_navigation(&self) {
39        self.navigation_count.fetch_add(1, Ordering::Relaxed);
40    }
41
42    pub fn record_expansion(&self, blocks_count: usize) {
43        self.expansion_count.fetch_add(1, Ordering::Relaxed);
44        self.blocks_visited
45            .fetch_add(blocks_count, Ordering::Relaxed);
46    }
47
48    pub fn record_search(&self) {
49        self.search_count.fetch_add(1, Ordering::Relaxed);
50    }
51
52    pub fn record_context_add(&self, count: usize) {
53        self.context_add_count.fetch_add(count, Ordering::Relaxed);
54    }
55
56    pub fn record_context_remove(&self) {
57        self.context_remove_count.fetch_add(1, Ordering::Relaxed);
58    }
59
60    /// Record a traversal operation (path finding, etc.).
61    pub fn record_traversal(&self) {
62        self.navigation_count.fetch_add(1, Ordering::Relaxed);
63    }
64
65    pub fn record_edges_followed(&self, count: usize) {
66        self.edges_followed.fetch_add(count, Ordering::Relaxed);
67    }
68
69    pub fn record_execution_time(&self, duration: Duration) {
70        self.total_execution_time_us
71            .fetch_add(duration.as_micros() as u64, Ordering::Relaxed);
72    }
73
74    pub fn record_error(&self) {
75        self.error_count.fetch_add(1, Ordering::Relaxed);
76    }
77
78    pub fn record_budget_warning(&self) {
79        self.budget_warnings.fetch_add(1, Ordering::Relaxed);
80    }
81
82    /// Get a snapshot of current metrics.
83    pub fn snapshot(&self) -> MetricsSnapshot {
84        MetricsSnapshot {
85            navigation_count: self.navigation_count.load(Ordering::Relaxed),
86            expansion_count: self.expansion_count.load(Ordering::Relaxed),
87            search_count: self.search_count.load(Ordering::Relaxed),
88            context_add_count: self.context_add_count.load(Ordering::Relaxed),
89            context_remove_count: self.context_remove_count.load(Ordering::Relaxed),
90            blocks_visited: self.blocks_visited.load(Ordering::Relaxed),
91            edges_followed: self.edges_followed.load(Ordering::Relaxed),
92            total_execution_time_us: self.total_execution_time_us.load(Ordering::Relaxed),
93            error_count: self.error_count.load(Ordering::Relaxed),
94            budget_warnings: self.budget_warnings.load(Ordering::Relaxed),
95            captured_at: Utc::now(),
96        }
97    }
98}
99
100/// Serializable snapshot of metrics.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct MetricsSnapshot {
103    pub navigation_count: usize,
104    pub expansion_count: usize,
105    pub search_count: usize,
106    pub context_add_count: usize,
107    pub context_remove_count: usize,
108    pub blocks_visited: usize,
109    pub edges_followed: usize,
110    pub total_execution_time_us: u64,
111    pub error_count: usize,
112    pub budget_warnings: usize,
113    pub captured_at: DateTime<Utc>,
114}
115
116impl MetricsSnapshot {
117    pub fn total_operations(&self) -> usize {
118        self.navigation_count
119            + self.expansion_count
120            + self.search_count
121            + self.context_add_count
122            + self.context_remove_count
123    }
124
125    pub fn total_execution_time(&self) -> Duration {
126        Duration::from_micros(self.total_execution_time_us)
127    }
128
129    pub fn average_operation_time(&self) -> Option<Duration> {
130        let total = self.total_operations();
131        if total == 0 {
132            None
133        } else {
134            Some(Duration::from_micros(
135                self.total_execution_time_us / total as u64,
136            ))
137        }
138    }
139}
140
141/// Metrics for a single operation.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct OperationMetrics {
144    /// Operation type.
145    pub operation: String,
146    /// Execution time.
147    pub duration: Duration,
148    /// Number of blocks processed.
149    pub blocks_processed: usize,
150    /// Whether the operation succeeded.
151    pub success: bool,
152    /// Timestamp.
153    pub timestamp: DateTime<Utc>,
154}
155
156impl OperationMetrics {
157    pub fn start(operation: &str) -> OperationMetricsBuilder {
158        OperationMetricsBuilder {
159            operation: operation.to_string(),
160            start: Instant::now(),
161            blocks_processed: 0,
162        }
163    }
164}
165
166/// Builder for operation metrics with timing.
167pub struct OperationMetricsBuilder {
168    operation: String,
169    start: Instant,
170    blocks_processed: usize,
171}
172
173impl OperationMetricsBuilder {
174    pub fn blocks(mut self, count: usize) -> Self {
175        self.blocks_processed = count;
176        self
177    }
178
179    pub fn finish(self, success: bool) -> OperationMetrics {
180        OperationMetrics {
181            operation: self.operation,
182            duration: self.start.elapsed(),
183            blocks_processed: self.blocks_processed,
184            success,
185            timestamp: Utc::now(),
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_session_metrics() {
196        let metrics = SessionMetrics::new();
197
198        metrics.record_navigation();
199        metrics.record_expansion(5);
200        metrics.record_search();
201
202        let snapshot = metrics.snapshot();
203        assert_eq!(snapshot.navigation_count, 1);
204        assert_eq!(snapshot.expansion_count, 1);
205        assert_eq!(snapshot.search_count, 1);
206        assert_eq!(snapshot.blocks_visited, 5);
207    }
208
209    #[test]
210    fn test_operation_metrics() {
211        let op = OperationMetrics::start("navigate").blocks(3).finish(true);
212
213        assert_eq!(op.operation, "navigate");
214        assert!(op.success);
215        assert_eq!(op.blocks_processed, 3);
216    }
217}