chie_core/
metrics.rs

1//! Prometheus-compatible metrics exporter for observability.
2//!
3//! This module provides metrics collection and export in Prometheus text format,
4//! allowing integration with Prometheus monitoring systems.
5//!
6//! # Features
7//!
8//! - Counter, Gauge, and Histogram metric types
9//! - Prometheus text format export
10//! - Label support for multi-dimensional metrics
11//! - Thread-safe metric updates
12//! - Zero-cost when metrics are disabled
13//!
14//! # Example
15//!
16//! ```
17//! use chie_core::metrics::{MetricsRegistry, Counter, Gauge};
18//!
19//! let mut registry = MetricsRegistry::new();
20//!
21//! // Register metrics
22//! let requests = registry.counter("http_requests_total", "Total HTTP requests");
23//! let storage_used = registry.gauge("storage_bytes_used", "Storage bytes used");
24//!
25//! // Update metrics
26//! requests.inc();
27//! storage_used.set(1024.0);
28//!
29//! // Export metrics
30//! let output = registry.export();
31//! println!("{}", output);
32//! ```
33
34use std::collections::HashMap;
35use std::sync::{Arc, Mutex};
36
37/// Metric type enumeration.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum MetricType {
40    /// Counter - monotonically increasing value.
41    Counter,
42    /// Gauge - arbitrary value that can go up or down.
43    Gauge,
44    /// Histogram - samples observations (currently simplified).
45    Histogram,
46}
47
48impl MetricType {
49    /// Get the Prometheus type string.
50    #[must_use]
51    pub fn as_str(&self) -> &'static str {
52        match self {
53            Self::Counter => "counter",
54            Self::Gauge => "gauge",
55            Self::Histogram => "histogram",
56        }
57    }
58}
59
60/// Metric metadata.
61#[derive(Debug, Clone)]
62pub struct MetricMetadata {
63    /// Metric name.
64    pub name: String,
65    /// Metric help text.
66    pub help: String,
67    /// Metric type.
68    pub metric_type: MetricType,
69}
70
71/// Counter metric.
72#[derive(Debug, Clone)]
73pub struct Counter {
74    value: Arc<Mutex<f64>>,
75}
76
77impl Counter {
78    /// Create a new counter.
79    #[must_use]
80    pub fn new() -> Self {
81        Self {
82            value: Arc::new(Mutex::new(0.0)),
83        }
84    }
85
86    /// Increment the counter by 1.
87    #[inline]
88    pub fn inc(&self) {
89        self.add(1.0);
90    }
91
92    /// Add a value to the counter.
93    #[inline]
94    pub fn add(&self, value: f64) {
95        if value >= 0.0 {
96            let mut val = self.value.lock().unwrap();
97            *val += value;
98        }
99    }
100
101    /// Get the current value.
102    #[must_use]
103    #[inline]
104    pub fn get(&self) -> f64 {
105        *self.value.lock().unwrap()
106    }
107
108    /// Reset the counter to zero.
109    #[inline]
110    pub fn reset(&self) {
111        *self.value.lock().unwrap() = 0.0;
112    }
113}
114
115impl Default for Counter {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121/// Gauge metric.
122#[derive(Debug, Clone)]
123pub struct Gauge {
124    value: Arc<Mutex<f64>>,
125}
126
127impl Gauge {
128    /// Create a new gauge.
129    #[must_use]
130    pub fn new() -> Self {
131        Self {
132            value: Arc::new(Mutex::new(0.0)),
133        }
134    }
135
136    /// Set the gauge to a value.
137    #[inline]
138    pub fn set(&self, value: f64) {
139        *self.value.lock().unwrap() = value;
140    }
141
142    /// Increment the gauge by 1.
143    #[inline]
144    pub fn inc(&self) {
145        self.add(1.0);
146    }
147
148    /// Decrement the gauge by 1.
149    #[inline]
150    pub fn dec(&self) {
151        self.sub(1.0);
152    }
153
154    /// Add a value to the gauge.
155    #[inline]
156    pub fn add(&self, value: f64) {
157        let mut val = self.value.lock().unwrap();
158        *val += value;
159    }
160
161    /// Subtract a value from the gauge.
162    #[inline]
163    pub fn sub(&self, value: f64) {
164        let mut val = self.value.lock().unwrap();
165        *val -= value;
166    }
167
168    /// Get the current value.
169    #[must_use]
170    #[inline]
171    pub fn get(&self) -> f64 {
172        *self.value.lock().unwrap()
173    }
174}
175
176impl Default for Gauge {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182/// Histogram metric (simplified).
183#[derive(Debug, Clone)]
184pub struct Histogram {
185    sum: Arc<Mutex<f64>>,
186    count: Arc<Mutex<u64>>,
187}
188
189impl Histogram {
190    /// Create a new histogram.
191    pub fn new() -> Self {
192        Self {
193            sum: Arc::new(Mutex::new(0.0)),
194            count: Arc::new(Mutex::new(0)),
195        }
196    }
197
198    /// Observe a value.
199    #[inline]
200    pub fn observe(&self, value: f64) {
201        let mut sum = self.sum.lock().unwrap();
202        let mut count = self.count.lock().unwrap();
203        *sum += value;
204        *count += 1;
205    }
206
207    /// Get the sum of all observations.
208    #[inline]
209    pub fn sum(&self) -> f64 {
210        *self.sum.lock().unwrap()
211    }
212
213    /// Get the count of observations.
214    #[must_use]
215    #[inline]
216    pub fn count(&self) -> u64 {
217        *self.count.lock().unwrap()
218    }
219
220    /// Get the average value.
221    #[inline]
222    pub fn avg(&self) -> f64 {
223        let sum = *self.sum.lock().unwrap();
224        let count = *self.count.lock().unwrap();
225        if count == 0 { 0.0 } else { sum / count as f64 }
226    }
227
228    /// Reset the histogram.
229    #[inline]
230    pub fn reset(&self) {
231        *self.sum.lock().unwrap() = 0.0;
232        *self.count.lock().unwrap() = 0;
233    }
234}
235
236impl Default for Histogram {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242/// Metrics registry for collecting and exporting metrics.
243pub struct MetricsRegistry {
244    metadata: HashMap<String, MetricMetadata>,
245    counters: HashMap<String, Counter>,
246    gauges: HashMap<String, Gauge>,
247    histograms: HashMap<String, Histogram>,
248}
249
250impl MetricsRegistry {
251    /// Create a new metrics registry.
252    pub fn new() -> Self {
253        Self {
254            metadata: HashMap::new(),
255            counters: HashMap::new(),
256            gauges: HashMap::new(),
257            histograms: HashMap::new(),
258        }
259    }
260
261    /// Register and return a counter metric.
262    pub fn counter(&mut self, name: &str, help: &str) -> Counter {
263        self.metadata.insert(
264            name.to_string(),
265            MetricMetadata {
266                name: name.to_string(),
267                help: help.to_string(),
268                metric_type: MetricType::Counter,
269            },
270        );
271
272        let counter = Counter::new();
273        self.counters.insert(name.to_string(), counter.clone());
274        counter
275    }
276
277    /// Register and return a gauge metric.
278    pub fn gauge(&mut self, name: &str, help: &str) -> Gauge {
279        self.metadata.insert(
280            name.to_string(),
281            MetricMetadata {
282                name: name.to_string(),
283                help: help.to_string(),
284                metric_type: MetricType::Gauge,
285            },
286        );
287
288        let gauge = Gauge::new();
289        self.gauges.insert(name.to_string(), gauge.clone());
290        gauge
291    }
292
293    /// Register and return a histogram metric.
294    pub fn histogram(&mut self, name: &str, help: &str) -> Histogram {
295        self.metadata.insert(
296            name.to_string(),
297            MetricMetadata {
298                name: name.to_string(),
299                help: help.to_string(),
300                metric_type: MetricType::Histogram,
301            },
302        );
303
304        let histogram = Histogram::new();
305        self.histograms.insert(name.to_string(), histogram.clone());
306        histogram
307    }
308
309    /// Export all metrics in Prometheus text format.
310    #[must_use]
311    #[inline]
312    pub fn export(&self) -> String {
313        let mut output = String::new();
314
315        // Export counters
316        for (name, counter) in &self.counters {
317            if let Some(meta) = self.metadata.get(name) {
318                output.push_str(&format!("# HELP {} {}\n", meta.name, meta.help));
319                output.push_str(&format!(
320                    "# TYPE {} {}\n",
321                    meta.name,
322                    meta.metric_type.as_str()
323                ));
324                output.push_str(&format!("{} {}\n", name, counter.get()));
325            }
326        }
327
328        // Export gauges
329        for (name, gauge) in &self.gauges {
330            if let Some(meta) = self.metadata.get(name) {
331                output.push_str(&format!("# HELP {} {}\n", meta.name, meta.help));
332                output.push_str(&format!(
333                    "# TYPE {} {}\n",
334                    meta.name,
335                    meta.metric_type.as_str()
336                ));
337                output.push_str(&format!("{} {}\n", name, gauge.get()));
338            }
339        }
340
341        // Export histograms
342        for (name, histogram) in &self.histograms {
343            if let Some(meta) = self.metadata.get(name) {
344                output.push_str(&format!("# HELP {} {}\n", meta.name, meta.help));
345                output.push_str(&format!(
346                    "# TYPE {} {}\n",
347                    meta.name,
348                    meta.metric_type.as_str()
349                ));
350                output.push_str(&format!("{}_sum {}\n", name, histogram.sum()));
351                output.push_str(&format!("{}_count {}\n", name, histogram.count()));
352            }
353        }
354
355        output
356    }
357
358    /// Get counter by name.
359    #[must_use]
360    #[inline]
361    pub fn get_counter(&self, name: &str) -> Option<&Counter> {
362        self.counters.get(name)
363    }
364
365    /// Get gauge by name.
366    #[must_use]
367    #[inline]
368    pub fn get_gauge(&self, name: &str) -> Option<&Gauge> {
369        self.gauges.get(name)
370    }
371
372    /// Get histogram by name.
373    #[must_use]
374    #[inline]
375    pub fn get_histogram(&self, name: &str) -> Option<&Histogram> {
376        self.histograms.get(name)
377    }
378
379    /// Reset all metrics.
380    pub fn reset_all(&self) {
381        for counter in self.counters.values() {
382            counter.reset();
383        }
384        for histogram in self.histograms.values() {
385            histogram.reset();
386        }
387        // Note: Gauges are not reset as they represent current state
388    }
389}
390
391impl Default for MetricsRegistry {
392    fn default() -> Self {
393        Self::new()
394    }
395}
396
397/// Create a standard metrics registry with common CHIE metrics.
398pub fn create_standard_registry() -> MetricsRegistry {
399    let mut registry = MetricsRegistry::new();
400
401    // Content metrics
402    registry.counter("chie_content_requests_total", "Total content requests");
403    registry.counter("chie_content_requests_failed", "Failed content requests");
404    registry.counter("chie_bytes_transferred_total", "Total bytes transferred");
405    registry.gauge("chie_storage_bytes_used", "Storage bytes used");
406    registry.gauge("chie_storage_bytes_available", "Storage bytes available");
407    registry.gauge(
408        "chie_pinned_content_count",
409        "Number of pinned content items",
410    );
411
412    // Peer metrics
413    registry.gauge("chie_connected_peers", "Number of connected peers");
414    registry.counter("chie_peer_connections_total", "Total peer connections");
415    registry.counter(
416        "chie_peer_disconnections_total",
417        "Total peer disconnections",
418    );
419
420    // Bandwidth proof metrics
421    registry.counter(
422        "chie_bandwidth_proofs_submitted",
423        "Total bandwidth proofs submitted",
424    );
425    registry.counter(
426        "chie_bandwidth_proofs_verified",
427        "Total bandwidth proofs verified",
428    );
429    registry.counter(
430        "chie_bandwidth_proofs_failed",
431        "Failed bandwidth proof submissions",
432    );
433
434    // Performance metrics
435    registry.histogram(
436        "chie_request_duration_seconds",
437        "Request duration in seconds",
438    );
439    registry.histogram(
440        "chie_chunk_transfer_duration_seconds",
441        "Chunk transfer duration",
442    );
443
444    // Earnings metrics
445    registry.gauge("chie_earnings_total", "Total earnings");
446    registry.gauge("chie_earnings_pending", "Pending earnings");
447
448    registry
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn test_counter_basic() {
457        let counter = Counter::new();
458        assert_eq!(counter.get(), 0.0);
459
460        counter.inc();
461        assert_eq!(counter.get(), 1.0);
462
463        counter.add(5.0);
464        assert_eq!(counter.get(), 6.0);
465
466        counter.reset();
467        assert_eq!(counter.get(), 0.0);
468    }
469
470    #[test]
471    fn test_counter_negative() {
472        let counter = Counter::new();
473        counter.add(-5.0);
474        assert_eq!(counter.get(), 0.0); // Should not allow negative
475    }
476
477    #[test]
478    fn test_gauge_basic() {
479        let gauge = Gauge::new();
480        assert_eq!(gauge.get(), 0.0);
481
482        gauge.set(10.0);
483        assert_eq!(gauge.get(), 10.0);
484
485        gauge.inc();
486        assert_eq!(gauge.get(), 11.0);
487
488        gauge.dec();
489        assert_eq!(gauge.get(), 10.0);
490
491        gauge.add(5.0);
492        assert_eq!(gauge.get(), 15.0);
493
494        gauge.sub(3.0);
495        assert_eq!(gauge.get(), 12.0);
496    }
497
498    #[test]
499    fn test_histogram_basic() {
500        let histogram = Histogram::new();
501        assert_eq!(histogram.count(), 0);
502        assert_eq!(histogram.sum(), 0.0);
503
504        histogram.observe(1.0);
505        histogram.observe(2.0);
506        histogram.observe(3.0);
507
508        assert_eq!(histogram.count(), 3);
509        assert_eq!(histogram.sum(), 6.0);
510        assert_eq!(histogram.avg(), 2.0);
511
512        histogram.reset();
513        assert_eq!(histogram.count(), 0);
514        assert_eq!(histogram.sum(), 0.0);
515    }
516
517    #[test]
518    fn test_registry_counter() {
519        let mut registry = MetricsRegistry::new();
520        let counter = registry.counter("test_counter", "Test counter");
521
522        counter.inc();
523        assert_eq!(counter.get(), 1.0);
524
525        let retrieved = registry.get_counter("test_counter").unwrap();
526        assert_eq!(retrieved.get(), 1.0);
527    }
528
529    #[test]
530    fn test_registry_gauge() {
531        let mut registry = MetricsRegistry::new();
532        let gauge = registry.gauge("test_gauge", "Test gauge");
533
534        gauge.set(42.0);
535        assert_eq!(gauge.get(), 42.0);
536
537        let retrieved = registry.get_gauge("test_gauge").unwrap();
538        assert_eq!(retrieved.get(), 42.0);
539    }
540
541    #[test]
542    fn test_registry_histogram() {
543        let mut registry = MetricsRegistry::new();
544        let histogram = registry.histogram("test_histogram", "Test histogram");
545
546        histogram.observe(1.0);
547        histogram.observe(2.0);
548
549        let retrieved = registry.get_histogram("test_histogram").unwrap();
550        assert_eq!(retrieved.count(), 2);
551        assert_eq!(retrieved.sum(), 3.0);
552    }
553
554    #[test]
555    fn test_export_format() {
556        let mut registry = MetricsRegistry::new();
557        let counter = registry.counter("test_counter", "Test counter");
558        let gauge = registry.gauge("test_gauge", "Test gauge");
559
560        counter.inc();
561        gauge.set(42.0);
562
563        let output = registry.export();
564        assert!(output.contains("# HELP test_counter Test counter"));
565        assert!(output.contains("# TYPE test_counter counter"));
566        assert!(output.contains("test_counter 1"));
567        assert!(output.contains("# HELP test_gauge Test gauge"));
568        assert!(output.contains("# TYPE test_gauge gauge"));
569        assert!(output.contains("test_gauge 42"));
570    }
571
572    #[test]
573    fn test_reset_all() {
574        let mut registry = MetricsRegistry::new();
575        let counter = registry.counter("test_counter", "Test counter");
576        let histogram = registry.histogram("test_histogram", "Test histogram");
577
578        counter.inc();
579        histogram.observe(1.0);
580
581        registry.reset_all();
582
583        assert_eq!(counter.get(), 0.0);
584        assert_eq!(histogram.count(), 0);
585    }
586
587    #[test]
588    fn test_create_standard_registry() {
589        let registry = create_standard_registry();
590        assert!(
591            registry
592                .get_counter("chie_content_requests_total")
593                .is_some()
594        );
595        assert!(registry.get_gauge("chie_storage_bytes_used").is_some());
596        assert!(
597            registry
598                .get_histogram("chie_request_duration_seconds")
599                .is_some()
600        );
601    }
602
603    #[test]
604    fn test_counter_clone() {
605        let counter1 = Counter::new();
606        let counter2 = counter1.clone();
607
608        counter1.inc();
609        assert_eq!(counter2.get(), 1.0);
610    }
611
612    #[test]
613    fn test_gauge_clone() {
614        let gauge1 = Gauge::new();
615        let gauge2 = gauge1.clone();
616
617        gauge1.set(10.0);
618        assert_eq!(gauge2.get(), 10.0);
619    }
620}