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    pub fn increment(&self, delta: u64) {
110        #[cfg(feature = "prometheus")]
111        {
112            let _ = metrics::counter!(self.name.clone()).increment(delta);
113        }
114
115        #[cfg(not(feature = "prometheus"))]
116        {
117            self.storage.increment_counter(&self.name, delta);
118        }
119    }
120
121    /// Get the current counter value.
122    pub fn get(&self) -> u64 {
123        self.storage
124            .get(&self.name)
125            .map(|v| v.as_counter())
126            .unwrap_or(0)
127    }
128}
129
130/// Histogram metric.
131#[derive(Debug, Clone)]
132pub struct Histogram {
133    name: String,
134    storage: Arc<MetricsStorage>,
135}
136
137impl Histogram {
138    /// Create a new histogram.
139    pub fn new(name: impl Into<String>, storage: Arc<MetricsStorage>) -> Self {
140        Self {
141            name: name.into(),
142            storage,
143        }
144    }
145
146    /// Record a value in the histogram.
147    pub fn record(&self, value: f64) {
148        #[cfg(feature = "prometheus")]
149        {
150            let _ = metrics::histogram!(self.name.clone()).record(value);
151        }
152
153        #[cfg(not(feature = "prometheus"))]
154        {
155            self.storage.record_histogram(&self.name, value);
156        }
157    }
158
159    /// Get all recorded values.
160    pub fn get_values(&self) -> Vec<f64> {
161        self.storage
162            .get(&self.name)
163            .map(|v| v.as_histogram().to_vec())
164            .unwrap_or_default()
165    }
166}
167
168/// Gauge metric.
169#[derive(Debug, Clone)]
170pub struct Gauge {
171    name: String,
172    storage: Arc<MetricsStorage>,
173}
174
175impl Gauge {
176    /// Create a new gauge.
177    pub fn new(name: impl Into<String>, storage: Arc<MetricsStorage>) -> Self {
178        Self {
179            name: name.into(),
180            storage,
181        }
182    }
183
184    /// Set the gauge to a value.
185    pub fn set(&self, value: f64) {
186        #[cfg(feature = "prometheus")]
187        {
188            let _ = metrics::gauge!(self.name.clone()).set(value);
189        }
190
191        #[cfg(not(feature = "prometheus"))]
192        {
193            self.storage.set_gauge(&self.name, value);
194        }
195    }
196
197    /// Get the current gauge value.
198    pub fn get(&self) -> f64 {
199        self.storage
200            .get(&self.name)
201            .map(|v| v.as_gauge())
202            .unwrap_or(0.0)
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_counter_increment() {
212        let storage = Arc::new(MetricsStorage::new());
213        let counter = Counter::new("test_counter", storage);
214
215        counter.increment(5);
216        assert_eq!(counter.get(), 5);
217
218        counter.increment(3);
219        assert_eq!(counter.get(), 8);
220    }
221
222    #[test]
223    fn test_gauge_set() {
224        let storage = Arc::new(MetricsStorage::new());
225        let gauge = Gauge::new("test_gauge", storage);
226
227        gauge.set(42.0);
228        assert_eq!(gauge.get(), 42.0);
229
230        gauge.set(100.0);
231        assert_eq!(gauge.get(), 100.0);
232    }
233
234    #[test]
235    fn test_histogram_record() {
236        let storage = Arc::new(MetricsStorage::new());
237        let histogram = Histogram::new("test_histogram", storage);
238
239        histogram.record(1.0);
240        histogram.record(2.0);
241        histogram.record(3.0);
242
243        let values = histogram.get_values();
244        assert_eq!(values, vec![1.0, 2.0, 3.0]);
245    }
246
247    #[test]
248    fn test_metrics_storage_list_names() {
249        let storage = Arc::new(MetricsStorage::new());
250        let counter = Counter::new("counter1", Arc::clone(&storage));
251        let gauge = Gauge::new("gauge1", Arc::clone(&storage));
252
253        counter.increment(1);
254        gauge.set(1.0);
255
256        let names = storage.list_names();
257        assert_eq!(names.len(), 2);
258        assert!(names.contains(&"counter1".to_string()));
259        assert!(names.contains(&"gauge1".to_string()));
260    }
261}