Skip to main content

aivpn_server/
metrics.rs

1//! Prometheus Metrics (Phase 5)
2//!
3//! Implements monitoring and metrics export for AIVPN
4//!
5//! Features:
6//! - Session count and state
7//! - Packet processing rates
8//! - Bandwidth usage
9//! - Mask rotation events
10//! - Neural module health
11//! - DPI attack detection
12
13#[cfg(feature = "metrics")]
14use prometheus::{Counter, Encoder, Gauge, Histogram, HistogramOpts, Opts, Registry, TextEncoder};
15#[cfg(feature = "metrics")]
16use std::sync::Arc;
17#[cfg(feature = "metrics")]
18use tracing::warn;
19
20/// Metrics collector
21pub struct MetricsCollector {
22    #[cfg(feature = "metrics")]
23    registry: Registry,
24
25    #[cfg(feature = "metrics")]
26    sessions_total: Gauge,
27
28    #[cfg(feature = "metrics")]
29    sessions_active: Gauge,
30
31    #[cfg(feature = "metrics")]
32    packets_received: Counter,
33
34    #[cfg(feature = "metrics")]
35    packets_sent: Counter,
36
37    #[cfg(feature = "metrics")]
38    bytes_received: Counter,
39
40    #[cfg(feature = "metrics")]
41    bytes_sent: Counter,
42
43    #[cfg(feature = "metrics")]
44    packet_processing_time: Histogram,
45
46    #[cfg(feature = "metrics")]
47    tag_validation_time: Histogram,
48
49    #[cfg(feature = "metrics")]
50    mask_rotations: Counter,
51
52    #[cfg(feature = "metrics")]
53    key_rotations: Counter,
54
55    #[cfg(feature = "metrics")]
56    neural_checks_total: Counter,
57
58    #[cfg(feature = "metrics")]
59    neural_checks_failed: Counter,
60
61    #[cfg(feature = "metrics")]
62    dpi_attacks_detected: Counter,
63}
64
65impl MetricsCollector {
66    /// Create new metrics collector
67    pub fn new() -> Self {
68        #[cfg(feature = "metrics")]
69        {
70            let registry = Registry::new();
71
72            // Session metrics
73            let sessions_total = Gauge::with_opts(Opts::new(
74                "aivpn_sessions_total",
75                "Total number of sessions",
76            ))
77            .unwrap();
78            registry.register(Box::new(sessions_total.clone())).unwrap();
79
80            let sessions_active = Gauge::with_opts(Opts::new(
81                "aivpn_sessions_active",
82                "Number of active sessions",
83            ))
84            .unwrap();
85            registry
86                .register(Box::new(sessions_active.clone()))
87                .unwrap();
88
89            // Packet metrics
90            let packets_received = Counter::with_opts(Opts::new(
91                "aivpn_packets_received_total",
92                "Total packets received",
93            ))
94            .unwrap();
95            registry
96                .register(Box::new(packets_received.clone()))
97                .unwrap();
98
99            let packets_sent =
100                Counter::with_opts(Opts::new("aivpn_packets_sent_total", "Total packets sent"))
101                    .unwrap();
102            registry.register(Box::new(packets_sent.clone())).unwrap();
103
104            // Bandwidth metrics
105            let bytes_received = Counter::with_opts(Opts::new(
106                "aivpn_bytes_received_total",
107                "Total bytes received",
108            ))
109            .unwrap();
110            registry.register(Box::new(bytes_received.clone())).unwrap();
111
112            let bytes_sent =
113                Counter::with_opts(Opts::new("aivpn_bytes_sent_total", "Total bytes sent"))
114                    .unwrap();
115            registry.register(Box::new(bytes_sent.clone())).unwrap();
116
117            // Performance metrics.
118            // HistogramOpts is required here — Histogram::with_opts does not accept plain Opts.
119            let packet_processing_time = Histogram::with_opts(HistogramOpts::new(
120                "aivpn_packet_processing_seconds",
121                "Packet processing time",
122            ))
123            .unwrap();
124            registry
125                .register(Box::new(packet_processing_time.clone()))
126                .unwrap();
127
128            let tag_validation_time = Histogram::with_opts(HistogramOpts::new(
129                "aivpn_tag_validation_seconds",
130                "Tag validation time",
131            ))
132            .unwrap();
133            registry
134                .register(Box::new(tag_validation_time.clone()))
135                .unwrap();
136
137            // Rotation metrics
138            let mask_rotations = Counter::with_opts(Opts::new(
139                "aivpn_mask_rotations_total",
140                "Total mask rotations",
141            ))
142            .unwrap();
143            registry.register(Box::new(mask_rotations.clone())).unwrap();
144
145            let key_rotations = Counter::with_opts(Opts::new(
146                "aivpn_key_rotations_total",
147                "Total key rotations",
148            ))
149            .unwrap();
150            registry.register(Box::new(key_rotations.clone())).unwrap();
151
152            // Neural module metrics
153            let neural_checks_total = Counter::with_opts(Opts::new(
154                "aivpn_neural_checks_total",
155                "Total neural resonance checks",
156            ))
157            .unwrap();
158            registry
159                .register(Box::new(neural_checks_total.clone()))
160                .unwrap();
161
162            let neural_checks_failed = Counter::with_opts(Opts::new(
163                "aivpn_neural_checks_failed_total",
164                "Failed neural resonance checks",
165            ))
166            .unwrap();
167            registry
168                .register(Box::new(neural_checks_failed.clone()))
169                .unwrap();
170
171            // Security metrics
172            let dpi_attacks_detected = Counter::with_opts(Opts::new(
173                "aivpn_dpi_attacks_detected_total",
174                "DPI attacks detected",
175            ))
176            .unwrap();
177            registry
178                .register(Box::new(dpi_attacks_detected.clone()))
179                .unwrap();
180
181            Self {
182                registry,
183                sessions_total,
184                sessions_active,
185                packets_received,
186                packets_sent,
187                bytes_received,
188                bytes_sent,
189                packet_processing_time,
190                tag_validation_time,
191                mask_rotations,
192                key_rotations,
193                neural_checks_total,
194                neural_checks_failed,
195                dpi_attacks_detected,
196            }
197        }
198
199        #[cfg(not(feature = "metrics"))]
200        Self {}
201    }
202
203    /// Update session count
204    pub fn update_session_count(&self, total: usize, active: usize) {
205        #[cfg(feature = "metrics")]
206        {
207            // Gauge::set takes f64
208            self.sessions_total.set(total as f64);
209            self.sessions_active.set(active as f64);
210        }
211
212        #[cfg(not(feature = "metrics"))]
213        let _ = (total, active);
214    }
215
216    /// Record packet received
217    pub fn record_packet_received(&self, bytes: usize) {
218        #[cfg(feature = "metrics")]
219        {
220            self.packets_received.inc();
221            // Counter::inc_by takes f64
222            self.bytes_received.inc_by(bytes as f64);
223        }
224
225        #[cfg(not(feature = "metrics"))]
226        let _ = bytes;
227    }
228
229    /// Record packet sent
230    pub fn record_packet_sent(&self, bytes: usize) {
231        #[cfg(feature = "metrics")]
232        {
233            self.packets_sent.inc();
234            // Counter::inc_by takes f64
235            self.bytes_sent.inc_by(bytes as f64);
236        }
237
238        #[cfg(not(feature = "metrics"))]
239        let _ = bytes;
240    }
241
242    /// Record packet processing time
243    pub fn record_processing_time(&self, _seconds: f64) {
244        #[cfg(feature = "metrics")]
245        {
246            self.packet_processing_time.observe(_seconds);
247        }
248    }
249
250    /// Record tag validation time
251    pub fn record_tag_validation_time(&self, _seconds: f64) {
252        #[cfg(feature = "metrics")]
253        {
254            self.tag_validation_time.observe(_seconds);
255        }
256    }
257
258    /// Record mask rotation
259    pub fn record_mask_rotation(&self) {
260        #[cfg(feature = "metrics")]
261        {
262            self.mask_rotations.inc();
263        }
264    }
265
266    /// Record key rotation
267    pub fn record_key_rotation(&self) {
268        #[cfg(feature = "metrics")]
269        {
270            self.key_rotations.inc();
271        }
272    }
273
274    /// Record neural check
275    pub fn record_neural_check(&self, _failed: bool) {
276        #[cfg(feature = "metrics")]
277        {
278            self.neural_checks_total.inc();
279            if _failed {
280                self.neural_checks_failed.inc();
281            }
282        }
283    }
284
285    /// Record DPI attack detection
286    pub fn record_dpi_attack(&self) {
287        #[cfg(feature = "metrics")]
288        {
289            self.dpi_attacks_detected.inc();
290            warn!("DPI attack detected!");
291        }
292    }
293
294    /// Export metrics in Prometheus text exposition format (Content-Type: text/plain; version=0.0.4)
295    pub fn gather(&self) -> String {
296        #[cfg(feature = "metrics")]
297        {
298            let encoder = TextEncoder::new();
299            let metric_families = self.registry.gather();
300            // encode() writes to impl Write; use a Vec<u8> buffer then convert to String.
301            let mut buf = Vec::new();
302            encoder
303                .encode(&metric_families, &mut buf)
304                .unwrap_or_default();
305            String::from_utf8(buf).unwrap_or_default()
306        }
307
308        #[cfg(not(feature = "metrics"))]
309        {
310            String::new()
311        }
312    }
313}
314
315impl Default for MetricsCollector {
316    fn default() -> Self {
317        Self::new()
318    }
319}
320
321/// Returns the current Prometheus metrics in text exposition format.
322/// The caller is responsible for serving this as HTTP with
323/// Content-Type: text/plain; version=0.0.4
324#[cfg(feature = "metrics")]
325pub async fn metrics_handler(collector: Arc<MetricsCollector>) -> String {
326    collector.gather()
327}