web/telemetry.rs
1//! Metric names and recorder setup.
2//!
3//! Names carry the `nomisync_` prefix because the series land in a
4//! VictoriaMetrics shared with every other app on the cluster: a bare
5//! `http_requests_total` would collide with anything else exporting the obvious
6//! name, and an ad-hoc query without a namespace filter would silently mix them.
7//! Keeping the names here as constants means the exporter and any test that
8//! asserts on them cannot drift apart.
9
10use metrics_exporter_prometheus::{BuildError, Matcher, PrometheusBuilder, PrometheusHandle};
11
12/// Requests served, labelled by method, matched path and status.
13pub const HTTP_REQUESTS: &str = "nomisync_http_requests_total";
14
15/// Request latency in seconds, same labels, bucketed by [`LATENCY_BUCKETS`].
16pub const HTTP_REQUEST_DURATION: &str = "nomisync_http_requests_duration_seconds";
17
18/// Auth attempts refused by the per-address throttle.
19pub const AUTH_THROTTLED: &str = "nomisync_auth_throttled_total";
20
21/// Web-request latencies: sub-10ms to 10s, which spans a cached page render and
22/// a slow report query.
23const LATENCY_BUCKETS: &[f64] = &[
24 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
25];
26
27/// Install the process-wide Prometheus recorder.
28///
29/// # Errors
30/// [`BuildError`] if the bucket configuration is rejected or a recorder is
31/// already installed — both are startup faults, reported rather than panicked.
32pub fn install_recorder() -> Result<PrometheusHandle, BuildError> {
33 PrometheusBuilder::new()
34 .set_buckets_for_metric(
35 Matcher::Full(HTTP_REQUEST_DURATION.to_string()),
36 LATENCY_BUCKETS,
37 )?
38 .install_recorder()
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 #[test]
46 fn names_carry_the_app_prefix() {
47 for name in [HTTP_REQUESTS, HTTP_REQUEST_DURATION] {
48 assert!(
49 name.starts_with("nomisync_"),
50 "{name} would collide with other apps in the shared VictoriaMetrics"
51 );
52 }
53 }
54
55 #[test]
56 fn recorder_renders_the_registered_series() {
57 let Ok(handle) = install_recorder() else {
58 // A recorder is global and install-once; another test in this binary
59 // may already hold it. Nothing to assert then.
60 return;
61 };
62 metrics::counter!(HTTP_REQUESTS, &[("method", "GET")]).increment(1);
63 let rendered = handle.render();
64 assert!(
65 rendered.contains(HTTP_REQUESTS),
66 "exporter did not render {HTTP_REQUESTS}"
67 );
68 }
69}