Skip to main content

rift_metrics/
lib.rs

1//! Lightweight in-process metrics collection.
2//!
3//! This crate intentionally avoids external dependencies or exporters. It stores
4//! counters, gauges, and histograms in memory and can render a simple text
5//! representation for debugging or log capture.
6
7use std::collections::HashMap;
8use std::sync::atomic::{AtomicBool, Ordering};
9
10use once_cell::sync::Lazy;
11use parking_lot::Mutex;
12use serde::{Deserialize, Serialize};
13
14static ENABLED: AtomicBool = AtomicBool::new(true);
15static STORE: Lazy<Mutex<MetricsStore>> = Lazy::new(|| Mutex::new(MetricsStore::default()));
16
17#[derive(Debug, Default, Clone, Serialize, Deserialize)]
18pub struct MetricsStore {
19    /// Monotonic counters.
20    pub counters: HashMap<String, u64>,
21    /// Last-set gauge values.
22    pub gauges: HashMap<String, f64>,
23    /// Aggregated histograms.
24    pub histograms: HashMap<String, Histogram>,
25}
26
27#[derive(Debug, Default, Clone, Serialize, Deserialize)]
28pub struct Histogram {
29    /// Number of observations.
30    pub count: u64,
31    /// Sum of all observations.
32    pub sum: f64,
33    /// Minimum observed value.
34    pub min: f64,
35    /// Maximum observed value.
36    pub max: f64,
37}
38
39impl Histogram {
40    /// Record a new observation, updating min/max/sum/count.
41    pub fn observe(&mut self, value: f64) {
42        if self.count == 0 {
43            self.min = value;
44            self.max = value;
45        } else {
46            if value < self.min {
47                self.min = value;
48            }
49            if value > self.max {
50                self.max = value;
51            }
52        }
53        self.count += 1;
54        self.sum += value;
55    }
56}
57
58/// Enable or disable metrics collection globally.
59pub fn set_enabled(enabled: bool) {
60    ENABLED.store(enabled, Ordering::Relaxed);
61}
62
63/// Increment a counter by 1.
64pub fn inc_counter(name: &str, labels: &[(&str, &str)]) {
65    add_counter(name, labels, 1);
66}
67
68/// Add a value to a counter (saturating on overflow).
69pub fn add_counter(name: &str, labels: &[(&str, &str)], value: u64) {
70    if !ENABLED.load(Ordering::Relaxed) {
71        return;
72    }
73    let key = format_key(name, labels);
74    let mut store = STORE.lock();
75    let entry = store.counters.entry(key).or_insert(0);
76    *entry = entry.saturating_add(value);
77}
78
79/// Set a gauge to an explicit value.
80pub fn set_gauge(name: &str, labels: &[(&str, &str)], value: f64) {
81    if !ENABLED.load(Ordering::Relaxed) {
82        return;
83    }
84    let key = format_key(name, labels);
85    let mut store = STORE.lock();
86    store.gauges.insert(key, value);
87}
88
89/// Observe a histogram value (stored as count/sum/min/max).
90pub fn observe_histogram(name: &str, labels: &[(&str, &str)], value: f64) {
91    if !ENABLED.load(Ordering::Relaxed) {
92        return;
93    }
94    let key = format_key(name, labels);
95    let mut store = STORE.lock();
96    store
97        .histograms
98        .entry(key)
99        .or_default()
100        .observe(value);
101}
102
103/// Snapshot the current metrics into an owned struct.
104pub fn snapshot() -> MetricsStore {
105    STORE.lock().clone()
106}
107
108/// Render metrics in a simple text format.
109pub fn render_text() -> String {
110    let store = snapshot();
111    let mut lines = Vec::new();
112    for (key, value) in store.counters {
113        lines.push(format!("{key} {value}"));
114    }
115    for (key, value) in store.gauges {
116        lines.push(format!("{key} {:.3}", value));
117    }
118    for (key, hist) in store.histograms {
119        let avg = if hist.count > 0 {
120            hist.sum / hist.count as f64
121        } else {
122            0.0
123        };
124        lines.push(format!(
125            "{key} count={} avg={:.3} min={:.3} max={:.3}",
126            hist.count, avg, hist.min, hist.max
127        ));
128    }
129    lines.join("\n")
130}
131
132/// Format metric keys with optional labels.
133fn format_key(name: &str, labels: &[(&str, &str)]) -> String {
134    if labels.is_empty() {
135        return name.to_string();
136    }
137    let mut parts = Vec::with_capacity(labels.len());
138    for (k, v) in labels {
139        parts.push(format!("{k}={v}"));
140    }
141    format!("{name}{{{}}}", parts.join(","))
142}