Skip to main content

armature_analytics/
metrics.rs

1//! Metrics types and helpers
2
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5
6/// Metric value types
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(untagged)]
9pub enum MetricValue {
10    Counter(u64),
11    Gauge(f64),
12    Histogram(HistogramValue),
13}
14
15/// Histogram metric value with buckets
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct HistogramValue {
18    pub count: u64,
19    pub sum: f64,
20    pub buckets: Vec<(f64, u64)>,
21}
22
23impl HistogramValue {
24    pub fn new() -> Self {
25        Self {
26            count: 0,
27            sum: 0.0,
28            buckets: vec![
29                (0.005, 0), // 5ms
30                (0.01, 0),  // 10ms
31                (0.025, 0), // 25ms
32                (0.05, 0),  // 50ms
33                (0.1, 0),   // 100ms
34                (0.25, 0),  // 250ms
35                (0.5, 0),   // 500ms
36                (1.0, 0),   // 1s
37                (2.5, 0),   // 2.5s
38                (5.0, 0),   // 5s
39                (10.0, 0),  // 10s
40                (f64::INFINITY, 0),
41            ],
42        }
43    }
44
45    pub fn observe(&mut self, value: f64) {
46        self.count += 1;
47        self.sum += value;
48
49        for (bound, count) in &mut self.buckets {
50            if value <= *bound {
51                *count += 1;
52            }
53        }
54    }
55}
56
57impl Default for HistogramValue {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63/// Named metric with labels
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct Metric {
66    pub name: String,
67    pub help: String,
68    pub metric_type: MetricType,
69    pub value: MetricValue,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub labels: Option<Vec<(String, String)>>,
72}
73
74impl Metric {
75    pub fn counter(name: impl Into<String>, help: impl Into<String>, value: u64) -> Self {
76        Self {
77            name: name.into(),
78            help: help.into(),
79            metric_type: MetricType::Counter,
80            value: MetricValue::Counter(value),
81            labels: None,
82        }
83    }
84
85    pub fn gauge(name: impl Into<String>, help: impl Into<String>, value: f64) -> Self {
86        Self {
87            name: name.into(),
88            help: help.into(),
89            metric_type: MetricType::Gauge,
90            value: MetricValue::Gauge(value),
91            labels: None,
92        }
93    }
94
95    pub fn histogram(
96        name: impl Into<String>,
97        help: impl Into<String>,
98        histogram: HistogramValue,
99    ) -> Self {
100        Self {
101            name: name.into(),
102            help: help.into(),
103            metric_type: MetricType::Histogram,
104            value: MetricValue::Histogram(histogram),
105            labels: None,
106        }
107    }
108
109    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
110        let labels = self.labels.get_or_insert_with(Vec::new);
111        labels.push((key.into(), value.into()));
112        self
113    }
114
115    pub fn with_labels(mut self, labels: Vec<(String, String)>) -> Self {
116        self.labels = Some(labels);
117        self
118    }
119}
120
121/// Metric types
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "lowercase")]
124pub enum MetricType {
125    Counter,
126    Gauge,
127    Histogram,
128    Summary,
129}
130
131/// Time series data point
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct TimeSeriesPoint {
134    pub timestamp: i64,
135    pub value: f64,
136}
137
138/// Time series data
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct TimeSeries {
141    pub name: String,
142    pub points: Vec<TimeSeriesPoint>,
143}
144
145impl TimeSeries {
146    pub fn new(name: impl Into<String>) -> Self {
147        Self {
148            name: name.into(),
149            points: Vec::new(),
150        }
151    }
152
153    pub fn add_point(&mut self, timestamp: i64, value: f64) {
154        self.points.push(TimeSeriesPoint { timestamp, value });
155    }
156}
157
158/// Duration formatting helpers
159pub trait DurationExt {
160    fn as_millis_f64(&self) -> f64;
161    fn as_micros_f64(&self) -> f64;
162}
163
164impl DurationExt for Duration {
165    fn as_millis_f64(&self) -> f64 {
166        self.as_secs_f64() * 1000.0
167    }
168
169    fn as_micros_f64(&self) -> f64 {
170        self.as_secs_f64() * 1_000_000.0
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_histogram() {
180        let mut hist = HistogramValue::new();
181        hist.observe(0.001); // 1ms
182        hist.observe(0.050); // 50ms
183        hist.observe(0.500); // 500ms
184
185        assert_eq!(hist.count, 3);
186    }
187
188    #[test]
189    fn test_metric_with_labels() {
190        let metric = Metric::counter("http_requests_total", "Total HTTP requests", 100)
191            .with_label("method", "GET")
192            .with_label("status", "200");
193
194        assert_eq!(metric.labels.as_ref().unwrap().len(), 2);
195    }
196
197    #[test]
198    #[allow(unstable_name_collisions)]
199    fn test_duration_ext() {
200        let duration = Duration::from_millis(150);
201        assert_eq!(DurationExt::as_millis_f64(&duration), 150.0);
202    }
203}