Skip to main content

obs_core/
gauge.rs

1use core_types::{HealthStatus, MetricsProvider, MetricsSnapshot};
2
3/// A simple in-memory time series: stores the last `capacity` snapshots for
4/// a single metric name.
5pub struct MetricTimeSeries {
6    name: String,
7    capacity: usize,
8    samples: std::collections::VecDeque<f64>,
9}
10
11impl MetricTimeSeries {
12    pub fn new(name: impl Into<String>, capacity: usize) -> Self {
13        Self {
14            name: name.into(),
15            capacity: capacity.max(1),
16            samples: std::collections::VecDeque::new(),
17        }
18    }
19
20    pub fn push(&mut self, value: f64) {
21        if self.samples.len() >= self.capacity {
22            self.samples.pop_front();
23        }
24        self.samples.push_back(value);
25    }
26
27    pub fn name(&self) -> &str {
28        &self.name
29    }
30    pub fn len(&self) -> usize {
31        self.samples.len()
32    }
33    pub fn is_empty(&self) -> bool {
34        self.samples.is_empty()
35    }
36    pub fn last(&self) -> Option<f64> {
37        self.samples.back().copied()
38    }
39
40    pub fn avg(&self) -> Option<f64> {
41        if self.samples.is_empty() {
42            return None;
43        }
44        Some(self.samples.iter().sum::<f64>() / self.samples.len() as f64)
45    }
46
47    pub fn max(&self) -> Option<f64> {
48        self.samples.iter().cloned().reduce(f64::max)
49    }
50
51    pub fn min(&self) -> Option<f64> {
52        self.samples.iter().cloned().reduce(f64::min)
53    }
54}
55
56/// A `MetricsProvider` that wraps a single `MetricTimeSeries` and a threshold
57/// for degraded/unhealthy detection.
58///
59/// Useful for tracking bounded scalar metrics (queue depth, CPU%, latency ms).
60pub struct ThresholdGauge {
61    series: MetricTimeSeries,
62    unit: &'static str,
63    warn_threshold: f64,
64    crit_threshold: f64,
65}
66
67impl ThresholdGauge {
68    /// Create a gauge.  `warn` is the degraded threshold, `crit` is unhealthy.
69    pub fn new(name: impl Into<String>, unit: &'static str, warn: f64, crit: f64) -> Self {
70        Self {
71            series: MetricTimeSeries::new(name, 60),
72            unit,
73            warn_threshold: warn,
74            crit_threshold: crit,
75        }
76    }
77
78    pub fn push(&mut self, value: f64) {
79        self.series.push(value);
80    }
81}
82
83impl MetricsProvider for ThresholdGauge {
84    fn collect(&self) -> Vec<MetricsSnapshot> {
85        let mut out = Vec::new();
86        if let Some(v) = self.series.last() {
87            out.push(MetricsSnapshot::gauge(
88                self.series.name().to_string(),
89                v,
90                self.unit,
91            ));
92        }
93        if let Some(avg) = self.series.avg() {
94            out.push(MetricsSnapshot::gauge(
95                format!("{}.avg", self.series.name()),
96                avg,
97                self.unit,
98            ));
99        }
100        out
101    }
102
103    fn health(&self) -> HealthStatus {
104        match self.series.last() {
105            None => HealthStatus::Healthy,
106            Some(v) if v >= self.crit_threshold => HealthStatus::Unhealthy {
107                reason: format!(
108                    "{} = {:.2} >= crit {:.2}",
109                    self.series.name(),
110                    v,
111                    self.crit_threshold
112                ),
113            },
114            Some(v) if v >= self.warn_threshold => HealthStatus::Degraded {
115                reason: format!(
116                    "{} = {:.2} >= warn {:.2}",
117                    self.series.name(),
118                    v,
119                    self.warn_threshold
120                ),
121            },
122            _ => HealthStatus::Healthy,
123        }
124    }
125}