Skip to main content

appcore_ops/
observation_metrics.rs

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