Skip to main content

llm_agent_runtime/
metrics.rs

1//! # Module: Metrics
2//!
3//! Runtime observability counters for `AgentRuntime`.
4//! All counters use atomics for lock-free, thread-safe increment/read.
5
6use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
7use std::sync::Arc;
8
9/// Shared runtime metrics. Clone the `Arc` to share across threads.
10#[derive(Debug, Default)]
11pub struct RuntimeMetrics {
12    /// Number of agent sessions currently in progress.
13    pub active_sessions: AtomicUsize,
14    /// Total number of sessions started since the runtime was created.
15    pub total_sessions: AtomicU64,
16    /// Total number of ReAct steps executed across all sessions.
17    pub total_steps: AtomicU64,
18    /// Total number of tool calls dispatched.
19    pub total_tool_calls: AtomicU64,
20    /// Total number of tool calls that returned an error observation.
21    pub failed_tool_calls: AtomicU64,
22    /// Total number of requests shed due to backpressure.
23    pub backpressure_shed_count: AtomicU64,
24    /// Total number of memory recall operations.
25    pub memory_recall_count: AtomicU64,
26}
27
28impl RuntimeMetrics {
29    /// Allocate a new `RuntimeMetrics` instance wrapped in an `Arc`.
30    pub fn new() -> Arc<Self> {
31        Arc::new(Self::default())
32    }
33
34    /// Return the number of agent sessions currently in progress.
35    pub fn active_sessions(&self) -> usize {
36        self.active_sessions.load(Ordering::Relaxed)
37    }
38
39    /// Return the total number of sessions started since the runtime was created.
40    pub fn total_sessions(&self) -> u64 {
41        self.total_sessions.load(Ordering::Relaxed)
42    }
43
44    /// Return the total number of ReAct steps executed across all sessions.
45    pub fn total_steps(&self) -> u64 {
46        self.total_steps.load(Ordering::Relaxed)
47    }
48
49    /// Return the total number of tool calls dispatched.
50    pub fn total_tool_calls(&self) -> u64 {
51        self.total_tool_calls.load(Ordering::Relaxed)
52    }
53
54    /// Return the total number of tool calls that returned an error observation.
55    pub fn failed_tool_calls(&self) -> u64 {
56        self.failed_tool_calls.load(Ordering::Relaxed)
57    }
58
59    /// Return the total number of requests shed due to backpressure.
60    pub fn backpressure_shed_count(&self) -> u64 {
61        self.backpressure_shed_count.load(Ordering::Relaxed)
62    }
63
64    /// Return the total number of memory recall operations performed.
65    pub fn memory_recall_count(&self) -> u64 {
66        self.memory_recall_count.load(Ordering::Relaxed)
67    }
68
69    /// Reset all counters to zero.
70    ///
71    /// Intended for testing. In production, counters are monotonically increasing.
72    pub fn reset(&self) {
73        self.active_sessions.store(0, Ordering::Relaxed);
74        self.total_sessions.store(0, Ordering::Relaxed);
75        self.total_steps.store(0, Ordering::Relaxed);
76        self.total_tool_calls.store(0, Ordering::Relaxed);
77        self.failed_tool_calls.store(0, Ordering::Relaxed);
78        self.backpressure_shed_count.store(0, Ordering::Relaxed);
79        self.memory_recall_count.store(0, Ordering::Relaxed);
80    }
81
82    /// Capture a snapshot of all counters as plain integers.
83    ///
84    /// Returns `(active_sessions, total_sessions, total_steps,
85    ///           total_tool_calls, failed_tool_calls,
86    ///           backpressure_shed_count, memory_recall_count)`.
87    pub fn to_snapshot(&self) -> (usize, u64, u64, u64, u64, u64, u64) {
88        (
89            self.active_sessions.load(Ordering::Relaxed),
90            self.total_sessions.load(Ordering::Relaxed),
91            self.total_steps.load(Ordering::Relaxed),
92            self.total_tool_calls.load(Ordering::Relaxed),
93            self.failed_tool_calls.load(Ordering::Relaxed),
94            self.backpressure_shed_count.load(Ordering::Relaxed),
95            self.memory_recall_count.load(Ordering::Relaxed),
96        )
97    }
98}
99
100// ── Tests ─────────────────────────────────────────────────────────────────────
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_metrics_new_returns_arc_with_zero_counters() {
108        let m = RuntimeMetrics::new();
109        assert_eq!(m.active_sessions(), 0);
110        assert_eq!(m.total_sessions(), 0);
111        assert_eq!(m.total_steps(), 0);
112        assert_eq!(m.total_tool_calls(), 0);
113        assert_eq!(m.failed_tool_calls(), 0);
114        assert_eq!(m.backpressure_shed_count(), 0);
115        assert_eq!(m.memory_recall_count(), 0);
116    }
117
118    #[test]
119    fn test_active_sessions_increments_and_decrements() {
120        let m = RuntimeMetrics::new();
121        m.active_sessions.fetch_add(1, Ordering::Relaxed);
122        assert_eq!(m.active_sessions(), 1);
123        m.active_sessions.fetch_sub(1, Ordering::Relaxed);
124        assert_eq!(m.active_sessions(), 0);
125    }
126
127    #[test]
128    fn test_total_sessions_increments() {
129        let m = RuntimeMetrics::new();
130        m.total_sessions.fetch_add(1, Ordering::Relaxed);
131        m.total_sessions.fetch_add(1, Ordering::Relaxed);
132        assert_eq!(m.total_sessions(), 2);
133    }
134
135    #[test]
136    fn test_total_steps_increments() {
137        let m = RuntimeMetrics::new();
138        m.total_steps.fetch_add(5, Ordering::Relaxed);
139        assert_eq!(m.total_steps(), 5);
140    }
141
142    #[test]
143    fn test_total_tool_calls_increments() {
144        let m = RuntimeMetrics::new();
145        m.total_tool_calls.fetch_add(3, Ordering::Relaxed);
146        assert_eq!(m.total_tool_calls(), 3);
147    }
148
149    #[test]
150    fn test_failed_tool_calls_increments() {
151        let m = RuntimeMetrics::new();
152        m.failed_tool_calls.fetch_add(2, Ordering::Relaxed);
153        assert_eq!(m.failed_tool_calls(), 2);
154    }
155
156    #[test]
157    fn test_backpressure_shed_count_increments() {
158        let m = RuntimeMetrics::new();
159        m.backpressure_shed_count.fetch_add(7, Ordering::Relaxed);
160        assert_eq!(m.backpressure_shed_count(), 7);
161    }
162
163    #[test]
164    fn test_memory_recall_count_increments() {
165        let m = RuntimeMetrics::new();
166        m.memory_recall_count.fetch_add(4, Ordering::Relaxed);
167        assert_eq!(m.memory_recall_count(), 4);
168    }
169
170    #[test]
171    fn test_reset_zeroes_all_counters() {
172        let m = RuntimeMetrics::new();
173        m.active_sessions.store(3, Ordering::Relaxed);
174        m.total_sessions.store(10, Ordering::Relaxed);
175        m.total_steps.store(50, Ordering::Relaxed);
176        m.total_tool_calls.store(20, Ordering::Relaxed);
177        m.failed_tool_calls.store(2, Ordering::Relaxed);
178        m.backpressure_shed_count.store(1, Ordering::Relaxed);
179        m.memory_recall_count.store(8, Ordering::Relaxed);
180
181        m.reset();
182
183        assert_eq!(m.active_sessions(), 0);
184        assert_eq!(m.total_sessions(), 0);
185        assert_eq!(m.total_steps(), 0);
186        assert_eq!(m.total_tool_calls(), 0);
187        assert_eq!(m.failed_tool_calls(), 0);
188        assert_eq!(m.backpressure_shed_count(), 0);
189        assert_eq!(m.memory_recall_count(), 0);
190    }
191
192    #[test]
193    fn test_to_snapshot_captures_correct_values() {
194        let m = RuntimeMetrics::new();
195        m.active_sessions.store(1, Ordering::Relaxed);
196        m.total_sessions.store(2, Ordering::Relaxed);
197        m.total_steps.store(3, Ordering::Relaxed);
198        m.total_tool_calls.store(4, Ordering::Relaxed);
199        m.failed_tool_calls.store(5, Ordering::Relaxed);
200        m.backpressure_shed_count.store(6, Ordering::Relaxed);
201        m.memory_recall_count.store(7, Ordering::Relaxed);
202
203        let snap = m.to_snapshot();
204        assert_eq!(snap, (1, 2, 3, 4, 5, 6, 7));
205    }
206
207    #[test]
208    fn test_metrics_is_send_sync() {
209        fn assert_send_sync<T: Send + Sync>() {}
210        assert_send_sync::<RuntimeMetrics>();
211    }
212
213    #[test]
214    fn test_multiple_increments_are_cumulative() {
215        let m = RuntimeMetrics::new();
216        for _ in 0..100 {
217            m.total_sessions.fetch_add(1, Ordering::Relaxed);
218        }
219        assert_eq!(m.total_sessions(), 100);
220    }
221
222    #[test]
223    fn test_arc_clone_shares_state() {
224        let m = RuntimeMetrics::new();
225        let m2 = Arc::clone(&m);
226        m.total_sessions.fetch_add(1, Ordering::Relaxed);
227        assert_eq!(m2.total_sessions(), 1);
228    }
229}