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(name: impl Into<String>, help: impl Into<String>, histogram: HistogramValue) -> Self {
96        Self {
97            name: name.into(),
98            help: help.into(),
99            metric_type: MetricType::Histogram,
100            value: MetricValue::Histogram(histogram),
101            labels: None,
102        }
103    }
104
105    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
106        let labels = self.labels.get_or_insert_with(Vec::new);
107        labels.push((key.into(), value.into()));
108        self
109    }
110
111    pub fn with_labels(mut self, labels: Vec<(String, String)>) -> Self {
112        self.labels = Some(labels);
113        self
114    }
115}
116
117/// Metric types
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "lowercase")]
120pub enum MetricType {
121    Counter,
122    Gauge,
123    Histogram,
124    Summary,
125}
126
127/// Time series data point
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct TimeSeriesPoint {
130    pub timestamp: i64,
131    pub value: f64,
132}
133
134/// Time series data
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct TimeSeries {
137    pub name: String,
138    pub points: Vec<TimeSeriesPoint>,
139}
140
141impl TimeSeries {
142    pub fn new(name: impl Into<String>) -> Self {
143        Self {
144            name: name.into(),
145            points: Vec::new(),
146        }
147    }
148
149    pub fn add_point(&mut self, timestamp: i64, value: f64) {
150        self.points.push(TimeSeriesPoint { timestamp, value });
151    }
152}
153
154/// Duration formatting helpers
155pub trait DurationExt {
156    fn as_millis_f64(&self) -> f64;
157    fn as_micros_f64(&self) -> f64;
158}
159
160impl DurationExt for Duration {
161    fn as_millis_f64(&self) -> f64 {
162        self.as_secs_f64() * 1000.0
163    }
164
165    fn as_micros_f64(&self) -> f64 {
166        self.as_secs_f64() * 1_000_000.0
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_histogram() {
176        let mut hist = HistogramValue::new();
177        hist.observe(0.001); // 1ms
178        hist.observe(0.050); // 50ms
179        hist.observe(0.500); // 500ms
180
181        assert_eq!(hist.count, 3);
182    }
183
184    #[test]
185    fn test_metric_with_labels() {
186        let metric = Metric::counter("http_requests_total", "Total HTTP requests", 100)
187            .with_label("method", "GET")
188            .with_label("status", "200");
189
190        assert_eq!(metric.labels.as_ref().unwrap().len(), 2);
191    }
192
193    #[test]
194    fn test_duration_ext() {
195        let duration = Duration::from_millis(150);
196        assert_eq!(duration.as_millis_f64(), 150.0);
197    }
198}
199