Skip to main content

appcore_ops/
observation_metrics.rs

1//! Low-cardinality metrics derived from runtime observations.
2
3use crate::{
4    InMemoryMetrics, ObservationEvent, ObservationKind, ObservationSeverity, ObservationSink,
5};
6use std::sync::Arc;
7
8/// Observation drain that records stable monotonic counters.
9#[derive(Debug, Clone)]
10pub struct ObservationMetricsSink {
11    metrics: Arc<InMemoryMetrics>,
12}
13
14impl ObservationMetricsSink {
15    /// Creates a drain backed by the provided process-local registry.
16    pub fn new(metrics: Arc<InMemoryMetrics>) -> Self {
17        Self { metrics }
18    }
19
20    /// Returns the shared metrics registry.
21    pub fn metrics(&self) -> Arc<InMemoryMetrics> {
22        Arc::clone(&self.metrics)
23    }
24}
25
26impl ObservationSink for ObservationMetricsSink {
27    fn emit(&self, event: ObservationEvent) {
28        let _ = self.metrics.increment("appcore.observations.total");
29        let _ = self.metrics.increment(kind_metric(event.kind));
30        let _ = self.metrics.increment(severity_metric(event.severity));
31    }
32}
33
34fn kind_metric(kind: ObservationKind) -> &'static str {
35    match kind {
36        ObservationKind::Lifecycle => "appcore.observations.kind.lifecycle",
37        ObservationKind::Configuration => "appcore.observations.kind.configuration",
38        ObservationKind::Health => "appcore.observations.kind.health",
39        ObservationKind::Security => "appcore.observations.kind.security",
40        ObservationKind::Storage => "appcore.observations.kind.storage",
41        ObservationKind::ControlPlane => "appcore.observations.kind.control_plane",
42        ObservationKind::PeerRpc => "appcore.observations.kind.peer_rpc",
43        ObservationKind::Scheduler => "appcore.observations.kind.scheduler",
44        ObservationKind::Sync => "appcore.observations.kind.sync",
45        ObservationKind::Audit => "appcore.observations.kind.audit",
46        ObservationKind::Diagnostic => "appcore.observations.kind.diagnostic",
47    }
48}
49
50fn severity_metric(severity: ObservationSeverity) -> &'static str {
51    match severity {
52        ObservationSeverity::Debug => "appcore.observations.severity.debug",
53        ObservationSeverity::Info => "appcore.observations.severity.info",
54        ObservationSeverity::Warning => "appcore.observations.severity.warning",
55        ObservationSeverity::Error => "appcore.observations.severity.error",
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn records_bounded_kind_and_severity_dimensions() {
65        let metrics = Arc::new(InMemoryMetrics::new());
66        let sink = ObservationMetricsSink::new(Arc::clone(&metrics));
67        sink.emit(ObservationEvent::new(
68            ObservationKind::Storage,
69            ObservationSeverity::Warning,
70            "untrusted.dynamic.name",
71            1,
72        ));
73
74        let snapshot = metrics.snapshot();
75        assert!(snapshot
76            .iter()
77            .any(|metric| metric.name == "appcore.observations.total" && metric.value == 1));
78        assert!(snapshot.iter().any(|metric| {
79            metric.name == "appcore.observations.kind.storage" && metric.value == 1
80        }));
81        assert!(snapshot.iter().any(|metric| {
82            metric.name == "appcore.observations.severity.warning" && metric.value == 1
83        }));
84        assert_eq!(snapshot.len(), 3);
85    }
86}