1use prometheus::{
21 Registry, Counter, Gauge, Histogram, HistogramOpts, Opts,
22 TextEncoder, Encoder,
23};
24use crate::error::VCLError;
25use crate::metrics::VCLMetrics;
26use crate::tunnel::TunnelStats;
27use tracing::debug;
28
29pub struct VCLPrometheusExporter {
34 registry: Registry,
35
36 bytes_sent: Counter,
38 bytes_received: Counter,
39 packets_sent: Counter,
40 packets_received: Counter,
41 packets_retransmitted: Counter,
42 packets_dropped: Counter,
43
44 connections_active: Gauge,
46 reconnect_count: Counter,
47 handshakes_total: Counter,
48 key_rotations_total: Counter,
49
50 loss_rate: Gauge,
52 rtt_seconds: Gauge,
53 cwnd_packets: Gauge,
54 obfuscation_overhead: Gauge,
55 mtu_bytes: Gauge,
56
57 dns_queries_total: Counter,
59 dns_blocked_total: Counter,
60 dns_cache_hits: Counter,
61
62 fragments_sent: Counter,
64 fragments_reassembled: Counter,
65
66 rtt_histogram: Histogram,
68
69 tunnel_state: Gauge,
72}
73
74impl VCLPrometheusExporter {
75 pub fn new() -> Result<Self, VCLError> {
77 let registry = Registry::new();
78
79 macro_rules! counter {
80 ($name:expr, $help:expr) => {{
81 let c = Counter::with_opts(Opts::new($name, $help))
82 .map_err(|e| VCLError::IoError(format!("Prometheus counter: {}", e)))?;
83 registry.register(Box::new(c.clone()))
84 .map_err(|e| VCLError::IoError(format!("Prometheus register: {}", e)))?;
85 c
86 }};
87 }
88
89 macro_rules! gauge {
90 ($name:expr, $help:expr) => {{
91 let g = Gauge::with_opts(Opts::new($name, $help))
92 .map_err(|e| VCLError::IoError(format!("Prometheus gauge: {}", e)))?;
93 registry.register(Box::new(g.clone()))
94 .map_err(|e| VCLError::IoError(format!("Prometheus register: {}", e)))?;
95 g
96 }};
97 }
98
99 let rtt_histogram = Histogram::with_opts(
100 HistogramOpts::new("vcl_rtt_seconds", "Round-trip time in seconds")
101 .buckets(vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0]),
102 ).map_err(|e| VCLError::IoError(format!("Prometheus histogram: {}", e)))?;
103 registry.register(Box::new(rtt_histogram.clone()))
104 .map_err(|e| VCLError::IoError(format!("Prometheus register: {}", e)))?;
105
106 debug!("VCLPrometheusExporter initialized");
107
108 Ok(VCLPrometheusExporter {
109 bytes_sent: counter!("vcl_bytes_sent_total", "Total bytes sent"),
110 bytes_received: counter!("vcl_bytes_received_total", "Total bytes received"),
111 packets_sent: counter!("vcl_packets_sent_total", "Total packets sent"),
112 packets_received: counter!("vcl_packets_received_total", "Total packets received"),
113 packets_retransmitted: counter!("vcl_packets_retransmitted_total","Total retransmitted packets"),
114 packets_dropped: counter!("vcl_packets_dropped_total", "Total dropped packets"),
115 connections_active: gauge! ("vcl_connections_active", "Currently active connections"),
116 reconnect_count: counter!("vcl_reconnects_total", "Total reconnection attempts"),
117 handshakes_total: counter!("vcl_handshakes_total", "Total handshakes completed"),
118 key_rotations_total: counter!("vcl_key_rotations_total", "Total key rotations"),
119 loss_rate: gauge! ("vcl_loss_rate", "Current packet loss rate 0.0-1.0"),
120 rtt_seconds: gauge! ("vcl_rtt_seconds_current", "Current smoothed RTT in seconds"),
121 cwnd_packets: gauge! ("vcl_cwnd_packets", "Current congestion window size"),
122 obfuscation_overhead: gauge! ("vcl_obfuscation_overhead_ratio", "Obfuscation overhead ratio"),
123 mtu_bytes: gauge! ("vcl_mtu_bytes", "Current path MTU in bytes"),
124 dns_queries_total: counter!("vcl_dns_queries_total", "Total DNS queries intercepted"),
125 dns_blocked_total: counter!("vcl_dns_blocked_total", "Total DNS queries blocked"),
126 dns_cache_hits: counter!("vcl_dns_cache_hits_total", "Total DNS cache hits"),
127 fragments_sent: counter!("vcl_fragments_sent_total", "Total fragmented messages sent"),
128 fragments_reassembled: counter!("vcl_fragments_reassembled_total","Total fragments reassembled"),
129 tunnel_state: gauge! ("vcl_tunnel_state", "Tunnel state: 0=Stopped 1=Connecting 2=Connected 3=Reconnecting 4=Failed"),
130 rtt_histogram,
131 registry,
132 })
133 }
134
135 pub fn update_bytes_sent(&self, bytes: u64) {
139 self.bytes_sent.inc_by(bytes as f64);
140 }
141
142 pub fn update_bytes_received(&self, bytes: u64) {
144 self.bytes_received.inc_by(bytes as f64);
145 }
146
147 pub fn update_packets_sent(&self, count: u64) {
149 self.packets_sent.inc_by(count as f64);
150 }
151
152 pub fn update_packets_received(&self, count: u64) {
154 self.packets_received.inc_by(count as f64);
155 }
156
157 pub fn update_retransmit(&self) {
159 self.packets_retransmitted.inc();
160 }
161
162 pub fn update_dropped(&self, count: u64) {
164 self.packets_dropped.inc_by(count as f64);
165 }
166
167 pub fn set_connections_active(&self, count: f64) {
169 self.connections_active.set(count);
170 }
171
172 pub fn update_reconnect(&self) {
174 self.reconnect_count.inc();
175 }
176
177 pub fn update_handshake(&self) {
179 self.handshakes_total.inc();
180 }
181
182 pub fn update_key_rotation(&self) {
184 self.key_rotations_total.inc();
185 }
186
187 pub fn set_loss_rate(&self, rate: f64) {
189 self.loss_rate.set(rate);
190 }
191
192 pub fn set_rtt_seconds(&self, rtt: f64) {
194 self.rtt_seconds.set(rtt);
195 self.rtt_histogram.observe(rtt);
196 }
197
198 pub fn set_cwnd(&self, cwnd: f64) {
200 self.cwnd_packets.set(cwnd);
201 }
202
203 pub fn set_obfuscation_overhead(&self, ratio: f64) {
205 self.obfuscation_overhead.set(ratio);
206 }
207
208 pub fn set_mtu(&self, mtu: u16) {
210 self.mtu_bytes.set(mtu as f64);
211 }
212
213 pub fn update_dns_queries(&self, count: u64) {
215 self.dns_queries_total.inc_by(count as f64);
216 }
217
218 pub fn update_dns_blocked(&self, count: u64) {
220 self.dns_blocked_total.inc_by(count as f64);
221 }
222
223 pub fn update_dns_cache_hits(&self, count: u64) {
225 self.dns_cache_hits.inc_by(count as f64);
226 }
227
228 pub fn update_fragments_sent(&self, count: u64) {
230 self.fragments_sent.inc_by(count as f64);
231 }
232
233 pub fn update_fragments_reassembled(&self, count: u64) {
235 self.fragments_reassembled.inc_by(count as f64);
236 }
237
238 pub fn set_tunnel_state(&self, state: f64) {
240 self.tunnel_state.set(state);
241 }
242
243 pub fn update_from_metrics(&self, m: &VCLMetrics) {
251 self.set_loss_rate(m.loss_rate());
255 self.set_obfuscation_overhead(0.0); if let Some(rtt) = m.avg_rtt() {
258 self.set_rtt_seconds(rtt.as_secs_f64());
259 }
260 if let Some(cwnd) = m.avg_cwnd() {
261 self.set_cwnd(cwnd);
262 }
263 }
264
265 pub fn update_from_tunnel_stats(&self, stats: &TunnelStats) {
267 self.set_loss_rate(stats.loss_rate);
268 self.set_obfuscation_overhead(stats.obfuscation_overhead);
269 self.set_mtu(stats.mtu);
270
271 if let Some(rtt) = stats.keepalive_rtt {
272 self.set_rtt_seconds(rtt.as_secs_f64());
273 }
274
275 let state_val = match stats.state {
276 crate::tunnel::TunnelState::Stopped => 0.0,
277 crate::tunnel::TunnelState::Connecting => 1.0,
278 crate::tunnel::TunnelState::Connected => 2.0,
279 crate::tunnel::TunnelState::Reconnecting => 3.0,
280 crate::tunnel::TunnelState::Failed => 4.0,
281 };
282 self.set_tunnel_state(state_val);
283 }
284
285 pub fn render(&self) -> String {
291 let encoder = TextEncoder::new();
292 let metric_families = self.registry.gather();
293 let mut output = Vec::new();
294 encoder.encode(&metric_families, &mut output)
295 .unwrap_or_default();
296 String::from_utf8(output).unwrap_or_default()
297 }
298
299 pub fn registry(&self) -> &Registry {
301 &self.registry
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use std::time::Duration;
309
310 fn exporter() -> VCLPrometheusExporter {
311 VCLPrometheusExporter::new().unwrap()
312 }
313
314 #[test]
315 fn test_new() {
316 let e = exporter();
317 let output = e.render();
318 assert!(!output.is_empty());
319 }
320
321 #[test]
322 fn test_render_contains_metric_names() {
323 let e = exporter();
324 let output = e.render();
325 assert!(output.contains("vcl_bytes_sent_total"));
326 assert!(output.contains("vcl_bytes_received_total"));
327 assert!(output.contains("vcl_packets_sent_total"));
328 assert!(output.contains("vcl_loss_rate"));
329 assert!(output.contains("vcl_rtt_seconds_current"));
330 assert!(output.contains("vcl_tunnel_state"));
331 assert!(output.contains("vcl_dns_queries_total"));
332 assert!(output.contains("vcl_mtu_bytes"));
333 }
334
335 #[test]
336 fn test_update_bytes_sent() {
337 let e = exporter();
338 e.update_bytes_sent(1024);
339 e.update_bytes_sent(512);
340 let output = e.render();
341 assert!(output.contains("vcl_bytes_sent_total 1536"));
342 }
343
344 #[test]
345 fn test_update_bytes_received() {
346 let e = exporter();
347 e.update_bytes_received(2048);
348 let output = e.render();
349 assert!(output.contains("vcl_bytes_received_total 2048"));
350 }
351
352 #[test]
353 fn test_update_packets() {
354 let e = exporter();
355 e.update_packets_sent(10);
356 e.update_packets_received(8);
357 let output = e.render();
358 assert!(output.contains("vcl_packets_sent_total 10"));
359 assert!(output.contains("vcl_packets_received_total 8"));
360 }
361
362 #[test]
363 fn test_update_retransmit() {
364 let e = exporter();
365 e.update_retransmit();
366 e.update_retransmit();
367 let output = e.render();
368 assert!(output.contains("vcl_packets_retransmitted_total 2"));
369 }
370
371 #[test]
372 fn test_update_dropped() {
373 let e = exporter();
374 e.update_dropped(5);
375 let output = e.render();
376 assert!(output.contains("vcl_packets_dropped_total 5"));
377 }
378
379 #[test]
380 fn test_set_loss_rate() {
381 let e = exporter();
382 e.set_loss_rate(0.05);
383 let output = e.render();
384 assert!(output.contains("vcl_loss_rate 0.05"));
385 }
386
387 #[test]
388 fn test_set_rtt() {
389 let e = exporter();
390 e.set_rtt_seconds(0.042);
391 let output = e.render();
392 assert!(output.contains("vcl_rtt_seconds_current 0.042"));
393 assert!(output.contains("vcl_rtt_seconds_bucket"));
394 }
395
396 #[test]
397 fn test_set_mtu() {
398 let e = exporter();
399 e.set_mtu(1420);
400 let output = e.render();
401 assert!(output.contains("vcl_mtu_bytes 1420"));
402 }
403
404 #[test]
405 fn test_set_connections_active() {
406 let e = exporter();
407 e.set_connections_active(3.0);
408 let output = e.render();
409 assert!(output.contains("vcl_connections_active 3"));
410 }
411
412 #[test]
413 fn test_dns_metrics() {
414 let e = exporter();
415 e.update_dns_queries(100);
416 e.update_dns_blocked(10);
417 e.update_dns_cache_hits(50);
418 let output = e.render();
419 assert!(output.contains("vcl_dns_queries_total 100"));
420 assert!(output.contains("vcl_dns_blocked_total 10"));
421 assert!(output.contains("vcl_dns_cache_hits_total 50"));
422 }
423
424 #[test]
425 fn test_fragment_metrics() {
426 let e = exporter();
427 e.update_fragments_sent(20);
428 e.update_fragments_reassembled(18);
429 let output = e.render();
430 assert!(output.contains("vcl_fragments_sent_total 20"));
431 assert!(output.contains("vcl_fragments_reassembled_total 18"));
432 }
433
434 #[test]
435 fn test_tunnel_state_connected() {
436 let e = exporter();
437 e.set_tunnel_state(2.0);
438 let output = e.render();
439 assert!(output.contains("vcl_tunnel_state 2"));
440 }
441
442 #[test]
443 fn test_reconnect_counter() {
444 let e = exporter();
445 e.update_reconnect();
446 e.update_reconnect();
447 e.update_reconnect();
448 let output = e.render();
449 assert!(output.contains("vcl_reconnects_total 3"));
450 }
451
452 #[test]
453 fn test_handshake_counter() {
454 let e = exporter();
455 e.update_handshake();
456 let output = e.render();
457 assert!(output.contains("vcl_handshakes_total 1"));
458 }
459
460 #[test]
461 fn test_key_rotation_counter() {
462 let e = exporter();
463 e.update_key_rotation();
464 e.update_key_rotation();
465 let output = e.render();
466 assert!(output.contains("vcl_key_rotations_total 2"));
467 }
468
469 #[test]
470 fn test_obfuscation_overhead() {
471 let e = exporter();
472 e.set_obfuscation_overhead(0.15);
473 let output = e.render();
474 assert!(output.contains("vcl_obfuscation_overhead_ratio 0.15"));
475 }
476
477 #[test]
478 fn test_update_from_metrics() {
479 let mut m = VCLMetrics::new();
480 m.record_sent(1000);
481 m.record_rtt_sample(Duration::from_millis(42));
482 let e = exporter();
483 e.update_from_metrics(&m);
484 let output = e.render();
485 assert!(output.contains("vcl_rtt_seconds_current"));
486 assert!(output.contains("vcl_loss_rate"));
487 }
488
489 #[test]
490 fn test_cwnd_gauge() {
491 let e = exporter();
492 e.set_cwnd(32.0);
493 let output = e.render();
494 assert!(output.contains("vcl_cwnd_packets 32"));
495 }
496
497 #[test]
498 fn test_render_is_valid_utf8() {
499 let e = exporter();
500 e.update_bytes_sent(42);
501 let output = e.render();
502 assert!(output.is_ascii() || !output.is_empty());
503 }
504
505 #[test]
506 fn test_multiple_exporters_independent() {
507 let e1 = exporter();
508 let e2 = exporter();
509 e1.update_bytes_sent(100);
510 e2.update_bytes_sent(200);
511 let o1 = e1.render();
512 let o2 = e2.render();
513 assert!(o1.contains("vcl_bytes_sent_total 100"));
514 assert!(o2.contains("vcl_bytes_sent_total 200"));
515 }
516}