use std::collections::BTreeMap;
use std::fmt::Write;
use std::sync::{Mutex, MutexGuard};
enum Metric {
Counter(u64),
Gauge(i64),
}
#[derive(Default)]
pub struct Registry {
metrics: Mutex<BTreeMap<String, Metric>>,
}
impl Registry {
pub fn new() -> Self {
Self::default()
}
pub fn inc(&self, name: &str) {
self.add(name, 1);
}
pub fn add(&self, name: &str, by: u64) {
self.update(name, Metric::Counter(0), |metric| match metric {
Metric::Counter(value) => *value = value.saturating_add(by),
Metric::Gauge(_) => panic!("metric {name} is a gauge"),
});
}
pub fn set(&self, name: &str, value: i64) {
self.update(name, Metric::Gauge(0), |metric| match metric {
Metric::Gauge(current) => *current = value,
Metric::Counter(_) => panic!("metric {name} is a counter"),
});
}
pub fn render(&self) -> String {
let mut out = String::new();
for (name, metric) in self.lock().iter() {
let _ = match metric {
Metric::Counter(value) => writeln!(out, "# TYPE {name} counter\n{name} {value}"),
Metric::Gauge(value) => writeln!(out, "# TYPE {name} gauge\n{name} {value}"),
};
}
out
}
fn update(&self, name: &str, new: Metric, apply: impl FnOnce(&mut Metric)) {
assert!(valid(name), "invalid metric name {name:?}");
let mut metrics = self.lock();
if let Some(metric) = metrics.get_mut(name) {
return apply(metric); }
apply(metrics.entry(name.to_string()).or_insert(new));
}
fn lock(&self) -> MutexGuard<'_, BTreeMap<String, Metric>> {
self.metrics
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
fn valid(name: &str) -> bool {
let mut chars = name.chars();
chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == ':')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_sorted_counters_and_gauges() {
let metrics = Registry::new();
metrics.set("b_gauge", 5);
metrics.inc("a_total");
metrics.add("a_total", 4);
metrics.set("b_gauge", -2);
assert_eq!(
metrics.render(),
"# TYPE a_total counter\na_total 5\n# TYPE b_gauge gauge\nb_gauge -2\n"
);
}
#[test]
#[should_panic(expected = "invalid metric name")]
fn rejects_bad_names() {
Registry::new().inc("bad name");
}
#[test]
#[should_panic(expected = "is a counter")]
fn rejects_kind_change() {
let metrics = Registry::new();
metrics.inc("x");
metrics.set("x", 1);
}
}