1use 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 pub counters: HashMap<String, u64>,
21 pub gauges: HashMap<String, f64>,
23 pub histograms: HashMap<String, Histogram>,
25}
26
27#[derive(Debug, Default, Clone, Serialize, Deserialize)]
28pub struct Histogram {
29 pub count: u64,
31 pub sum: f64,
33 pub min: f64,
35 pub max: f64,
37}
38
39impl Histogram {
40 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
58pub fn set_enabled(enabled: bool) {
60 ENABLED.store(enabled, Ordering::Relaxed);
61}
62
63pub fn inc_counter(name: &str, labels: &[(&str, &str)]) {
65 add_counter(name, labels, 1);
66}
67
68pub 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
79pub 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
89pub 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
103pub fn snapshot() -> MetricsStore {
105 STORE.lock().clone()
106}
107
108pub 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
132fn 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}