use std::collections::BTreeMap;
use std::sync::{Mutex, OnceLock};
struct State {
values: BTreeMap<String, f64>,
units: BTreeMap<String, String>,
}
static METRICS: OnceLock<Mutex<State>> = OnceLock::new();
fn state() -> &'static Mutex<State> {
METRICS.get_or_init(|| {
Mutex::new(State {
values: BTreeMap::new(),
units: BTreeMap::new(),
})
})
}
pub fn record_metric(name: &str, value: f64) {
state().lock().unwrap().values.insert(name.to_owned(), value);
}
pub fn record_metric_unit(name: &str, unit: &str) {
state()
.lock()
.unwrap()
.units
.insert(name.to_owned(), unit.to_owned());
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) struct DrainedMetric {
pub name: String,
pub value: f64,
pub unit: String,
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn drain_metrics() -> Vec<DrainedMetric> {
let mut guard = state().lock().unwrap();
let State { values, units } = &mut *guard;
std::mem::take(values)
.into_iter()
.map(|(name, value)| {
let unit = units.get(&name).cloned().unwrap_or_default();
DrainedMetric { name, value, unit }
})
.collect()
}
#[cfg(test)]
pub(crate) static TEST_GUARD: Mutex<()> = Mutex::new(());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_metric_latest_wins_then_drains_with_units() {
let _guard = TEST_GUARD.lock().unwrap();
record_metric_unit("loss", "nats");
record_metric("loss", 1.5);
record_metric("lr", 3e-4);
record_metric("loss", 1.2);
let drained = drain_metrics();
let view: Vec<_> = drained
.iter()
.map(|m| (m.name.as_str(), m.value, m.unit.as_str()))
.collect();
assert_eq!(view, vec![("loss", 1.2, "nats"), ("lr", 3e-4, "")]);
assert!(drain_metrics().is_empty());
}
}