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::durability::{DedupHeadCounts, dedup_head_counts};
12use liminal::metrics::{
13    CounterHandle, GaugeHandle, MetricRegistrationError, MetricsRegistry, global_registry,
14    install_global_registry,
15};
16
17use crate::console::counts::ConsoleFailure;
18use crate::server::connection::refusal::AdmissionRefusal;
19use crate::server::connection::websocket::UpgradeRefusal;
20use crate::server::mount::MountKind;
21
22const CONNECTIONS_ACTIVE: &str = "liminal_connections_active";
23const PUBLISHES_TOTAL: &str = "liminal_publishes_total";
24const DELIVERIES_TOTAL: &str = "liminal_deliveries_total";
25/// Second-wave: deliveries split by the transport that carried them.
26///
27/// ADDITIVE. [`DELIVERIES_TOTAL`] keeps its meaning and its exposition line
28/// unchanged — an operator's existing `/metrics` scrape sees every line it saw
29/// before, plus these. The two families count different things and are not
30/// expected to agree: the aggregate counts subscriber deliveries accepted at
31/// PUBLISH time (channel-actor fan-out), this one counts `Deliver` frames the
32/// per-connection pump actually enqueued toward a socket.
33const TRANSPORT_DELIVERIES_TOTAL: &str = "liminal_transport_deliveries_total";
34/// Second-wave: subscriptions shed by an inbox overflow (P0 #55).
35///
36/// Deliberately UNLABELLED. A per-channel or per-subscription shed counter is
37/// unbounded cardinality on a surface a scraper keeps forever; the channel and
38/// subscription of any individual shed are on the `warn` line the shed emits
39/// beside this increment, where they cost one log record rather than a permanent
40/// time series.
41const SHEDS_TOTAL: &str = "liminal_subscription_sheds_total";
42const SELF_STREAM_APPEND_FAILURES_TOTAL: &str = "liminal_self_stream_append_failures_total";
43const SELF_STREAMS_RESERVED_PUBLICATIONS_TOTAL: &str =
44    "liminal_self_streams_reserved_publications_total";
45/// Starts that omitted self streams because no participant service was installed.
46const SELF_STREAMS_DISABLED_TOTAL: &str = "liminal_self_streams_disabled_total";
47/// SRV-012 R4: set to 1 when the console streams are off for want of a
48/// participant (no publisher owner runs them).
49const CONSOLE_STREAMS_DISABLED_TOTAL: &str = "liminal_console_streams_disabled_total";
50/// SRV-012 R8: console bookkeeping failures, by `kind` (bounded by
51/// `ConsoleFailure`'s variants).
52const CONSOLE_FAILURES_TOTAL: &str = "liminal_console_failures_total";
53const KIND_LABEL: &str = "kind";
54const SELF_STREAMS_UNMATCHED_PRESSURE_TOTAL: &str = "liminal_self_streams_unmatched_pressure_total";
55const SELF_STREAMS_SUPPRESSED_PRESSURE_TOTAL: &str =
56    "liminal_self_streams_suppressed_pressure_total";
57const SELF_STREAMS_PRESSURE_LEDGER_EVICTIONS_TOTAL: &str =
58    "liminal_self_streams_pressure_ledger_evictions_total";
59/// Backpressure signals (`Accept`/`Defer`/`Reject`) refused by the access
60/// decision on their subscription's subject: the signal has no refusal frame,
61/// so the count is the only place the refusal shows besides the log line.
62const PRESSURE_REFUSED_TOTAL: &str = "liminal_pressure_refused_total";
63/// Label key carried by [`TRANSPORT_DELIVERIES_TOTAL`]. Cardinality is bounded by
64/// [`MountKind`]'s variants, which is why THIS split is affordable and a
65/// per-channel one is not.
66const TRANSPORT_LABEL: &str = "transport";
67/// P0 #56: connections turned away at the admission door, by reason class.
68///
69/// ADDITIVE, and it has to be, because no existing family can answer this.
70/// [`CONNECTIONS_ACTIVE`] is incremented in `register_record`
71/// (`server/connection/supervisor.rs:2822`), which is reached only AFTER
72/// admission has succeeded — a refusal is exactly the case where it never runs.
73/// On a server refusing every connection that gauge therefore reads a flat
74/// zero, indistinguishable from a healthy idle server, which is what the field
75/// estate's dashboards showed while 82,166 consecutive connections were turned
76/// away. This counter can only move when a connection was refused.
77const ADMISSION_REFUSALS_TOTAL: &str = "liminal_admission_refusals_total";
78/// P0 #56: WebSocket upgrades refused before admission was even attempted.
79///
80/// Separate from [`ADMISSION_REFUSALS_TOTAL`] because it counts a different
81/// event at a different door: these connections never reached the shared
82/// admission bound, the incarnation authority, or the registry. Folding them
83/// together would make an origin-policy rejection look like a capacity problem.
84const HANDSHAKE_REFUSALS_TOTAL: &str = "liminal_handshake_refusals_total";
85/// Label key carried by both refusal families.
86///
87/// Cardinality is bounded by the variant count of `AdmissionRefusal` and
88/// `UpgradeRefusal` respectively — fixed, enum-derived strings. A peer address,
89/// an origin value, or an error message here would be unbounded cardinality on
90/// a surface a scraper keeps forever.
91const REASON_LABEL: &str = "reason";
92/// Lane p0-39: entries displaced out of a per-participant retention window.
93///
94/// ADDITIVE, and it has to be: the event it counts did not exist before. The
95/// stage-8 receipt windows used to REFUSE at their bound, which was loud on the
96/// wire (a typed `ReceiptCapacityExceeded` the client could see). They now
97/// displace instead, which is silent to the arriving client BY DESIGN — the
98/// (N+1)th honest fingerprint lands. A bound that neither refuses nor discloses
99/// would hide exactly what the old wall at least made loud, so displacement is
100/// silent to experience and loud to record: this counter is the record.
101const RECEIPT_DISPLACEMENTS_TOTAL: &str = "liminal_receipt_displacements_total";
102/// Lane p0-39: observations that a SHARED receipt pool is carrying a churn
103/// storm (`liminal_receipt_pool_runaway_total`).
104///
105/// The three shared pools stopped being admission gates entirely — no
106/// configured number may refuse an honest third party there — so their only
107/// bound is the TTL window. This counter is the tripwire that replaces the
108/// wall: it is an OCCUPANCY OBSERVATION, never a gate, and nothing is refused
109/// because it moved. It increments once per admitted operation that observed a
110/// pool at or above its configured reporting threshold, so its rate is the
111/// storm's rate.
112const RECEIPT_POOL_RUNAWAY_TOTAL: &str = "liminal_receipt_pool_runaway_total";
113/// Connection fates currently parked on candidate-lane `Precedence`.
114///
115/// A parked fate holds a durable Open with no Complete. It waits on the drain of
116/// a lane another participant's terminal or a compaction marker occupies, and
117/// while it waits nothing on the wire shows it. A gauge that stays above zero is
118/// a conversation nobody has published into since, and a non-zero value at a
119/// clean stop is a stop that could not settle.
120const CONNECTION_FATES_PARKED: &str = "liminal_connection_fates_parked";
121/// Label key carried by [`RECEIPT_DISPLACEMENTS_TOTAL`]. Cardinality is bounded
122/// by [`ReceiptWindowScope`]'s two variants.
123const SCOPE_LABEL: &str = "scope";
124/// Label key carried by [`RECEIPT_POOL_RUNAWAY_TOTAL`]. Cardinality is bounded
125/// by [`SharedReceiptPool`]'s three variants.
126const POOL_LABEL: &str = "pool";
127
128/// The per-participant retention windows that can displace an older entry.
129///
130/// A fixed, enum-derived label vocabulary: these are the only two scopes whose
131/// configured number is a window size rather than a gate.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub(crate) enum ReceiptWindowScope {
134    /// Stage-8 `LiveReceiptParticipant`.
135    LiveReceiptParticipant,
136    /// Stage-8 `ProvenanceParticipant`.
137    ProvenanceParticipant,
138}
139
140impl ReceiptWindowScope {
141    /// Every window scope, in handle-storage order.
142    pub(crate) const LABELS: [&'static str; 2] =
143        ["live_receipt_participant", "provenance_participant"];
144
145    const fn slot(self) -> usize {
146        match self {
147            Self::LiveReceiptParticipant => 0,
148            Self::ProvenanceParticipant => 1,
149        }
150    }
151}
152
153/// The shared receipt pools whose retention is TTL-bounded and tripwired.
154#[derive(Clone, Copy, Debug, PartialEq, Eq)]
155pub(crate) enum SharedReceiptPool {
156    /// Stage-8 `LiveReceiptServer`.
157    LiveReceiptServer,
158    /// Stage-8 `ProvenanceServer`.
159    ProvenanceServer,
160    /// Stage-8 `ProvenanceConversation`.
161    ProvenanceConversation,
162}
163
164impl SharedReceiptPool {
165    /// Every shared pool, in handle-storage order.
166    pub(crate) const LABELS: [&'static str; 3] = [
167        "live_receipt_server",
168        "provenance_server",
169        "provenance_conversation",
170    ];
171
172    const fn slot(self) -> usize {
173        match self {
174            Self::LiveReceiptServer => 0,
175            Self::ProvenanceServer => 1,
176            Self::ProvenanceConversation => 2,
177        }
178    }
179
180    /// Human-readable pool name for the paired rising-edge warning.
181    pub(crate) const fn label(self) -> &'static str {
182        Self::LABELS[self.slot()]
183    }
184}
185
186static SERVER_METRICS: OnceLock<ServerMetrics> = OnceLock::new();
187
188/// Cached handles for the first-wave server metrics.
189#[derive(Clone, Debug)]
190struct ServerMetrics {
191    connections_active: GaugeHandle,
192    connection_fates_parked: GaugeHandle,
193    publishes_total: CounterHandle,
194    deliveries_total: CounterHandle,
195    /// One pre-registered handle per [`MountKind`], so the delivery hot path
196    /// looks a counter up by enum discriminant and never registers, allocates a
197    /// label string, or hashes a name mid-slice.
198    transport_deliveries: [CounterHandle; MOUNT_KINDS.len()],
199    sheds_total: CounterHandle,
200    self_stream_append_failures_total: CounterHandle,
201    self_streams_disabled_total: CounterHandle,
202    console_streams_disabled_total: CounterHandle,
203    console_failures: [CounterHandle; ConsoleFailure::LABELS.len()],
204    self_streams_reserved_publications_total: CounterHandle,
205    self_streams_unmatched_pressure_total: CounterHandle,
206    self_streams_suppressed_pressure_total: CounterHandle,
207    self_streams_pressure_ledger_evictions_total: CounterHandle,
208    pressure_refused_total: CounterHandle,
209    /// One pre-registered handle per admission-refusal class, so the refusal
210    /// path looks a counter up by discriminant and never allocates a label
211    /// string while it is busy turning a connection away.
212    admission_refusals: [CounterHandle; AdmissionRefusal::LABELS.len()],
213    /// One pre-registered handle per WebSocket upgrade-refusal class.
214    handshake_refusals: [CounterHandle; UpgradeRefusal::LABELS.len()],
215    /// One pre-registered handle per per-participant retention window.
216    receipt_displacements: [CounterHandle; ReceiptWindowScope::LABELS.len()],
217    /// One pre-registered handle per shared receipt pool.
218    receipt_pool_runaway: [CounterHandle; SharedReceiptPool::LABELS.len()],
219}
220
221/// Every transport a delivery can ride, in the order their handles are stored.
222const MOUNT_KINDS: [MountKind; 3] = [MountKind::Tcp, MountKind::WebSocket, MountKind::Loopback];
223
224const fn transport_slot(transport: MountKind) -> usize {
225    match transport {
226        MountKind::Tcp => 0,
227        MountKind::WebSocket => 1,
228        MountKind::Loopback => 2,
229    }
230}
231
232/// Enables metrics for this server process and registers the server families.
233///
234/// Idempotent: a second call is a no-op. Called once at server startup so the
235/// `/metrics` endpoint has data to render; the recording helpers below stay
236/// inert until this runs.
237pub fn init() {
238    if SERVER_METRICS.get().is_some() {
239        return;
240    }
241    let Some(registry) = global_or_install() else {
242        tracing::warn!(
243            "global metrics registry could not be installed or read back; server metrics stay disabled"
244        );
245        return;
246    };
247    if let Some(metrics) = ServerMetrics::register(registry) {
248        let _ = SERVER_METRICS.set(metrics);
249    }
250}
251
252/// Records the spawn of a supervised connection (`liminal_connections_active`
253/// gauge increment). Paired with [`connection_closed`] on every teardown route.
254pub fn connection_spawned() {
255    if let Some(metrics) = SERVER_METRICS.get() {
256        metrics.connections_active.increment();
257    }
258}
259
260/// Records the teardown of a supervised connection (`liminal_connections_active`
261/// gauge decrement). Paired with [`connection_spawned`].
262pub fn connection_closed() {
263    if let Some(metrics) = SERVER_METRICS.get() {
264        metrics.connections_active.decrement();
265    }
266}
267
268/// Records one connection fate parked on candidate-lane `Precedence`
269/// (`liminal_connection_fates_parked` gauge increment). Paired with
270/// [`connection_fate_unparked`] when the fate leaves the index.
271pub(crate) fn connection_fate_parked() {
272    if let Some(metrics) = SERVER_METRICS.get() {
273        metrics.connection_fates_parked.increment();
274    }
275}
276
277/// Records one parked connection fate leaving the index for its retry
278/// (`liminal_connection_fates_parked` gauge decrement).
279pub(crate) fn connection_fate_unparked() {
280    if let Some(metrics) = SERVER_METRICS.get() {
281        metrics.connection_fates_parked.decrement();
282    }
283}
284
285/// Records one accepted publish on the services publish path
286/// (`liminal_publishes_total`).
287pub fn publish_accepted() {
288    if let Some(metrics) = SERVER_METRICS.get() {
289        metrics.publishes_total.increment();
290    }
291}
292
293/// Records `count` genuine subscriber deliveries from a single publish
294/// (`liminal_deliveries_total`). A publish that reached no subscriber records
295/// nothing.
296pub fn deliveries_recorded(count: u64) {
297    if count == 0 {
298        return;
299    }
300    if let Some(metrics) = SERVER_METRICS.get() {
301        metrics.deliveries_total.increment_by(count);
302    }
303}
304
305/// Records pump deliveries on `transport` (`liminal_transport_deliveries_total`).
306///
307/// `count` is the number of `Deliver` frames one connection slice enqueued. A
308/// slice that delivered nothing records nothing, so an idle connection touches no
309/// atomic at all.
310pub fn transport_deliveries_recorded(transport: MountKind, count: u64) {
311    if count == 0 {
312        return;
313    }
314    if let Some(metrics) = SERVER_METRICS.get() {
315        metrics.transport_deliveries[transport_slot(transport)].increment_by(count);
316    }
317}
318
319/// Records one connection turned away at the admission door
320/// (`liminal_admission_refusals_total`, labelled by reason class).
321///
322/// Called at the three admission doors — TCP accept, WebSocket upgrade, and the
323/// in-process loopback — and nowhere else, so a refusal is counted exactly once
324/// no matter which mount knocked.
325pub(crate) fn admission_refused(refusal: AdmissionRefusal) {
326    if let Some(metrics) = SERVER_METRICS.get() {
327        metrics.admission_refusals[refusal.slot()].increment();
328    }
329}
330
331/// Records one WebSocket upgrade refused before admission was attempted
332/// (`liminal_handshake_refusals_total`, labelled by reason class).
333pub(crate) fn handshake_refused(refusal: &UpgradeRefusal) {
334    if let Some(metrics) = SERVER_METRICS.get() {
335        metrics.handshake_refusals[refusal.slot()].increment();
336    }
337}
338
339pub(crate) fn self_stream_append_failed() {
340    if let Some(metrics) = SERVER_METRICS.get() {
341        metrics.self_stream_append_failures_total.increment();
342    }
343}
344
345pub(crate) fn self_stream_reserved_publication() {
346    if let Some(metrics) = SERVER_METRICS.get() {
347        metrics.self_streams_reserved_publications_total.increment();
348    }
349}
350
351pub(crate) fn self_streams_disabled() {
352    if let Some(metrics) = SERVER_METRICS.get() {
353        metrics.self_streams_disabled_total.increment();
354    }
355}
356
357/// SRV-012 R8: `amount` console bookkeeping failures of `kind`.
358pub(crate) fn console_failure(kind: ConsoleFailure, amount: u64) {
359    if let Some(metrics) = SERVER_METRICS.get() {
360        metrics.console_failures[kind.slot()].increment_by(amount);
361    }
362}
363
364/// SRV-012 R4: the console streams are off for want of a participant.
365pub(crate) fn console_streams_disabled() {
366    if let Some(metrics) = SERVER_METRICS.get() {
367        metrics.console_streams_disabled_total.increment();
368    }
369}
370
371pub(crate) fn self_stream_pressure_unmatched() {
372    if let Some(metrics) = SERVER_METRICS.get() {
373        metrics.self_streams_unmatched_pressure_total.increment();
374    }
375}
376
377pub(crate) fn self_stream_pressure_suppressed() {
378    if let Some(metrics) = SERVER_METRICS.get() {
379        metrics.self_streams_suppressed_pressure_total.increment();
380    }
381}
382
383pub(crate) fn pressure_refused() {
384    if let Some(metrics) = SERVER_METRICS.get() {
385        metrics.pressure_refused_total.increment();
386    }
387}
388
389pub(crate) fn pressure_ledger_evicted() {
390    if let Some(metrics) = SERVER_METRICS.get() {
391        metrics
392            .self_streams_pressure_ledger_evictions_total
393            .increment();
394    }
395}
396
397/// Records one subscription shed by an inbox overflow
398/// (`liminal_subscription_sheds_total`).
399///
400/// Paired at its only call site with the `warn` that names the channel,
401/// subscription and transport: the counter is what an alert fires on, the log
402/// line is what the alert is then read against.
403pub fn subscription_shed() {
404    if let Some(metrics) = SERVER_METRICS.get() {
405        metrics.sheds_total.increment();
406    }
407}
408
409/// Records `count` entries displaced out of one per-participant retention
410/// window (`liminal_receipt_displacements_total`, labelled by scope).
411///
412/// The arriving client sees nothing — that is the ruled behaviour, "silent to
413/// experience" — so this counter and the `debug` line beside it are the only
414/// record that a bound did work. A zero count returns early, so the ordinary
415/// with-headroom path touches no atomic.
416pub(crate) fn receipt_entries_displaced(scope: ReceiptWindowScope, count: u64) {
417    if count == 0 {
418        return;
419    }
420    if let Some(metrics) = SERVER_METRICS.get() {
421        metrics.receipt_displacements[scope.slot()].increment_by(count);
422    }
423}
424
425/// Records one observation of a shared receipt pool at or above its configured
426/// reporting threshold (`liminal_receipt_pool_runaway_total`, labelled by pool).
427///
428/// An OBSERVATION, not a refusal: the operation that made it was admitted. The
429/// rising-edge `warn` beside the first such observation carries the occupancy
430/// and threshold; this counter carries the storm's rate.
431pub(crate) fn receipt_pool_runaway_observed(pool: SharedReceiptPool) {
432    if let Some(metrics) = SERVER_METRICS.get() {
433        metrics.receipt_pool_runaway[pool.slot()].increment();
434    }
435}
436
437/// Appends the dedup head counters (DUR-008 A12) to a `/metrics` body: one
438/// unlabelled counter family per name of
439/// [`DedupHeadCounts::named`](liminal::durability::DedupHeadCounts::named),
440/// all three from one [`dedup_head_counts`] read.
441///
442/// Rendered at SCRAPE time, never registered. The counters are the library's
443/// process-global atomics, moved on the dedup lookup path by one relaxed
444/// `fetch_add` with no registry in reach; a registry handle mirroring them
445/// would need that path to bump a second atomic, and `liminal::durability` to
446/// know the server's registry, or a sampler copying them on a clock. Read here
447/// they cost three relaxed loads per scrape, take no lock, and touch nothing a
448/// publish or a dedup lookup holds.
449///
450/// The names are the library's own, iterated rather than retyped, so the
451/// exposition cannot drift from them. They carry the server's `liminal_`
452/// prefix and the counter `_total` suffix; no registry family is named
453/// `liminal_dedup_head_*`, so no family is declared twice. The header lines
454/// are the ones `liminal::metrics::render` writes.
455pub(crate) fn render_dedup_head_counters(output: &mut String) {
456    render_dedup_head_block(output, dedup_head_counts());
457}
458
459/// The exposition block for one reading of the dedup head counters: for each
460/// counter, in [`DedupHeadCounts::named`]'s order, its `# HELP` line, its
461/// `# TYPE` line and its value line.
462fn render_dedup_head_block(output: &mut String, counts: DedupHeadCounts) {
463    for (name, value) in counts.named() {
464        let value = value.to_string();
465        for piece in [
466            "# HELP ",
467            name,
468            " ",
469            name,
470            "\n# TYPE ",
471            name,
472            " counter\n",
473            name,
474            " ",
475            &value,
476            "\n",
477        ] {
478            output.push_str(piece);
479        }
480    }
481}
482
483/// The current value of the accepted-publish counter, for a test that needs an
484/// UNRELATED counter to prove its harness measured anything at all.
485///
486/// `None` means the family is not readable — either [`init`] has not run in this
487/// process or the registry holds no such counter — which a caller must treat as
488/// "no measurement", never as zero. The name is read from the same constant the
489/// registration uses, so the two cannot drift.
490#[cfg(test)]
491pub(crate) fn publishes_total_value() -> Option<u64> {
492    use liminal::metrics::MetricValue;
493
494    let registry = global_registry()?;
495    registry
496        .snapshot()
497        .metrics()
498        .iter()
499        .find(|metric| metric.name == PUBLISHES_TOTAL)
500        .and_then(|metric| match metric.value {
501            MetricValue::Counter(value) => Some(value),
502            MetricValue::Gauge(_) | MetricValue::Histogram(_) => None,
503        })
504}
505
506/// The current value of one lane p0-39 displacement counter, by scope.
507///
508/// `None` means the family is not readable — either [`init`] has not run in
509/// this process or the registry holds no such counter — which a caller must
510/// treat as "no measurement", never as zero. Name and label are read from the
511/// same constants the registration uses, so a rename shows up as a failure
512/// rather than as a vacuous pass.
513#[cfg(test)]
514pub(crate) fn receipt_displacements_value(scope: ReceiptWindowScope) -> Option<u64> {
515    labelled_counter_value(
516        RECEIPT_DISPLACEMENTS_TOTAL,
517        SCOPE_LABEL,
518        ReceiptWindowScope::LABELS[scope.slot()],
519    )
520}
521
522/// The current value of one lane p0-39 shared-pool tripwire counter, by pool.
523///
524/// `None` means "not measured", never zero — see
525/// [`receipt_displacements_value`].
526#[cfg(test)]
527pub(crate) fn receipt_pool_runaway_value(pool: SharedReceiptPool) -> Option<u64> {
528    labelled_counter_value(RECEIPT_POOL_RUNAWAY_TOTAL, POOL_LABEL, pool.label())
529}
530
531/// The current value of the parked connection-fate gauge.
532///
533/// `None` means "not measured", never zero — see
534/// [`receipt_displacements_value`].
535#[cfg(test)]
536pub(crate) fn connection_fates_parked_value() -> Option<i64> {
537    use liminal::metrics::MetricValue;
538
539    let registry = global_registry()?;
540    registry
541        .snapshot()
542        .metrics()
543        .iter()
544        .find(|metric| metric.name == CONNECTION_FATES_PARKED)
545        .and_then(|metric| match metric.value {
546            MetricValue::Gauge(value) => Some(value),
547            MetricValue::Counter(_) | MetricValue::Histogram(_) => None,
548        })
549}
550
551#[cfg(test)]
552fn labelled_counter_value(name: &str, key: &str, label: &str) -> Option<u64> {
553    use liminal::metrics::MetricValue;
554
555    let registry = global_registry()?;
556    registry
557        .snapshot()
558        .metrics()
559        .iter()
560        .find(|metric| {
561            metric.name == name
562                && metric
563                    .labels
564                    .iter()
565                    .any(|(metric_key, value)| metric_key == key && value == label)
566        })
567        .and_then(|metric| match metric.value {
568            MetricValue::Counter(value) => Some(value),
569            MetricValue::Gauge(_) | MetricValue::Histogram(_) => None,
570        })
571}
572
573impl ServerMetrics {
574    fn register(registry: &MetricsRegistry) -> Option<Self> {
575        Some(Self {
576            connections_active: register_gauge(registry, CONNECTIONS_ACTIVE)?,
577            connection_fates_parked: register_gauge(registry, CONNECTION_FATES_PARKED)?,
578            publishes_total: register_counter(registry, PUBLISHES_TOTAL)?,
579            deliveries_total: register_counter(registry, DELIVERIES_TOTAL)?,
580            transport_deliveries: register_transport_deliveries(registry)?,
581            sheds_total: register_counter(registry, SHEDS_TOTAL)?,
582            self_stream_append_failures_total: register_counter(
583                registry,
584                SELF_STREAM_APPEND_FAILURES_TOTAL,
585            )?,
586            self_streams_disabled_total: register_counter(registry, SELF_STREAMS_DISABLED_TOTAL)?,
587            console_streams_disabled_total: register_counter(
588                registry,
589                CONSOLE_STREAMS_DISABLED_TOTAL,
590            )?,
591            console_failures: register_labelled(
592                registry,
593                CONSOLE_FAILURES_TOTAL,
594                KIND_LABEL,
595                &ConsoleFailure::LABELS,
596            )?,
597            self_streams_reserved_publications_total: register_counter(
598                registry,
599                SELF_STREAMS_RESERVED_PUBLICATIONS_TOTAL,
600            )?,
601            self_streams_unmatched_pressure_total: register_counter(
602                registry,
603                SELF_STREAMS_UNMATCHED_PRESSURE_TOTAL,
604            )?,
605            self_streams_suppressed_pressure_total: register_counter(
606                registry,
607                SELF_STREAMS_SUPPRESSED_PRESSURE_TOTAL,
608            )?,
609            self_streams_pressure_ledger_evictions_total: register_counter(
610                registry,
611                SELF_STREAMS_PRESSURE_LEDGER_EVICTIONS_TOTAL,
612            )?,
613            pressure_refused_total: register_counter(registry, PRESSURE_REFUSED_TOTAL)?,
614            admission_refusals: register_labelled(
615                registry,
616                ADMISSION_REFUSALS_TOTAL,
617                REASON_LABEL,
618                &AdmissionRefusal::LABELS,
619            )?,
620            handshake_refusals: register_labelled(
621                registry,
622                HANDSHAKE_REFUSALS_TOTAL,
623                REASON_LABEL,
624                &UpgradeRefusal::LABELS,
625            )?,
626            receipt_displacements: register_labelled(
627                registry,
628                RECEIPT_DISPLACEMENTS_TOTAL,
629                SCOPE_LABEL,
630                &ReceiptWindowScope::LABELS,
631            )?,
632            receipt_pool_runaway: register_labelled(
633                registry,
634                RECEIPT_POOL_RUNAWAY_TOTAL,
635                POOL_LABEL,
636                &SharedReceiptPool::LABELS,
637            )?,
638        })
639    }
640}
641
642/// One unlabelled counter, registered; `None` after the warning when the
643/// registry refuses it.
644fn register_counter(registry: &MetricsRegistry, name: &'static str) -> Option<CounterHandle> {
645    registry
646        .register_counter(name, no_labels())
647        .inspect_err(warn_registration_failure)
648        .ok()
649}
650
651/// One unlabelled gauge, registered; `None` after the warning when the
652/// registry refuses it.
653fn register_gauge(registry: &MetricsRegistry, name: &'static str) -> Option<GaugeHandle> {
654    registry
655        .register_gauge(name, no_labels())
656        .inspect_err(warn_registration_failure)
657        .ok()
658}
659
660fn register_transport_deliveries(
661    registry: &MetricsRegistry,
662) -> Option<[CounterHandle; MOUNT_KINDS.len()]> {
663    let mut transport_deliveries = Vec::with_capacity(MOUNT_KINDS.len());
664    for transport in MOUNT_KINDS {
665        transport_deliveries.push(
666            registry
667                .register_counter(
668                    TRANSPORT_DELIVERIES_TOTAL,
669                    [(TRANSPORT_LABEL, transport.as_str())],
670                )
671                .inspect_err(warn_registration_failure)
672                .ok()?,
673        );
674    }
675    transport_deliveries.try_into().ok()
676}
677
678/// Pre-registers one counter per label value, in label order.
679///
680/// Every class is registered at `init`, not lazily on first refusal, so the
681/// exposition carries an explicit zero for classes that have not fired. A
682/// missing line and a zero line mean very different things to an operator: the
683/// first is "this server does not know about that failure mode", the second is
684/// "it has not happened".
685fn register_labelled<const N: usize>(
686    registry: &MetricsRegistry,
687    name: &'static str,
688    key: &'static str,
689    labels: &[&'static str; N],
690) -> Option<[CounterHandle; N]> {
691    let mut handles = Vec::with_capacity(N);
692    for label in labels {
693        handles.push(
694            registry
695                .register_counter(name, [(key, *label)])
696                .inspect_err(warn_registration_failure)
697                .ok()?,
698        );
699    }
700    handles.try_into().ok()
701}
702
703/// Names a failed family registration instead of letting `.ok()?` discard it.
704///
705/// `register` still answers `None` and metrics recording stays disabled; what
706/// changes is that the operator can see why from the log.
707fn warn_registration_failure(error: &MetricRegistrationError) {
708    tracing::warn!(
709        %error,
710        "server metrics family registration failed; metrics recording stays disabled"
711    );
712}
713
714const fn no_labels() -> std::iter::Empty<(&'static str, &'static str)> {
715    std::iter::empty()
716}
717
718/// Returns the process-global registry, installing a fresh one when none exists.
719///
720/// Enabling the gate here (rather than in the library) keeps standalone liminal
721/// users on the disabled fast path; the server is the sole installer.
722fn global_or_install() -> Option<&'static MetricsRegistry> {
723    if let Some(registry) = global_registry() {
724        return Some(registry);
725    }
726    // Best-effort install; if a concurrent caller won the race we still read the
727    // now-installed registry back below.
728    let _ = install_global_registry(MetricsRegistry::new());
729    global_registry()
730}
731
732#[cfg(test)]
733mod tests {
734    use super::{
735        CONNECTIONS_ACTIVE, DELIVERIES_TOTAL, PUBLISHES_TOTAL, ServerMetrics, connection_spawned,
736        deliveries_recorded, init, no_labels, publish_accepted, render_dedup_head_block,
737    };
738    use liminal::durability::DedupHeadCounts;
739    use liminal::metrics::{MetricsRegistry, global_registry, render};
740
741    /// The dedup head block, byte for byte, for one reading with three
742    /// distinct values: each counter's `# HELP`, `# TYPE` and value line, in
743    /// `named()` order, each line ended by one newline.
744    #[test]
745    fn the_dedup_head_block_renders_byte_for_byte() {
746        let mut output = String::new();
747        render_dedup_head_block(
748            &mut output,
749            DedupHeadCounts {
750                refusals: 7,
751                record_lost_races: 0,
752                repairs: 12,
753            },
754        );
755        assert_eq!(
756            output,
757            "# HELP liminal_dedup_head_refusals_total liminal_dedup_head_refusals_total\n\
758             # TYPE liminal_dedup_head_refusals_total counter\n\
759             liminal_dedup_head_refusals_total 7\n\
760             # HELP liminal_dedup_head_record_lost_races_total \
761             liminal_dedup_head_record_lost_races_total\n\
762             # TYPE liminal_dedup_head_record_lost_races_total counter\n\
763             liminal_dedup_head_record_lost_races_total 0\n\
764             # HELP liminal_dedup_head_repairs_total liminal_dedup_head_repairs_total\n\
765             # TYPE liminal_dedup_head_repairs_total counter\n\
766             liminal_dedup_head_repairs_total 12\n"
767        );
768    }
769
770    #[test]
771    fn init_registers_the_three_server_families_on_the_global_registry()
772    -> Result<(), Box<dyn std::error::Error>> {
773        init();
774        connection_spawned();
775        publish_accepted();
776        deliveries_recorded(2);
777
778        let registry =
779            global_registry().ok_or("init must install and enable the global registry")?;
780        let exposition = render(&registry.snapshot());
781
782        assert!(exposition.contains(CONNECTIONS_ACTIVE));
783        assert!(exposition.contains(PUBLISHES_TOTAL));
784        assert!(exposition.contains(DELIVERIES_TOTAL));
785
786        Ok(())
787    }
788
789    /// A failed family registration must not vanish: `register` returns None
790    /// (unchanged) AND warns with the underlying registration error.
791    #[test]
792    fn register_warns_when_a_family_registration_fails() -> Result<(), Box<dyn std::error::Error>> {
793        let registry = MetricsRegistry::new();
794        // Pre-register the gauge's name as a COUNTER so the gauge registration
795        // fails with an incompatible-kind error.
796        registry.register_counter(CONNECTIONS_ACTIVE, no_labels())?;
797
798        let log = crate::test_log::CapturedLog::default();
799        let metrics = log.capture(|| ServerMetrics::register(&registry));
800
801        assert!(
802            metrics.is_none(),
803            "a failed registration must still yield None"
804        );
805        let output = log.contents();
806        assert!(
807            output.contains("WARN") && output.contains(CONNECTIONS_ACTIVE),
808            "expected a warn carrying the registration error, got: {output}"
809        );
810
811        Ok(())
812    }
813}