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