forge_core/observability/
metric.rs1use std::collections::HashMap;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum MetricKind {
10 Counter,
12 Gauge,
14 Histogram,
16 Summary,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ParseMetricKindError(pub String);
23
24impl std::fmt::Display for ParseMetricKindError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "invalid metric kind: {}", self.0)
27 }
28}
29
30impl std::error::Error for ParseMetricKindError {}
31
32impl FromStr for MetricKind {
33 type Err = ParseMetricKindError;
34
35 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
36 match s.to_lowercase().as_str() {
37 "counter" => Ok(Self::Counter),
38 "gauge" => Ok(Self::Gauge),
39 "histogram" => Ok(Self::Histogram),
40 "summary" => Ok(Self::Summary),
41 _ => Err(ParseMetricKindError(s.to_string())),
42 }
43 }
44}
45
46impl std::fmt::Display for MetricKind {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Counter => write!(f, "counter"),
50 Self::Gauge => write!(f, "gauge"),
51 Self::Histogram => write!(f, "histogram"),
52 Self::Summary => write!(f, "summary"),
53 }
54 }
55}
56
57pub type MetricLabels = HashMap<String, String>;
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum MetricValue {
63 Value(f64),
65 Histogram {
67 buckets: Vec<(f64, u64)>,
68 count: u64,
69 sum: f64,
70 },
71 Summary {
73 quantiles: Vec<(f64, f64)>,
74 count: u64,
75 sum: f64,
76 },
77}
78
79impl MetricValue {
80 pub fn value(v: f64) -> Self {
82 Self::Value(v)
83 }
84
85 pub fn histogram(buckets: Vec<(f64, u64)>, count: u64, sum: f64) -> Self {
87 Self::Histogram {
88 buckets,
89 count,
90 sum,
91 }
92 }
93
94 pub fn summary(quantiles: Vec<(f64, f64)>, count: u64, sum: f64) -> Self {
96 Self::Summary {
97 quantiles,
98 count,
99 sum,
100 }
101 }
102
103 pub fn as_value(&self) -> Option<f64> {
105 match self {
106 Self::Value(v) => Some(*v),
107 _ => None,
108 }
109 }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Metric {
115 pub name: String,
117 pub kind: MetricKind,
119 pub labels: MetricLabels,
121 pub value: MetricValue,
123 pub timestamp: chrono::DateTime<chrono::Utc>,
125 pub description: Option<String>,
127}
128
129impl Metric {
130 pub fn counter(name: impl Into<String>, value: f64) -> Self {
132 Self {
133 name: name.into(),
134 kind: MetricKind::Counter,
135 labels: HashMap::new(),
136 value: MetricValue::Value(value),
137 timestamp: chrono::Utc::now(),
138 description: None,
139 }
140 }
141
142 pub fn gauge(name: impl Into<String>, value: f64) -> Self {
144 Self {
145 name: name.into(),
146 kind: MetricKind::Gauge,
147 labels: HashMap::new(),
148 value: MetricValue::Value(value),
149 timestamp: chrono::Utc::now(),
150 description: None,
151 }
152 }
153
154 pub fn histogram(
156 name: impl Into<String>,
157 buckets: Vec<(f64, u64)>,
158 count: u64,
159 sum: f64,
160 ) -> Self {
161 Self {
162 name: name.into(),
163 kind: MetricKind::Histogram,
164 labels: HashMap::new(),
165 value: MetricValue::Histogram {
166 buckets,
167 count,
168 sum,
169 },
170 timestamp: chrono::Utc::now(),
171 description: None,
172 }
173 }
174
175 pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
177 self.labels.insert(key.into(), value.into());
178 self
179 }
180
181 pub fn with_labels(mut self, labels: MetricLabels) -> Self {
183 self.labels.extend(labels);
184 self
185 }
186
187 pub fn with_description(mut self, description: impl Into<String>) -> Self {
189 self.description = Some(description.into());
190 self
191 }
192
193 pub fn with_timestamp(mut self, timestamp: chrono::DateTime<chrono::Utc>) -> Self {
195 self.timestamp = timestamp;
196 self
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_metric_kind_from_str() {
206 assert_eq!("counter".parse::<MetricKind>(), Ok(MetricKind::Counter));
207 assert_eq!("GAUGE".parse::<MetricKind>(), Ok(MetricKind::Gauge));
208 assert_eq!("Histogram".parse::<MetricKind>(), Ok(MetricKind::Histogram));
209 assert!("unknown".parse::<MetricKind>().is_err());
210 }
211
212 #[test]
213 fn test_counter_metric() {
214 let metric = Metric::counter("http_requests_total", 100.0)
215 .with_label("method", "GET")
216 .with_label("status", "200");
217
218 assert_eq!(metric.name, "http_requests_total");
219 assert_eq!(metric.kind, MetricKind::Counter);
220 assert_eq!(metric.labels.get("method"), Some(&"GET".to_string()));
221 assert_eq!(metric.value.as_value(), Some(100.0));
222 }
223
224 #[test]
225 fn test_gauge_metric() {
226 let metric = Metric::gauge("active_connections", 42.0);
227 assert_eq!(metric.kind, MetricKind::Gauge);
228 assert_eq!(metric.value.as_value(), Some(42.0));
229 }
230
231 #[test]
232 fn test_histogram_metric() {
233 let buckets = vec![(0.1, 10), (0.5, 50), (1.0, 80), (5.0, 100)];
234 let metric = Metric::histogram("request_duration", buckets.clone(), 100, 45.5);
235
236 assert_eq!(metric.kind, MetricKind::Histogram);
237 if let MetricValue::Histogram {
238 buckets: b,
239 count,
240 sum,
241 } = &metric.value
242 {
243 assert_eq!(b, &buckets);
244 assert_eq!(*count, 100);
245 assert_eq!(*sum, 45.5);
246 } else {
247 panic!("Expected histogram value");
248 }
249 }
250}