1use core_types::{HealthStatus, MetricsProvider, MetricsSnapshot};
2
3pub 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
56pub struct ThresholdGauge {
61 series: MetricTimeSeries,
62 unit: &'static str,
63 warn_threshold: f64,
64 crit_threshold: f64,
65}
66
67impl ThresholdGauge {
68 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}