1use std::fmt::{Debug, Display};
18use std::hash::Hash;
19use std::time::Instant;
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct MetricId {
27 pub namespace: String,
29 pub name: String,
31 pub labels: Vec<(String, String)>,
33}
34
35impl MetricId {
36 pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
38 Self {
39 namespace: namespace.into(),
40 name: name.into(),
41 labels: Vec::new(),
42 }
43 }
44
45 pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
48 self.labels.push((key.into(), value.into()));
49 self.labels.sort_by(|a, b| a.0.cmp(&b.0));
50 self
51 }
52
53 pub fn to_string_formatted(&self) -> String {
55 if self.labels.is_empty() {
56 format!("{}:{}", self.namespace, self.name)
57 } else {
58 let labels_str = self
59 .labels
60 .iter()
61 .map(|(k, v)| format!("{k}={v}"))
62 .collect::<Vec<_>>()
63 .join(",");
64 format!("{}:{}[{}]", self.namespace, self.name, labels_str)
65 }
66 }
67}
68
69impl Display for MetricId {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 write!(f, "{}", self.to_string_formatted())
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum MetricType {
78 Counter,
80 Gauge,
82 Histogram,
84}
85
86#[derive(Debug, Clone)]
88pub enum MetricValue {
89 Counter(u64),
91 Gauge(f64),
93 Histogram {
95 samples: Vec<f64>,
97 bucket_bounds: Vec<f64>,
99 bucket_counts: Vec<u64>,
101 },
102}
103
104impl MetricValue {
105 pub fn metric_type(&self) -> MetricType {
107 match self {
108 MetricValue::Counter(_) => MetricType::Counter,
109 MetricValue::Gauge(_) => MetricType::Gauge,
110 MetricValue::Histogram { .. } => MetricType::Histogram,
111 }
112 }
113
114 pub fn as_f64(&self) -> Option<f64> {
116 match self {
117 MetricValue::Counter(v) => Some(*v as f64),
118 MetricValue::Gauge(v) => Some(*v),
119 MetricValue::Histogram { .. } => None,
120 }
121 }
122
123 pub fn as_counter(&self) -> Option<u64> {
125 match self {
126 MetricValue::Counter(v) => Some(*v),
127 _ => None,
128 }
129 }
130
131 pub fn as_gauge(&self) -> Option<f64> {
133 match self {
134 MetricValue::Gauge(v) => Some(*v),
135 _ => None,
136 }
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct MetricMetadata {
143 pub id: MetricId,
145 pub metric_type: MetricType,
147 pub description: String,
149 pub unit: String,
151 pub created_at: Instant,
153 pub last_updated: Instant,
155}
156
157impl MetricMetadata {
158 pub fn new(
160 id: MetricId,
161 metric_type: MetricType,
162 description: impl Into<String>,
163 unit: impl Into<String>,
164 ) -> Self {
165 let now = Instant::now();
166 Self {
167 id,
168 metric_type,
169 description: description.into(),
170 unit: unit.into(),
171 created_at: now,
172 last_updated: now,
173 }
174 }
175
176 pub fn update_timestamp(&mut self) {
178 self.last_updated = Instant::now();
179 }
180}
181
182#[derive(Debug, Clone)]
184pub struct Metric {
185 pub metadata: MetricMetadata,
187 pub value: MetricValue,
189}
190
191impl Metric {
192 pub fn new_counter(id: MetricId, description: impl Into<String>, initial_value: u64) -> Self {
194 Self {
195 metadata: MetricMetadata::new(id, MetricType::Counter, description, "count"),
196 value: MetricValue::Counter(initial_value),
197 }
198 }
199
200 pub fn new_gauge(
202 id: MetricId,
203 description: impl Into<String>,
204 unit: impl Into<String>,
205 initial_value: f64,
206 ) -> Self {
207 Self {
208 metadata: MetricMetadata::new(id, MetricType::Gauge, description, unit),
209 value: MetricValue::Gauge(initial_value),
210 }
211 }
212
213 pub fn new_histogram(
215 id: MetricId,
216 description: impl Into<String>,
217 unit: impl Into<String>,
218 bucket_bounds: Vec<f64>,
219 ) -> Self {
220 let bucket_counts = vec![0; bucket_bounds.len()];
221 Self {
222 metadata: MetricMetadata::new(id, MetricType::Histogram, description, unit),
223 value: MetricValue::Histogram {
224 samples: Vec::new(),
225 bucket_bounds,
226 bucket_counts,
227 },
228 }
229 }
230}
231
232pub type MetricsResult<T> = Result<T, MetricsError>;
234
235#[derive(Debug, Clone)]
237pub enum MetricsError {
238 MetricNotFound(MetricId),
240 TypeMismatch {
243 expected: MetricType,
245 found: MetricType,
247 },
248 StorageError(String),
250 InvalidOperation(String),
252}
253
254impl Display for MetricsError {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 match self {
257 MetricsError::MetricNotFound(id) => write!(f, "Metric not found: {id}"),
258 MetricsError::TypeMismatch { expected, found } => {
259 write!(f, "Type mismatch: expected {expected:?}, found {found:?}")
260 }
261 MetricsError::StorageError(msg) => write!(f, "Storage error: {msg}"),
262 MetricsError::InvalidOperation(msg) => write!(f, "Invalid operation: {msg}"),
263 }
264 }
265}
266
267impl std::error::Error for MetricsError {}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_metric_id_creation() {
275 let id = MetricId::new("engine", "frame_time")
276 .with_label("gpu", "nvidia")
277 .with_label("quality", "high");
278
279 assert_eq!(id.namespace, "engine");
280 assert_eq!(id.name, "frame_time");
281 assert_eq!(id.labels.len(), 2);
282
283 assert_eq!(id.labels[0], ("gpu".to_string(), "nvidia".to_string()));
285 assert_eq!(id.labels[1], ("quality".to_string(), "high".to_string()));
286 }
287
288 #[test]
289 fn test_metric_id_formatting() {
290 let id1 = MetricId::new("engine", "frame_time");
291 assert_eq!(id1.to_string_formatted(), "engine:frame_time");
292
293 let id2 = MetricId::new("renderer", "triangles")
294 .with_label("pass", "main")
295 .with_label("quality", "high");
296 assert_eq!(
297 id2.to_string_formatted(),
298 "renderer:triangles[pass=main,quality=high]"
299 );
300 }
301
302 #[test]
303 fn test_metric_value_types() {
304 let counter = MetricValue::Counter(42);
305 assert_eq!(counter.metric_type(), MetricType::Counter);
306 assert_eq!(counter.as_counter(), Some(42));
307 assert_eq!(counter.as_f64(), Some(42.0));
308
309 use crate::math::PI;
310
311 let gauge = MetricValue::Gauge(PI as f64);
312 assert_eq!(gauge.metric_type(), MetricType::Gauge);
313 assert_eq!(gauge.as_gauge(), Some(PI as f64));
314 assert_eq!(gauge.as_f64(), Some(PI as f64));
315 }
316
317 #[test]
318 fn test_metric_creation() {
319 let id = MetricId::new("test", "counter");
320 let metric = Metric::new_counter(id.clone(), "Test counter", 0);
321
322 assert_eq!(metric.metadata.id, id);
323 assert_eq!(metric.metadata.metric_type, MetricType::Counter);
324 assert_eq!(metric.metadata.unit, "count");
325 assert_eq!(metric.value.as_counter(), Some(0));
326 }
327}