oxur_repl/metrics/
server.rs

1//! Server-side metrics for connection and session tracking
2//!
3//! Provides the [`ServerMetrics`] struct for recording server-level metrics
4//! with both local tracking (for `(stats)` display) and `metrics` crate facade
5//! integration (for external monitoring).
6
7use metrics::{counter, gauge};
8use std::sync::atomic::{AtomicU64, Ordering};
9
10/// Server-side metrics recorder.
11///
12/// Records metrics for:
13/// - Connection lifecycle (accepted, closed, active count)
14/// - Session lifecycle (created, closed, active count)
15/// - Request/response counts by type and status
16///
17/// Maintains local state for `(stats server)` display while also emitting
18/// to the `metrics` crate facade for external monitoring.
19///
20/// # Usage
21///
22/// ```
23/// use oxur_repl::metrics::ServerMetrics;
24///
25/// let metrics = ServerMetrics::new();
26///
27/// // On new connection
28/// metrics.connection_accepted();
29///
30/// // On connection close
31/// metrics.connection_closed();
32///
33/// // On session operations
34/// metrics.session_created();
35/// metrics.request_received("eval");
36/// metrics.response_sent("success");
37/// metrics.session_closed();
38///
39/// // Query local state for display
40/// let snapshot = metrics.snapshot();
41/// println!("Total connections: {}", snapshot.connections_total);
42/// ```
43#[derive(Debug, Default)]
44pub struct ServerMetrics {
45    // Connection tracking
46    connections_total: AtomicU64,
47    connections_active: AtomicU64,
48
49    // Session tracking
50    sessions_total: AtomicU64,
51    sessions_active: AtomicU64,
52
53    // Request/response tracking (simplified - detailed by label in metrics facade)
54    requests_total: AtomicU64,
55    responses_total: AtomicU64,
56    responses_success: AtomicU64,
57    responses_error: AtomicU64,
58}
59
60/// Snapshot of server metrics for display
61#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
62pub struct ServerMetricsSnapshot {
63    pub connections_total: u64,
64    pub connections_active: u64,
65    pub sessions_total: u64,
66    pub sessions_active: u64,
67    pub requests_total: u64,
68    pub responses_total: u64,
69    pub responses_success: u64,
70    pub responses_error: u64,
71}
72
73impl ServerMetrics {
74    /// Create a new ServerMetrics instance.
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Record a new connection being accepted.
80    ///
81    /// Increments `repl.server.connections_total` counter and
82    /// `repl.server.connections_active` gauge.
83    pub fn connection_accepted(&self) {
84        self.connections_total.fetch_add(1, Ordering::Relaxed);
85        self.connections_active.fetch_add(1, Ordering::Relaxed);
86
87        counter!("repl.server.connections_total").increment(1);
88        gauge!("repl.server.connections_active")
89            .set(self.connections_active.load(Ordering::Relaxed) as f64);
90    }
91
92    /// Record a connection being closed.
93    ///
94    /// Decrements `repl.server.connections_active` gauge.
95    pub fn connection_closed(&self) {
96        self.connections_active.fetch_sub(1, Ordering::Relaxed);
97
98        gauge!("repl.server.connections_active")
99            .set(self.connections_active.load(Ordering::Relaxed) as f64);
100    }
101
102    /// Record a new session being created.
103    ///
104    /// Increments `repl.server.sessions_total` counter and
105    /// `repl.server.sessions_active` gauge.
106    pub fn session_created(&self) {
107        self.sessions_total.fetch_add(1, Ordering::Relaxed);
108        self.sessions_active.fetch_add(1, Ordering::Relaxed);
109
110        counter!("repl.server.sessions_total").increment(1);
111        gauge!("repl.server.sessions_active")
112            .set(self.sessions_active.load(Ordering::Relaxed) as f64);
113    }
114
115    /// Record a session being closed.
116    ///
117    /// Decrements `repl.server.sessions_active` gauge.
118    pub fn session_closed(&self) {
119        self.sessions_active.fetch_sub(1, Ordering::Relaxed);
120
121        gauge!("repl.server.sessions_active")
122            .set(self.sessions_active.load(Ordering::Relaxed) as f64);
123    }
124
125    /// Record a request being received.
126    ///
127    /// Increments `repl.server.requests_total` counter with operation label.
128    ///
129    /// # Arguments
130    ///
131    /// * `operation` - The operation type (e.g., "eval", "create_session", "close")
132    pub fn request_received(&self, operation: &'static str) {
133        self.requests_total.fetch_add(1, Ordering::Relaxed);
134
135        counter!("repl.server.requests_total", "operation" => operation).increment(1);
136    }
137
138    /// Record a response being sent.
139    ///
140    /// Increments `repl.server.responses_total` counter with status label.
141    ///
142    /// # Arguments
143    ///
144    /// * `status` - The response status (e.g., "success", "error")
145    pub fn response_sent(&self, status: &'static str) {
146        self.responses_total.fetch_add(1, Ordering::Relaxed);
147
148        match status {
149            "success" => {
150                self.responses_success.fetch_add(1, Ordering::Relaxed);
151            }
152            "error" => {
153                self.responses_error.fetch_add(1, Ordering::Relaxed);
154            }
155            _ => {}
156        }
157
158        counter!("repl.server.responses_total", "status" => status).increment(1);
159    }
160
161    /// Get a snapshot of current metrics for display.
162    ///
163    /// Returns a point-in-time snapshot of all server metrics.
164    pub fn snapshot(&self) -> ServerMetricsSnapshot {
165        ServerMetricsSnapshot {
166            connections_total: self.connections_total.load(Ordering::Relaxed),
167            connections_active: self.connections_active.load(Ordering::Relaxed),
168            sessions_total: self.sessions_total.load(Ordering::Relaxed),
169            sessions_active: self.sessions_active.load(Ordering::Relaxed),
170            requests_total: self.requests_total.load(Ordering::Relaxed),
171            responses_total: self.responses_total.load(Ordering::Relaxed),
172            responses_success: self.responses_success.load(Ordering::Relaxed),
173            responses_error: self.responses_error.load(Ordering::Relaxed),
174        }
175    }
176
177    /// Get total connections count.
178    pub fn connections_total(&self) -> u64 {
179        self.connections_total.load(Ordering::Relaxed)
180    }
181
182    /// Get active connections count.
183    pub fn connections_active(&self) -> u64 {
184        self.connections_active.load(Ordering::Relaxed)
185    }
186
187    /// Get total sessions count.
188    pub fn sessions_total(&self) -> u64 {
189        self.sessions_total.load(Ordering::Relaxed)
190    }
191
192    /// Get active sessions count.
193    pub fn sessions_active(&self) -> u64 {
194        self.sessions_active.load(Ordering::Relaxed)
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_server_metrics_creation() {
204        let metrics = ServerMetrics::new();
205
206        // Should start at zero
207        assert_eq!(metrics.connections_total(), 0);
208        assert_eq!(metrics.connections_active(), 0);
209        assert_eq!(metrics.sessions_total(), 0);
210        assert_eq!(metrics.sessions_active(), 0);
211    }
212
213    #[test]
214    fn test_connection_tracking() {
215        let metrics = ServerMetrics::new();
216
217        metrics.connection_accepted();
218        assert_eq!(metrics.connections_total(), 1);
219        assert_eq!(metrics.connections_active(), 1);
220
221        metrics.connection_accepted();
222        assert_eq!(metrics.connections_total(), 2);
223        assert_eq!(metrics.connections_active(), 2);
224
225        metrics.connection_closed();
226        assert_eq!(metrics.connections_total(), 2);
227        assert_eq!(metrics.connections_active(), 1);
228    }
229
230    #[test]
231    fn test_session_tracking() {
232        let metrics = ServerMetrics::new();
233
234        metrics.session_created();
235        assert_eq!(metrics.sessions_total(), 1);
236        assert_eq!(metrics.sessions_active(), 1);
237
238        metrics.session_closed();
239        assert_eq!(metrics.sessions_total(), 1);
240        assert_eq!(metrics.sessions_active(), 0);
241    }
242
243    #[test]
244    fn test_request_response_tracking() {
245        let metrics = ServerMetrics::new();
246
247        metrics.request_received("eval");
248        metrics.request_received("eval");
249        metrics.request_received("close");
250
251        let snapshot = metrics.snapshot();
252        assert_eq!(snapshot.requests_total, 3);
253
254        metrics.response_sent("success");
255        metrics.response_sent("success");
256        metrics.response_sent("error");
257
258        let snapshot = metrics.snapshot();
259        assert_eq!(snapshot.responses_total, 3);
260        assert_eq!(snapshot.responses_success, 2);
261        assert_eq!(snapshot.responses_error, 1);
262    }
263
264    #[test]
265    fn test_snapshot() {
266        let metrics = ServerMetrics::new();
267
268        metrics.connection_accepted();
269        metrics.session_created();
270        metrics.request_received("eval");
271        metrics.response_sent("success");
272
273        let snapshot = metrics.snapshot();
274
275        assert_eq!(snapshot.connections_total, 1);
276        assert_eq!(snapshot.connections_active, 1);
277        assert_eq!(snapshot.sessions_total, 1);
278        assert_eq!(snapshot.sessions_active, 1);
279        assert_eq!(snapshot.requests_total, 1);
280        assert_eq!(snapshot.responses_total, 1);
281        assert_eq!(snapshot.responses_success, 1);
282        assert_eq!(snapshot.responses_error, 0);
283    }
284}