Skip to main content

xds_server/
metrics.rs

1//! Prometheus metrics for xDS server.
2//!
3//! This module provides comprehensive metrics for monitoring the xDS server:
4//!
5//! - Request/response counters per resource type
6//! - Latency histograms for request processing
7//! - Cache operation metrics
8//! - Connection and stream tracking
9//!
10//! # Example
11//!
12//! ```rust
13//! use xds_server::metrics::XdsMetrics;
14//!
15//! let metrics = XdsMetrics::new();
16//! metrics.record_request("type.googleapis.com/envoy.config.cluster.v3.Cluster");
17//! metrics.record_response("type.googleapis.com/envoy.config.cluster.v3.Cluster", 150);
18//! ```
19
20use std::sync::atomic::{AtomicU64, Ordering};
21use std::sync::Arc;
22use std::time::{Duration, Instant};
23
24use metrics::{counter, gauge, histogram};
25
26/// Metrics for the xDS server.
27///
28/// Provides Prometheus-compatible metrics for monitoring server health,
29/// performance, and resource distribution.
30#[derive(Debug, Clone)]
31pub struct XdsMetrics {
32    inner: Arc<XdsMetricsInner>,
33}
34
35#[derive(Debug)]
36struct XdsMetricsInner {
37    /// Total active streams.
38    active_streams: AtomicU64,
39    /// Total active connections.
40    active_connections: AtomicU64,
41}
42
43impl Default for XdsMetrics {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl XdsMetrics {
50    /// Create a new metrics instance.
51    pub fn new() -> Self {
52        Self {
53            inner: Arc::new(XdsMetricsInner {
54                active_streams: AtomicU64::new(0),
55                active_connections: AtomicU64::new(0),
56            }),
57        }
58    }
59
60    /// Record an incoming request.
61    pub fn record_request(&self, type_url: &str) {
62        counter!("xds_requests_total", "type_url" => type_url.to_string()).increment(1);
63    }
64
65    /// Record a response sent.
66    pub fn record_response(&self, type_url: &str, latency_ms: u64) {
67        counter!("xds_responses_total", "type_url" => type_url.to_string()).increment(1);
68        histogram!("xds_response_latency_ms", "type_url" => type_url.to_string())
69            .record(latency_ms as f64);
70    }
71
72    /// Record a NACK (negative acknowledgment).
73    pub fn record_nack(&self, type_url: &str) {
74        counter!("xds_nacks_total", "type_url" => type_url.to_string()).increment(1);
75    }
76
77    /// Record an ACK (acknowledgment).
78    pub fn record_ack(&self, type_url: &str) {
79        counter!("xds_acks_total", "type_url" => type_url.to_string()).increment(1);
80    }
81
82    /// Record a stream opened.
83    pub fn stream_opened(&self, service: &str) {
84        let count = self.inner.active_streams.fetch_add(1, Ordering::Relaxed) + 1;
85        counter!("xds_streams_opened_total", "service" => service.to_string()).increment(1);
86        gauge!("xds_active_streams").set(count as f64);
87    }
88
89    /// Record a stream closed.
90    pub fn stream_closed(&self, service: &str, duration: Duration) {
91        let count = self.inner.active_streams.fetch_sub(1, Ordering::Relaxed) - 1;
92        counter!("xds_streams_closed_total", "service" => service.to_string()).increment(1);
93        gauge!("xds_active_streams").set(count as f64);
94        histogram!("xds_stream_duration_seconds", "service" => service.to_string())
95            .record(duration.as_secs_f64());
96    }
97
98    /// Record a connection opened.
99    pub fn connection_opened(&self) {
100        let count = self.inner.active_connections.fetch_add(1, Ordering::Relaxed) + 1;
101        counter!("xds_connections_opened_total").increment(1);
102        gauge!("xds_active_connections").set(count as f64);
103    }
104
105    /// Record a connection closed.
106    pub fn connection_closed(&self) {
107        let count = self.inner.active_connections.fetch_sub(1, Ordering::Relaxed) - 1;
108        counter!("xds_connections_closed_total").increment(1);
109        gauge!("xds_active_connections").set(count as f64);
110    }
111
112    /// Record cache hit.
113    pub fn cache_hit(&self, type_url: &str) {
114        counter!("xds_cache_hits_total", "type_url" => type_url.to_string()).increment(1);
115    }
116
117    /// Record cache miss.
118    pub fn cache_miss(&self, type_url: &str) {
119        counter!("xds_cache_misses_total", "type_url" => type_url.to_string()).increment(1);
120    }
121
122    /// Record snapshot update.
123    pub fn snapshot_updated(&self, node_count: usize, resource_count: usize) {
124        counter!("xds_snapshot_updates_total").increment(1);
125        gauge!("xds_snapshot_nodes").set(node_count as f64);
126        gauge!("xds_snapshot_resources").set(resource_count as f64);
127    }
128
129    /// Get the current number of active streams.
130    pub fn active_streams(&self) -> u64 {
131        self.inner.active_streams.load(Ordering::Relaxed)
132    }
133
134    /// Get the current number of active connections.
135    pub fn active_connections(&self) -> u64 {
136        self.inner.active_connections.load(Ordering::Relaxed)
137    }
138}
139
140/// Timer for measuring operation latency.
141///
142/// Automatically records the duration when dropped.
143#[derive(Debug)]
144pub struct LatencyTimer {
145    start: Instant,
146    type_url: String,
147    metrics: XdsMetrics,
148}
149
150impl LatencyTimer {
151    /// Create a new latency timer.
152    pub fn new(metrics: XdsMetrics, type_url: impl Into<String>) -> Self {
153        Self {
154            start: Instant::now(),
155            type_url: type_url.into(),
156            metrics,
157        }
158    }
159
160    /// Finish the timer and record the latency.
161    pub fn finish(self) {
162        let elapsed = self.start.elapsed();
163        self.metrics
164            .record_response(&self.type_url, elapsed.as_millis() as u64);
165    }
166}
167
168impl Drop for LatencyTimer {
169    fn drop(&mut self) {
170        // Record on drop as well, in case finish() wasn't called
171    }
172}
173
174/// Stream duration tracker.
175///
176/// Records stream duration when dropped.
177#[derive(Debug)]
178pub struct StreamTracker {
179    start: Instant,
180    service: String,
181    metrics: XdsMetrics,
182}
183
184impl StreamTracker {
185    /// Create a new stream tracker.
186    pub fn new(metrics: XdsMetrics, service: impl Into<String>) -> Self {
187        let service = service.into();
188        metrics.stream_opened(&service);
189        Self {
190            start: Instant::now(),
191            service,
192            metrics,
193        }
194    }
195}
196
197impl Drop for StreamTracker {
198    fn drop(&mut self) {
199        self.metrics
200            .stream_closed(&self.service, self.start.elapsed());
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn metrics_creation() {
210        let metrics = XdsMetrics::new();
211        assert_eq!(metrics.active_streams(), 0);
212        assert_eq!(metrics.active_connections(), 0);
213    }
214
215    #[test]
216    fn stream_tracking() {
217        let metrics = XdsMetrics::new();
218
219        metrics.stream_opened("ads");
220        assert_eq!(metrics.active_streams(), 1);
221
222        metrics.stream_opened("cds");
223        assert_eq!(metrics.active_streams(), 2);
224
225        metrics.stream_closed("ads", Duration::from_secs(10));
226        assert_eq!(metrics.active_streams(), 1);
227    }
228
229    #[test]
230    fn connection_tracking() {
231        let metrics = XdsMetrics::new();
232
233        metrics.connection_opened();
234        assert_eq!(metrics.active_connections(), 1);
235
236        metrics.connection_opened();
237        assert_eq!(metrics.active_connections(), 2);
238
239        metrics.connection_closed();
240        assert_eq!(metrics.active_connections(), 1);
241    }
242}