//! Prometheus metrics for the WZP web bridge. use prometheus::{ Encoder, Histogram, HistogramOpts, IntCounter, IntCounterVec, IntGauge, Opts, Registry, TextEncoder, }; /// Holds all Prometheus metrics for the web bridge. #[derive(Clone)] pub struct WebMetrics { pub active_connections: IntGauge, pub frames_bridged: IntCounterVec, pub auth_failures: IntCounter, pub handshake_latency: Histogram, registry: Registry, } impl WebMetrics { /// Create and register all web bridge metrics. pub fn new() -> Self { let registry = Registry::new(); let active_connections = IntGauge::with_opts( Opts::new("wzp_web_active_connections", "Current WebSocket connections"), ) .expect("metric"); registry .register(Box::new(active_connections.clone())) .expect("register"); let frames_bridged = IntCounterVec::new( Opts::new("wzp_web_frames_bridged_total", "Audio frames bridged"), &["direction"], ) .expect("metric"); registry .register(Box::new(frames_bridged.clone())) .expect("register"); let auth_failures = IntCounter::with_opts( Opts::new("wzp_web_auth_failures_total", "Browser auth failures"), ) .expect("metric"); registry .register(Box::new(auth_failures.clone())) .expect("register"); let handshake_latency = Histogram::with_opts( HistogramOpts::new( "wzp_web_handshake_latency_seconds", "Relay handshake time", ) .buckets(vec![0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]), ) .expect("metric"); registry .register(Box::new(handshake_latency.clone())) .expect("register"); Self { active_connections, frames_bridged, auth_failures, handshake_latency, registry, } } /// Encode all metrics as Prometheus text exposition format. pub fn gather(&self) -> String { let encoder = TextEncoder::new(); let metric_families = self.registry.gather(); let mut buf = Vec::new(); encoder.encode(&metric_families, &mut buf).unwrap(); String::from_utf8(buf).unwrap() } } /// Axum handler that returns Prometheus text metrics. pub async fn metrics_handler( axum::extract::State(state): axum::extract::State, ) -> String { state.metrics.gather() } #[cfg(test)] mod tests { use super::*; #[test] fn web_metrics_register() { let m = WebMetrics::new(); // Touch CounterVec labels so they appear in output m.frames_bridged.with_label_values(&["up"]); m.frames_bridged.with_label_values(&["down"]); let output = m.gather(); assert!( output.contains("wzp_web_active_connections"), "missing active_connections" ); assert!( output.contains("wzp_web_frames_bridged_total"), "missing frames_bridged" ); assert!( output.contains("wzp_web_auth_failures_total"), "missing auth_failures" ); assert!( output.contains("wzp_web_handshake_latency_seconds"), "missing handshake_latency" ); } #[test] fn web_metrics_track_connections() { let m = WebMetrics::new(); assert_eq!(m.active_connections.get(), 0); m.active_connections.inc(); m.active_connections.inc(); assert_eq!(m.active_connections.get(), 2); m.active_connections.dec(); assert_eq!(m.active_connections.get(), 1); let output = m.gather(); assert!(output.contains("wzp_web_active_connections 1")); } }