1use 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";
25const TRANSPORT_DELIVERIES_TOTAL: &str = "liminal_transport_deliveries_total";
34const 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";
45const SELF_STREAMS_DISABLED_TOTAL: &str = "liminal_self_streams_disabled_total";
47const CONSOLE_STREAMS_DISABLED_TOTAL: &str = "liminal_console_streams_disabled_total";
50const 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";
59const PRESSURE_REFUSED_TOTAL: &str = "liminal_pressure_refused_total";
63const TRANSPORT_LABEL: &str = "transport";
67const ADMISSION_REFUSALS_TOTAL: &str = "liminal_admission_refusals_total";
78const HANDSHAKE_REFUSALS_TOTAL: &str = "liminal_handshake_refusals_total";
85const REASON_LABEL: &str = "reason";
92const RECEIPT_DISPLACEMENTS_TOTAL: &str = "liminal_receipt_displacements_total";
102const RECEIPT_POOL_RUNAWAY_TOTAL: &str = "liminal_receipt_pool_runaway_total";
113const CONNECTION_FATES_PARKED: &str = "liminal_connection_fates_parked";
121const SCOPE_LABEL: &str = "scope";
124const POOL_LABEL: &str = "pool";
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub(crate) enum ReceiptWindowScope {
134 LiveReceiptParticipant,
136 ProvenanceParticipant,
138}
139
140impl ReceiptWindowScope {
141 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
155pub(crate) enum SharedReceiptPool {
156 LiveReceiptServer,
158 ProvenanceServer,
160 ProvenanceConversation,
162}
163
164impl SharedReceiptPool {
165 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 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#[derive(Clone, Debug)]
190struct ServerMetrics {
191 connections_active: GaugeHandle,
192 connection_fates_parked: GaugeHandle,
193 publishes_total: CounterHandle,
194 deliveries_total: CounterHandle,
195 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 admission_refusals: [CounterHandle; AdmissionRefusal::LABELS.len()],
213 handshake_refusals: [CounterHandle; UpgradeRefusal::LABELS.len()],
215 receipt_displacements: [CounterHandle; ReceiptWindowScope::LABELS.len()],
217 receipt_pool_runaway: [CounterHandle; SharedReceiptPool::LABELS.len()],
219}
220
221const 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
232pub 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
252pub fn connection_spawned() {
255 if let Some(metrics) = SERVER_METRICS.get() {
256 metrics.connections_active.increment();
257 }
258}
259
260pub fn connection_closed() {
263 if let Some(metrics) = SERVER_METRICS.get() {
264 metrics.connections_active.decrement();
265 }
266}
267
268pub(crate) fn connection_fate_parked() {
272 if let Some(metrics) = SERVER_METRICS.get() {
273 metrics.connection_fates_parked.increment();
274 }
275}
276
277pub(crate) fn connection_fate_unparked() {
280 if let Some(metrics) = SERVER_METRICS.get() {
281 metrics.connection_fates_parked.decrement();
282 }
283}
284
285pub fn publish_accepted() {
288 if let Some(metrics) = SERVER_METRICS.get() {
289 metrics.publishes_total.increment();
290 }
291}
292
293pub 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
305pub 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
319pub(crate) fn admission_refused(refusal: AdmissionRefusal) {
326 if let Some(metrics) = SERVER_METRICS.get() {
327 metrics.admission_refusals[refusal.slot()].increment();
328 }
329}
330
331pub(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
357pub(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
364pub(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
397pub fn subscription_shed() {
404 if let Some(metrics) = SERVER_METRICS.get() {
405 metrics.sheds_total.increment();
406 }
407}
408
409pub(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
425pub(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
437pub(crate) fn render_dedup_head_counters(output: &mut String) {
456 render_dedup_head_block(output, dedup_head_counts());
457}
458
459fn 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#[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#[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#[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#[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
642fn 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
651fn 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
678fn 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
703fn 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
718fn global_or_install() -> Option<&'static MetricsRegistry> {
723 if let Some(registry) = global_registry() {
724 return Some(registry);
725 }
726 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 #[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(®istry.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 #[test]
792 fn register_warns_when_a_family_registration_fails() -> Result<(), Box<dyn std::error::Error>> {
793 let registry = MetricsRegistry::new();
794 registry.register_counter(CONNECTIONS_ACTIVE, no_labels())?;
797
798 let log = crate::test_log::CapturedLog::default();
799 let metrics = log.capture(|| ServerMetrics::register(®istry));
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}