1use serde::{Deserialize, Serialize};
4use std::time::Duration;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(untagged)]
9pub enum MetricValue {
10 Counter(u64),
11 Gauge(f64),
12 Histogram(HistogramValue),
13}
14
15#[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), (0.01, 0), (0.025, 0), (0.05, 0), (0.1, 0), (0.25, 0), (0.5, 0), (1.0, 0), (2.5, 0), (5.0, 0), (10.0, 0), (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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct TimeSeriesPoint {
134 pub timestamp: i64,
135 pub value: f64,
136}
137
138#[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
158pub 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); hist.observe(0.050); hist.observe(0.500); 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}