Files
wz-phone/crates/wzp-web/src/metrics.rs
2026-05-11 12:37:32 +04:00

130 lines
3.8 KiB
Rust

//! 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<super::AppState>,
) -> 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"));
}
}