Skip to main content

liminal_server/
metrics.rs

1//! Server-side metric recording over the process-global liminal registry.
2//!
3//! [`init`] installs a global [`MetricsRegistry`] — which flips the library's
4//! metrics gate on for this process — and registers the first-wave server
5//! families, caching their handles. Every recording helper no-ops until `init`
6//! has run, so a standalone liminal library user (who never calls `init`) pays
7//! nothing and the registry stays disabled.
8
9use std::sync::OnceLock;
10
11use liminal::metrics::{
12    CounterHandle, GaugeHandle, MetricsRegistry, global_registry, install_global_registry,
13};
14
15const CONNECTIONS_ACTIVE: &str = "liminal_connections_active";
16const PUBLISHES_TOTAL: &str = "liminal_publishes_total";
17const DELIVERIES_TOTAL: &str = "liminal_deliveries_total";
18
19static SERVER_METRICS: OnceLock<ServerMetrics> = OnceLock::new();
20
21/// Cached handles for the first-wave server metrics.
22#[derive(Clone, Debug)]
23struct ServerMetrics {
24    connections_active: GaugeHandle,
25    publishes_total: CounterHandle,
26    deliveries_total: CounterHandle,
27}
28
29/// Enables metrics for this server process and registers the server families.
30///
31/// Idempotent: a second call is a no-op. Called once at server startup so the
32/// `/metrics` endpoint has data to render; the recording helpers below stay
33/// inert until this runs.
34pub fn init() {
35    if SERVER_METRICS.get().is_some() {
36        return;
37    }
38    let Some(registry) = global_or_install() else {
39        return;
40    };
41    if let Some(metrics) = ServerMetrics::register(registry) {
42        let _ = SERVER_METRICS.set(metrics);
43    }
44}
45
46/// Records the spawn of a supervised connection (`liminal_connections_active`
47/// gauge increment). Paired with [`connection_closed`] on every teardown route.
48pub fn connection_spawned() {
49    if let Some(metrics) = SERVER_METRICS.get() {
50        metrics.connections_active.increment();
51    }
52}
53
54/// Records the teardown of a supervised connection (`liminal_connections_active`
55/// gauge decrement). Paired with [`connection_spawned`].
56pub fn connection_closed() {
57    if let Some(metrics) = SERVER_METRICS.get() {
58        metrics.connections_active.decrement();
59    }
60}
61
62/// Records one accepted publish on the services publish path
63/// (`liminal_publishes_total`).
64pub fn publish_accepted() {
65    if let Some(metrics) = SERVER_METRICS.get() {
66        metrics.publishes_total.increment();
67    }
68}
69
70/// Records `count` genuine subscriber deliveries from a single publish
71/// (`liminal_deliveries_total`). A publish that reached no subscriber records
72/// nothing.
73pub fn deliveries_recorded(count: u64) {
74    if count == 0 {
75        return;
76    }
77    if let Some(metrics) = SERVER_METRICS.get() {
78        metrics.deliveries_total.increment_by(count);
79    }
80}
81
82/// The current value of the accepted-publish counter, for a test that needs an
83/// UNRELATED counter to prove its harness measured anything at all.
84///
85/// `None` means the family is not readable — either [`init`] has not run in this
86/// process or the registry holds no such counter — which a caller must treat as
87/// "no measurement", never as zero. The name is read from the same constant the
88/// registration uses, so the two cannot drift.
89#[cfg(test)]
90pub(crate) fn publishes_total_value() -> Option<u64> {
91    use liminal::metrics::MetricValue;
92
93    let registry = global_registry()?;
94    registry
95        .snapshot()
96        .metrics()
97        .iter()
98        .find(|metric| metric.name == PUBLISHES_TOTAL)
99        .and_then(|metric| match metric.value {
100            MetricValue::Counter(value) => Some(value),
101            MetricValue::Gauge(_) | MetricValue::Histogram(_) => None,
102        })
103}
104
105impl ServerMetrics {
106    fn register(registry: &MetricsRegistry) -> Option<Self> {
107        let connections_active = registry
108            .register_gauge(CONNECTIONS_ACTIVE, no_labels())
109            .ok()?;
110        let publishes_total = registry
111            .register_counter(PUBLISHES_TOTAL, no_labels())
112            .ok()?;
113        let deliveries_total = registry
114            .register_counter(DELIVERIES_TOTAL, no_labels())
115            .ok()?;
116        Some(Self {
117            connections_active,
118            publishes_total,
119            deliveries_total,
120        })
121    }
122}
123
124const fn no_labels() -> std::iter::Empty<(&'static str, &'static str)> {
125    std::iter::empty()
126}
127
128/// Returns the process-global registry, installing a fresh one when none exists.
129///
130/// Enabling the gate here (rather than in the library) keeps standalone liminal
131/// users on the disabled fast path; the server is the sole installer.
132fn global_or_install() -> Option<&'static MetricsRegistry> {
133    if let Some(registry) = global_registry() {
134        return Some(registry);
135    }
136    // Best-effort install; if a concurrent caller won the race we still read the
137    // now-installed registry back below.
138    let _ = install_global_registry(MetricsRegistry::new());
139    global_registry()
140}
141
142#[cfg(test)]
143mod tests {
144    use super::{
145        CONNECTIONS_ACTIVE, DELIVERIES_TOTAL, PUBLISHES_TOTAL, connection_spawned,
146        deliveries_recorded, init, publish_accepted,
147    };
148    use liminal::metrics::{global_registry, render};
149
150    #[test]
151    fn init_registers_the_three_server_families_on_the_global_registry()
152    -> Result<(), Box<dyn std::error::Error>> {
153        init();
154        connection_spawned();
155        publish_accepted();
156        deliveries_recorded(2);
157
158        let registry =
159            global_registry().ok_or("init must install and enable the global registry")?;
160        let exposition = render(&registry.snapshot());
161
162        assert!(exposition.contains(CONNECTIONS_ACTIVE));
163        assert!(exposition.contains(PUBLISHES_TOTAL));
164        assert!(exposition.contains(DELIVERIES_TOTAL));
165
166        Ok(())
167    }
168}