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 ADVISED_TOTAL: &str = "liminal_advised_total";
66const TRANSPORT_LABEL: &str = "transport";
70const ADMISSION_REFUSALS_TOTAL: &str = "liminal_admission_refusals_total";
81const HANDSHAKE_REFUSALS_TOTAL: &str = "liminal_handshake_refusals_total";
88const REASON_LABEL: &str = "reason";
95const RECEIPT_DISPLACEMENTS_TOTAL: &str = "liminal_receipt_displacements_total";
105const RECEIPT_POOL_RUNAWAY_TOTAL: &str = "liminal_receipt_pool_runaway_total";
116const CONNECTION_FATES_PARKED: &str = "liminal_connection_fates_parked";
124const SCOPE_LABEL: &str = "scope";
127const POOL_LABEL: &str = "pool";
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub(crate) enum ReceiptWindowScope {
137 LiveReceiptParticipant,
139 ProvenanceParticipant,
141}
142
143impl ReceiptWindowScope {
144 pub(crate) const LABELS: [&'static str; 2] =
146 ["live_receipt_participant", "provenance_participant"];
147
148 const fn slot(self) -> usize {
149 match self {
150 Self::LiveReceiptParticipant => 0,
151 Self::ProvenanceParticipant => 1,
152 }
153 }
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub(crate) enum SharedReceiptPool {
159 LiveReceiptServer,
161 ProvenanceServer,
163 ProvenanceConversation,
165}
166
167impl SharedReceiptPool {
168 pub(crate) const LABELS: [&'static str; 3] = [
170 "live_receipt_server",
171 "provenance_server",
172 "provenance_conversation",
173 ];
174
175 const fn slot(self) -> usize {
176 match self {
177 Self::LiveReceiptServer => 0,
178 Self::ProvenanceServer => 1,
179 Self::ProvenanceConversation => 2,
180 }
181 }
182
183 pub(crate) const fn label(self) -> &'static str {
185 Self::LABELS[self.slot()]
186 }
187}
188
189static SERVER_METRICS: OnceLock<ServerMetrics> = OnceLock::new();
190
191#[derive(Clone, Debug)]
193struct ServerMetrics {
194 connections_active: GaugeHandle,
195 connection_fates_parked: GaugeHandle,
196 publishes_total: CounterHandle,
197 deliveries_total: CounterHandle,
198 transport_deliveries: [CounterHandle; MOUNT_KINDS.len()],
202 sheds_total: CounterHandle,
203 self_stream_append_failures_total: CounterHandle,
204 self_streams_disabled_total: CounterHandle,
205 console_streams_disabled_total: CounterHandle,
206 console_failures: [CounterHandle; ConsoleFailure::LABELS.len()],
207 self_streams_reserved_publications_total: CounterHandle,
208 self_streams_unmatched_pressure_total: CounterHandle,
209 self_streams_suppressed_pressure_total: CounterHandle,
210 self_streams_pressure_ledger_evictions_total: CounterHandle,
211 pressure_refused_total: CounterHandle,
212 advised_total: CounterHandle,
213 admission_refusals: [CounterHandle; AdmissionRefusal::LABELS.len()],
217 handshake_refusals: [CounterHandle; UpgradeRefusal::LABELS.len()],
219 receipt_displacements: [CounterHandle; ReceiptWindowScope::LABELS.len()],
221 receipt_pool_runaway: [CounterHandle; SharedReceiptPool::LABELS.len()],
223}
224
225const MOUNT_KINDS: [MountKind; 3] = [MountKind::Tcp, MountKind::WebSocket, MountKind::Loopback];
227
228const fn transport_slot(transport: MountKind) -> usize {
229 match transport {
230 MountKind::Tcp => 0,
231 MountKind::WebSocket => 1,
232 MountKind::Loopback => 2,
233 }
234}
235
236pub fn init() {
242 if SERVER_METRICS.get().is_some() {
243 return;
244 }
245 let Some(registry) = global_or_install() else {
246 tracing::warn!(
247 "global metrics registry could not be installed or read back; server metrics stay disabled"
248 );
249 return;
250 };
251 if let Some(metrics) = ServerMetrics::register(registry) {
252 let _ = SERVER_METRICS.set(metrics);
253 }
254}
255
256pub fn connection_spawned() {
259 if let Some(metrics) = SERVER_METRICS.get() {
260 metrics.connections_active.increment();
261 }
262}
263
264pub fn connection_closed() {
267 if let Some(metrics) = SERVER_METRICS.get() {
268 metrics.connections_active.decrement();
269 }
270}
271
272pub(crate) fn connection_fate_parked() {
276 if let Some(metrics) = SERVER_METRICS.get() {
277 metrics.connection_fates_parked.increment();
278 }
279}
280
281pub(crate) fn connection_fate_unparked() {
284 if let Some(metrics) = SERVER_METRICS.get() {
285 metrics.connection_fates_parked.decrement();
286 }
287}
288
289pub fn publish_accepted() {
292 if let Some(metrics) = SERVER_METRICS.get() {
293 metrics.publishes_total.increment();
294 }
295}
296
297pub fn deliveries_recorded(count: u64) {
301 if count == 0 {
302 return;
303 }
304 if let Some(metrics) = SERVER_METRICS.get() {
305 metrics.deliveries_total.increment_by(count);
306 }
307}
308
309pub fn transport_deliveries_recorded(transport: MountKind, count: u64) {
315 if count == 0 {
316 return;
317 }
318 if let Some(metrics) = SERVER_METRICS.get() {
319 metrics.transport_deliveries[transport_slot(transport)].increment_by(count);
320 }
321}
322
323pub(crate) fn admission_refused(refusal: AdmissionRefusal) {
330 if let Some(metrics) = SERVER_METRICS.get() {
331 metrics.admission_refusals[refusal.slot()].increment();
332 }
333}
334
335pub(crate) fn handshake_refused(refusal: &UpgradeRefusal) {
338 if let Some(metrics) = SERVER_METRICS.get() {
339 metrics.handshake_refusals[refusal.slot()].increment();
340 }
341}
342
343pub(crate) fn self_stream_append_failed() {
344 if let Some(metrics) = SERVER_METRICS.get() {
345 metrics.self_stream_append_failures_total.increment();
346 }
347}
348
349pub(crate) fn self_stream_reserved_publication() {
350 if let Some(metrics) = SERVER_METRICS.get() {
351 metrics.self_streams_reserved_publications_total.increment();
352 }
353}
354
355pub(crate) fn self_streams_disabled() {
356 if let Some(metrics) = SERVER_METRICS.get() {
357 metrics.self_streams_disabled_total.increment();
358 }
359}
360
361pub(crate) fn console_failure(kind: ConsoleFailure, amount: u64) {
363 if let Some(metrics) = SERVER_METRICS.get() {
364 metrics.console_failures[kind.slot()].increment_by(amount);
365 }
366}
367
368pub(crate) fn console_streams_disabled() {
370 if let Some(metrics) = SERVER_METRICS.get() {
371 metrics.console_streams_disabled_total.increment();
372 }
373}
374
375pub(crate) fn self_stream_pressure_unmatched() {
376 if let Some(metrics) = SERVER_METRICS.get() {
377 metrics.self_streams_unmatched_pressure_total.increment();
378 }
379}
380
381pub(crate) fn self_stream_pressure_suppressed() {
382 if let Some(metrics) = SERVER_METRICS.get() {
383 metrics.self_streams_suppressed_pressure_total.increment();
384 }
385}
386
387pub(crate) fn pressure_refused() {
388 if let Some(metrics) = SERVER_METRICS.get() {
389 metrics.pressure_refused_total.increment();
390 }
391}
392
393pub(crate) fn advised() {
395 if let Some(metrics) = SERVER_METRICS.get() {
396 metrics.advised_total.increment();
397 }
398}
399
400pub(crate) fn pressure_ledger_evicted() {
401 if let Some(metrics) = SERVER_METRICS.get() {
402 metrics
403 .self_streams_pressure_ledger_evictions_total
404 .increment();
405 }
406}
407
408pub fn subscription_shed() {
415 if let Some(metrics) = SERVER_METRICS.get() {
416 metrics.sheds_total.increment();
417 }
418}
419
420pub(crate) fn receipt_entries_displaced(scope: ReceiptWindowScope, count: u64) {
428 if count == 0 {
429 return;
430 }
431 if let Some(metrics) = SERVER_METRICS.get() {
432 metrics.receipt_displacements[scope.slot()].increment_by(count);
433 }
434}
435
436pub(crate) fn receipt_pool_runaway_observed(pool: SharedReceiptPool) {
443 if let Some(metrics) = SERVER_METRICS.get() {
444 metrics.receipt_pool_runaway[pool.slot()].increment();
445 }
446}
447
448pub(crate) fn render_dedup_head_counters(output: &mut String) {
467 render_dedup_head_block(output, dedup_head_counts());
468}
469
470fn render_dedup_head_block(output: &mut String, counts: DedupHeadCounts) {
474 for (name, value) in counts.named() {
475 let value = value.to_string();
476 for piece in [
477 "# HELP ",
478 name,
479 " ",
480 name,
481 "\n# TYPE ",
482 name,
483 " counter\n",
484 name,
485 " ",
486 &value,
487 "\n",
488 ] {
489 output.push_str(piece);
490 }
491 }
492}
493
494#[cfg(test)]
502pub(crate) fn publishes_total_value() -> Option<u64> {
503 use liminal::metrics::MetricValue;
504
505 let registry = global_registry()?;
506 registry
507 .snapshot()
508 .metrics()
509 .iter()
510 .find(|metric| metric.name == PUBLISHES_TOTAL)
511 .and_then(|metric| match metric.value {
512 MetricValue::Counter(value) => Some(value),
513 MetricValue::Gauge(_) | MetricValue::Histogram(_) => None,
514 })
515}
516
517#[cfg(test)]
525pub(crate) fn receipt_displacements_value(scope: ReceiptWindowScope) -> Option<u64> {
526 labelled_counter_value(
527 RECEIPT_DISPLACEMENTS_TOTAL,
528 SCOPE_LABEL,
529 ReceiptWindowScope::LABELS[scope.slot()],
530 )
531}
532
533#[cfg(test)]
538pub(crate) fn receipt_pool_runaway_value(pool: SharedReceiptPool) -> Option<u64> {
539 labelled_counter_value(RECEIPT_POOL_RUNAWAY_TOTAL, POOL_LABEL, pool.label())
540}
541
542#[cfg(test)]
547pub(crate) fn connection_fates_parked_value() -> Option<i64> {
548 use liminal::metrics::MetricValue;
549
550 let registry = global_registry()?;
551 registry
552 .snapshot()
553 .metrics()
554 .iter()
555 .find(|metric| metric.name == CONNECTION_FATES_PARKED)
556 .and_then(|metric| match metric.value {
557 MetricValue::Gauge(value) => Some(value),
558 MetricValue::Counter(_) | MetricValue::Histogram(_) => None,
559 })
560}
561
562#[cfg(test)]
563fn labelled_counter_value(name: &str, key: &str, label: &str) -> Option<u64> {
564 use liminal::metrics::MetricValue;
565
566 let registry = global_registry()?;
567 registry
568 .snapshot()
569 .metrics()
570 .iter()
571 .find(|metric| {
572 metric.name == name
573 && metric
574 .labels
575 .iter()
576 .any(|(metric_key, value)| metric_key == key && value == label)
577 })
578 .and_then(|metric| match metric.value {
579 MetricValue::Counter(value) => Some(value),
580 MetricValue::Gauge(_) | MetricValue::Histogram(_) => None,
581 })
582}
583
584impl ServerMetrics {
585 fn register(registry: &MetricsRegistry) -> Option<Self> {
586 Some(Self {
587 connections_active: register_gauge(registry, CONNECTIONS_ACTIVE)?,
588 connection_fates_parked: register_gauge(registry, CONNECTION_FATES_PARKED)?,
589 publishes_total: register_counter(registry, PUBLISHES_TOTAL)?,
590 deliveries_total: register_counter(registry, DELIVERIES_TOTAL)?,
591 transport_deliveries: register_transport_deliveries(registry)?,
592 sheds_total: register_counter(registry, SHEDS_TOTAL)?,
593 self_stream_append_failures_total: register_counter(
594 registry,
595 SELF_STREAM_APPEND_FAILURES_TOTAL,
596 )?,
597 self_streams_disabled_total: register_counter(registry, SELF_STREAMS_DISABLED_TOTAL)?,
598 console_streams_disabled_total: register_counter(
599 registry,
600 CONSOLE_STREAMS_DISABLED_TOTAL,
601 )?,
602 console_failures: register_labelled(
603 registry,
604 CONSOLE_FAILURES_TOTAL,
605 KIND_LABEL,
606 &ConsoleFailure::LABELS,
607 )?,
608 self_streams_reserved_publications_total: register_counter(
609 registry,
610 SELF_STREAMS_RESERVED_PUBLICATIONS_TOTAL,
611 )?,
612 self_streams_unmatched_pressure_total: register_counter(
613 registry,
614 SELF_STREAMS_UNMATCHED_PRESSURE_TOTAL,
615 )?,
616 self_streams_suppressed_pressure_total: register_counter(
617 registry,
618 SELF_STREAMS_SUPPRESSED_PRESSURE_TOTAL,
619 )?,
620 self_streams_pressure_ledger_evictions_total: register_counter(
621 registry,
622 SELF_STREAMS_PRESSURE_LEDGER_EVICTIONS_TOTAL,
623 )?,
624 pressure_refused_total: register_counter(registry, PRESSURE_REFUSED_TOTAL)?,
625 advised_total: register_counter(registry, ADVISED_TOTAL)?,
626 admission_refusals: register_labelled(
627 registry,
628 ADMISSION_REFUSALS_TOTAL,
629 REASON_LABEL,
630 &AdmissionRefusal::LABELS,
631 )?,
632 handshake_refusals: register_labelled(
633 registry,
634 HANDSHAKE_REFUSALS_TOTAL,
635 REASON_LABEL,
636 &UpgradeRefusal::LABELS,
637 )?,
638 receipt_displacements: register_labelled(
639 registry,
640 RECEIPT_DISPLACEMENTS_TOTAL,
641 SCOPE_LABEL,
642 &ReceiptWindowScope::LABELS,
643 )?,
644 receipt_pool_runaway: register_labelled(
645 registry,
646 RECEIPT_POOL_RUNAWAY_TOTAL,
647 POOL_LABEL,
648 &SharedReceiptPool::LABELS,
649 )?,
650 })
651 }
652}
653
654fn register_counter(registry: &MetricsRegistry, name: &'static str) -> Option<CounterHandle> {
657 registry
658 .register_counter(name, no_labels())
659 .inspect_err(warn_registration_failure)
660 .ok()
661}
662
663fn register_gauge(registry: &MetricsRegistry, name: &'static str) -> Option<GaugeHandle> {
666 registry
667 .register_gauge(name, no_labels())
668 .inspect_err(warn_registration_failure)
669 .ok()
670}
671
672fn register_transport_deliveries(
673 registry: &MetricsRegistry,
674) -> Option<[CounterHandle; MOUNT_KINDS.len()]> {
675 let mut transport_deliveries = Vec::with_capacity(MOUNT_KINDS.len());
676 for transport in MOUNT_KINDS {
677 transport_deliveries.push(
678 registry
679 .register_counter(
680 TRANSPORT_DELIVERIES_TOTAL,
681 [(TRANSPORT_LABEL, transport.as_str())],
682 )
683 .inspect_err(warn_registration_failure)
684 .ok()?,
685 );
686 }
687 transport_deliveries.try_into().ok()
688}
689
690fn register_labelled<const N: usize>(
698 registry: &MetricsRegistry,
699 name: &'static str,
700 key: &'static str,
701 labels: &[&'static str; N],
702) -> Option<[CounterHandle; N]> {
703 let mut handles = Vec::with_capacity(N);
704 for label in labels {
705 handles.push(
706 registry
707 .register_counter(name, [(key, *label)])
708 .inspect_err(warn_registration_failure)
709 .ok()?,
710 );
711 }
712 handles.try_into().ok()
713}
714
715fn warn_registration_failure(error: &MetricRegistrationError) {
720 tracing::warn!(
721 %error,
722 "server metrics family registration failed; metrics recording stays disabled"
723 );
724}
725
726const fn no_labels() -> std::iter::Empty<(&'static str, &'static str)> {
727 std::iter::empty()
728}
729
730fn global_or_install() -> Option<&'static MetricsRegistry> {
735 if let Some(registry) = global_registry() {
736 return Some(registry);
737 }
738 let _ = install_global_registry(MetricsRegistry::new());
741 global_registry()
742}
743
744#[cfg(test)]
745mod tests {
746 use super::{
747 CONNECTIONS_ACTIVE, DELIVERIES_TOTAL, PUBLISHES_TOTAL, ServerMetrics, connection_spawned,
748 deliveries_recorded, init, no_labels, publish_accepted, render_dedup_head_block,
749 };
750 use liminal::durability::DedupHeadCounts;
751 use liminal::metrics::{MetricsRegistry, global_registry, render};
752
753 #[test]
757 fn the_dedup_head_block_renders_byte_for_byte() {
758 let mut output = String::new();
759 render_dedup_head_block(
760 &mut output,
761 DedupHeadCounts {
762 refusals: 7,
763 record_lost_races: 0,
764 repairs: 12,
765 },
766 );
767 assert_eq!(
768 output,
769 "# HELP liminal_dedup_head_refusals_total liminal_dedup_head_refusals_total\n\
770 # TYPE liminal_dedup_head_refusals_total counter\n\
771 liminal_dedup_head_refusals_total 7\n\
772 # HELP liminal_dedup_head_record_lost_races_total \
773 liminal_dedup_head_record_lost_races_total\n\
774 # TYPE liminal_dedup_head_record_lost_races_total counter\n\
775 liminal_dedup_head_record_lost_races_total 0\n\
776 # HELP liminal_dedup_head_repairs_total liminal_dedup_head_repairs_total\n\
777 # TYPE liminal_dedup_head_repairs_total counter\n\
778 liminal_dedup_head_repairs_total 12\n"
779 );
780 }
781
782 #[test]
783 fn init_registers_the_three_server_families_on_the_global_registry()
784 -> Result<(), Box<dyn std::error::Error>> {
785 init();
786 connection_spawned();
787 publish_accepted();
788 deliveries_recorded(2);
789
790 let registry =
791 global_registry().ok_or("init must install and enable the global registry")?;
792 let exposition = render(®istry.snapshot());
793
794 assert!(exposition.contains(CONNECTIONS_ACTIVE));
795 assert!(exposition.contains(PUBLISHES_TOTAL));
796 assert!(exposition.contains(DELIVERIES_TOTAL));
797
798 Ok(())
799 }
800
801 #[test]
804 fn register_warns_when_a_family_registration_fails() -> Result<(), Box<dyn std::error::Error>> {
805 let registry = MetricsRegistry::new();
806 registry.register_counter(CONNECTIONS_ACTIVE, no_labels())?;
809
810 let log = crate::test_log::CapturedLog::default();
811 let metrics = log.capture(|| ServerMetrics::register(®istry));
812
813 assert!(
814 metrics.is_none(),
815 "a failed registration must still yield None"
816 );
817 let output = log.contents();
818 assert!(
819 output.contains("WARN") && output.contains(CONNECTIONS_ACTIVE),
820 "expected a warn carrying the registration error, got: {output}"
821 );
822
823 Ok(())
824 }
825}