use std::sync::{Arc, LazyLock, Mutex};
use crate::metrics_query::{MetricsQuery, Selection};
use crate::snapshot::{MetricSet, MetricValue};
static METRICS_QUERY: LazyLock<Mutex<Option<Arc<MetricsQuery>>>> =
LazyLock::new(|| Mutex::new(None));
pub fn set_global_query(query: Arc<MetricsQuery>) {
*METRICS_QUERY.lock().unwrap_or_else(|e| e.into_inner()) = Some(query);
}
fn get_query() -> Option<Arc<MetricsQuery>> {
METRICS_QUERY
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
fn selection_from_pattern(pattern: &str) -> (Selection, Option<String>) {
let mut sel = Selection::all();
let mut family: Option<String> = None;
for part in pattern.split(',').map(str::trim) {
if part.is_empty() {
continue;
}
if let Some((key, value)) = part.split_once('=') {
sel = sel.with_label(key.trim(), value.trim());
} else if let Some((key, substring)) = part.split_once('~') {
sel = sel.with_label_containing(key.trim(), substring.trim());
} else {
sel = sel.with_family(part);
family = Some(part.to_string());
}
}
(sel, family)
}
#[polydat::polydat_node(
category = Context,
purity = Nondeterministic("reads live session-lifetime metrics; value changes over the run"),
)]
fn metric(label_pattern: Const<&str>, stat: Const<&str>) -> f64 {
let (sel, family) = selection_from_pattern(label_pattern.0);
let Some(fam) = family else {
warn_family_required("metric", label_pattern.0);
return 0.0;
};
get_query()
.map(|q| q.session_lifetime(&sel))
.and_then(|snap| extract_family_stat(&snap, &fam, stat.0))
.unwrap_or(0.0)
}
#[polydat::polydat_node(
category = Context,
purity = Nondeterministic("reads the latest framed cadence window; value changes over the run"),
)]
fn metric_window(label_pattern: Const<&str>, stat: Const<&str>) -> f64 {
let (sel, family) = selection_from_pattern(label_pattern.0);
let Some(family) = family else {
warn_family_required("metric_window", label_pattern.0);
return 0.0;
};
get_query()
.and_then(|q| {
let smallest = q.reporter().declared_cadences().smallest();
if smallest.is_zero() {
return None;
}
let snap = match stat.0 {
"p50" | "p99" | "mean" => q.distribution_over(smallest, &sel),
_ => q.increase_over(smallest, &sel),
};
extract_family_stat(&snap, &family, stat.0)
})
.unwrap_or(0.0)
}
fn warn_family_required(node: &str, pattern: &str) {
static WARNED: LazyLock<Mutex<std::collections::HashSet<String>>> =
LazyLock::new(|| Mutex::new(std::collections::HashSet::new()));
let key = format!("{node}:{pattern}");
let mut warned = WARNED.lock().unwrap_or_else(|e| e.into_inner());
if warned.insert(key) {
crate::diag::warn(&format!(
"{node}('{pattern}', …): no metric family named — the first \
bare token in the pattern must be an instrument name (e.g. \
{node}('errors_total', 'rate')); reading 0.0"
));
}
}
pub fn metric_selector_resolves(pattern: &str) -> bool {
let (sel, family) = selection_from_pattern(pattern);
let Some(fam) = family else { return false };
get_query()
.map(|q| q.session_lifetime(&sel))
.and_then(|snap| {
snap.family(&fam)
.map(|f| f.metrics().next().and_then(|m| m.point()).is_some())
})
.unwrap_or(false)
}
fn extract_family_stat(snapshot: &MetricSet, family: &str, stat: &str) -> Option<f64> {
let f = snapshot.family(family)?;
let m = f.metrics().next()?;
match (m.point()?.value(), stat) {
(MetricValue::Counter(c), "count" | "value") => Some(c.cumulative as f64),
(MetricValue::Counter(c), "rate") => {
let secs = snapshot.interval().as_secs_f64().max(0.001);
Some(c.cumulative as f64 / secs)
}
(MetricValue::Gauge(g), "value") => Some(g.value),
(MetricValue::Histogram(h), "count") => Some(h.count as f64),
(MetricValue::Histogram(h), "mean") if h.count > 0 => Some(h.reservoir.mean()),
(MetricValue::Histogram(h), "p50") if h.count > 0 => {
Some(h.reservoir.value_at_quantile(0.50) as f64)
}
(MetricValue::Histogram(h), "p99") if h.count > 0 => {
Some(h.reservoir.value_at_quantile(0.99) as f64)
}
_ => None,
}
}