Skip to main content

sh_layer1/observability/
metrics.rs

1//! Metrics types for observability.
2
3use parking_lot::RwLock;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7/// Internal metric value storage.
8#[derive(Debug, Clone)]
9pub enum MetricValue {
10    Counter(u64),
11    Gauge(f64),
12    Histogram(Vec<f64>),
13}
14
15impl MetricValue {
16    /// Get the counter value, or 0 if not a counter.
17    pub fn as_counter(&self) -> u64 {
18        match self {
19            MetricValue::Counter(v) => *v,
20            _ => 0,
21        }
22    }
23
24    /// Get the gauge value, or 0.0 if not a gauge.
25    pub fn as_gauge(&self) -> f64 {
26        match self {
27            MetricValue::Gauge(v) => *v,
28            _ => 0.0,
29        }
30    }
31
32    /// Get the histogram values, or empty if not a histogram.
33    pub fn as_histogram(&self) -> &[f64] {
34        match self {
35            MetricValue::Histogram(v) => v,
36            _ => &[],
37        }
38    }
39}
40
41/// Internal metrics storage shared by Counter, Histogram, Gauge.
42#[derive(Debug, Default)]
43pub struct MetricsStorage {
44    metrics: RwLock<HashMap<String, MetricValue>>,
45}
46
47impl MetricsStorage {
48    /// Create a new metrics storage.
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Increment a counter metric.
54    pub fn increment_counter(&self, name: &str, delta: u64) {
55        let mut metrics = self.metrics.write();
56        let entry = metrics
57            .entry(name.to_string())
58            .or_insert(MetricValue::Counter(0));
59        if let MetricValue::Counter(v) = entry {
60            *v += delta;
61        }
62    }
63
64    /// Set a gauge metric.
65    pub fn set_gauge(&self, name: &str, value: f64) {
66        let mut metrics = self.metrics.write();
67        metrics.insert(name.to_string(), MetricValue::Gauge(value));
68    }
69
70    /// Record a histogram value.
71    pub fn record_histogram(&self, name: &str, value: f64) {
72        let mut metrics = self.metrics.write();
73        let entry = metrics
74            .entry(name.to_string())
75            .or_insert(MetricValue::Histogram(Vec::new()));
76        if let MetricValue::Histogram(v) = entry {
77            v.push(value);
78        }
79    }
80
81    /// Get a metric value by name.
82    pub fn get(&self, name: &str) -> Option<MetricValue> {
83        self.metrics.read().get(name).cloned()
84    }
85
86    /// List all metric names.
87    pub fn list_names(&self) -> Vec<String> {
88        self.metrics.read().keys().cloned().collect()
89    }
90}
91
92/// Counter metric.
93#[derive(Debug, Clone)]
94pub struct Counter {
95    name: String,
96    storage: Arc<MetricsStorage>,
97}
98
99impl Counter {
100    /// Create a new counter.
101    pub fn new(name: impl Into<String>, storage: Arc<MetricsStorage>) -> Self {
102        Self {
103            name: name.into(),
104            storage,
105        }
106    }
107
108    /// Increment the counter by the given delta.
109    #[allow(clippy::let_unit_value)]
110    pub fn increment(&self, delta: u64) {
111        #[cfg(feature = "prometheus")]
112        {
113            let _ = metrics::counter!(self.name.clone()).increment(delta);
114        }
115
116        #[cfg(not(feature = "prometheus"))]
117        {
118            self.storage.increment_counter(&self.name, delta);
119        }
120    }
121
122    /// Get the current counter value.
123    pub fn get(&self) -> u64 {
124        self.storage
125            .get(&self.name)
126            .map(|v| v.as_counter())
127            .unwrap_or(0)
128    }
129}
130
131/// Histogram metric.
132#[derive(Debug, Clone)]
133pub struct Histogram {
134    name: String,
135    storage: Arc<MetricsStorage>,
136}
137
138impl Histogram {
139    /// Create a new histogram.
140    pub fn new(name: impl Into<String>, storage: Arc<MetricsStorage>) -> Self {
141        Self {
142            name: name.into(),
143            storage,
144        }
145    }
146
147    /// Record a value in the histogram.
148    #[allow(clippy::let_unit_value)]
149    pub fn record(&self, value: f64) {
150        #[cfg(feature = "prometheus")]
151        {
152            let _ = metrics::histogram!(self.name.clone()).record(value);
153        }
154
155        #[cfg(not(feature = "prometheus"))]
156        {
157            self.storage.record_histogram(&self.name, value);
158        }
159    }
160
161    /// Get all recorded values.
162    pub fn get_values(&self) -> Vec<f64> {
163        self.storage
164            .get(&self.name)
165            .map(|v| v.as_histogram().to_vec())
166            .unwrap_or_default()
167    }
168}
169
170/// Gauge metric.
171#[derive(Debug, Clone)]
172pub struct Gauge {
173    name: String,
174    storage: Arc<MetricsStorage>,
175}
176
177impl Gauge {
178    /// Create a new gauge.
179    pub fn new(name: impl Into<String>, storage: Arc<MetricsStorage>) -> Self {
180        Self {
181            name: name.into(),
182            storage,
183        }
184    }
185
186    /// Set the gauge to a value.
187    #[allow(clippy::let_unit_value)]
188    pub fn set(&self, value: f64) {
189        #[cfg(feature = "prometheus")]
190        {
191            let _ = metrics::gauge!(self.name.clone()).set(value);
192        }
193
194        #[cfg(not(feature = "prometheus"))]
195        {
196            self.storage.set_gauge(&self.name, value);
197        }
198    }
199
200    /// Get the current gauge value.
201    pub fn get(&self) -> f64 {
202        self.storage
203            .get(&self.name)
204            .map(|v| v.as_gauge())
205            .unwrap_or(0.0)
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_counter_increment() {
215        let storage = Arc::new(MetricsStorage::new());
216        let counter = Counter::new("test_counter", storage);
217
218        counter.increment(5);
219        assert_eq!(counter.get(), 5);
220
221        counter.increment(3);
222        assert_eq!(counter.get(), 8);
223    }
224
225    #[test]
226    fn test_gauge_set() {
227        let storage = Arc::new(MetricsStorage::new());
228        let gauge = Gauge::new("test_gauge", storage);
229
230        gauge.set(42.0);
231        assert_eq!(gauge.get(), 42.0);
232
233        gauge.set(100.0);
234        assert_eq!(gauge.get(), 100.0);
235    }
236
237    #[test]
238    fn test_histogram_record() {
239        let storage = Arc::new(MetricsStorage::new());
240        let histogram = Histogram::new("test_histogram", storage);
241
242        histogram.record(1.0);
243        histogram.record(2.0);
244        histogram.record(3.0);
245
246        let values = histogram.get_values();
247        assert_eq!(values, vec![1.0, 2.0, 3.0]);
248    }
249
250    #[test]
251    fn test_metrics_storage_list_names() {
252        let storage = Arc::new(MetricsStorage::new());
253        let counter = Counter::new("counter1", Arc::clone(&storage));
254        let gauge = Gauge::new("gauge1", Arc::clone(&storage));
255
256        counter.increment(1);
257        gauge.set(1.0);
258
259        let names = storage.list_names();
260        assert_eq!(names.len(), 2);
261        assert!(names.contains(&"counter1".to_string()));
262        assert!(names.contains(&"gauge1".to_string()));
263    }
264}