use nmbrs_workload::model::{StopConditionSpec, WorkloadPhase};
const ACTIVITY_FAMILIES: [&str; 18] = [
"cycles_servicetime",
"cycles_waittime",
"cycles_responsetime",
"result_success",
"result_failure",
"result_total",
"cycles_total",
"skips_total",
"errors_total",
"attempt_total",
"attempt_success",
"attempt_failure",
"stanzas_total",
"daemon_cancelled_total",
"daemon_errors_total",
"result_elements",
"result_bytes",
"tries",
];
pub fn lint_workload<'a>(
workload_stop_when: &[StopConditionSpec],
phases: impl Iterator<Item = (&'a str, &'a WorkloadPhase)> + Clone,
) -> Result<Vec<String>, String> {
let mut warnings = Vec::new();
let mut known: Vec<String> = ACTIVITY_FAMILIES.iter().map(|s| s.to_string()).collect();
for (_, phase) in phases.clone() {
known.extend(phase.metrics.keys().cloned());
}
for sc in workload_stop_when {
lint_metric_families(&sc.when, &known, "workload `stop_when`", &mut warnings);
}
for (name, phase) in phases {
let ctx = format!("phase '{name}'");
if let Some(spec) = phase.errors.as_deref() {
lint_error_router(spec, &ctx, &mut warnings)?;
}
for op in &phase.ops {
if let Some(serde_json::Value::String(spec)) = op.params.get("errors") {
lint_error_router(spec, &format!("{ctx} op '{}'", op.name), &mut warnings)?;
}
}
for sc in &phase.stop_when {
lint_metric_families(
&sc.when,
&known,
&format!("{ctx} `stop_when`"),
&mut warnings,
);
}
if let Some(gate) = &phase.continue_if {
lint_metric_families(
&gate.when,
&known,
&format!("{ctx} `continue_if`"),
&mut warnings,
);
}
if let Some(poll) = &phase.poll {
lint_metric_families(
&poll.until,
&known,
&format!("{ctx} `poll.until`"),
&mut warnings,
);
}
}
Ok(warnings)
}
fn lint_error_router(spec: &str, ctx: &str, warnings: &mut Vec<String>) -> Result<(), String> {
if spec.contains('{') {
return Ok(()); }
let router = nmbrs_errorhandler::ErrorRouter::parse(spec)
.map_err(|e| format!("{ctx}: invalid `errors:` spec '{spec}': {e}"))?;
if !router.has_catch_all() {
warnings.push(format!(
"{ctx}: `errors: \"{spec}\"` has no catch-all rule — an error \
class matching no pattern falls through to `stop`; end the \
spec with `.*:<verbs>` to make the default explicit"
));
}
Ok(())
}
fn lint_metric_families(src: &str, known: &[String], ctx: &str, warnings: &mut Vec<String>) {
for family in metric_families(src) {
if family.contains('{') {
continue; }
if !known.iter().any(|k| k == &family) {
warnings.push(format!(
"{ctx}: `metric()` selector names family '{family}', which is \
not a built-in instrument family or a declared phase metric — \
an unregistered family reads 0.0 silently; check the spelling \
(predicate: {src})"
));
}
}
}
fn metric_families(src: &str) -> Vec<String> {
let bytes = src.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while let Some(rel) = src[i..].find("metric") {
let start = i + rel;
i = start + "metric".len();
if start > 0 {
let prev = bytes[start - 1] as char;
if prev.is_alphanumeric() || prev == '_' {
continue;
}
}
let mut j = i;
if src[j..].starts_with("_window") {
j += "_window".len();
}
let rest = src[j..].trim_start();
let Some(after_paren) = rest.strip_prefix('(') else {
continue;
};
let sel = after_paren.trim_start();
let Some(quote) = sel.chars().next().filter(|c| *c == '\'' || *c == '"') else {
continue; };
let body = &sel[1..];
let Some(end) = body.find(quote) else {
continue;
};
let selector = &body[..end];
let family = selector.split(',').next().unwrap_or("").trim().to_string();
if !family.is_empty() {
out.push(family);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_families_from_both_call_forms() {
let src = "metric('cycles_total, phase=ingest', 'count') >= 5 \
&& metric_window(\"result_failure\", 'rate') < 0.1";
assert_eq!(
metric_families(src),
vec!["cycles_total".to_string(), "result_failure".to_string()]
);
}
#[test]
fn ignores_non_literal_selectors_and_other_identifiers() {
assert!(metric_families("my_metric('x','count') + metric(fam, 'count')").is_empty());
}
#[test]
fn unknown_family_warns_known_family_does_not() {
let known: Vec<String> = ACTIVITY_FAMILIES.iter().map(|s| s.to_string()).collect();
let mut w = Vec::new();
lint_metric_families(
"metric('cycles_totl, phase=x', 'count') > 0",
&known,
"t",
&mut w,
);
assert_eq!(w.len(), 1, "typo'd family must warn: {w:?}");
w.clear();
lint_metric_families(
"metric('cycles_total, phase=x', 'count') > 0",
&known,
"t",
&mut w,
);
assert!(w.is_empty(), "known family must not warn: {w:?}");
}
#[test]
fn router_without_catch_all_warns_with_catch_all_does_not() {
let mut w = Vec::new();
lint_error_router("TimeoutError:retry,warn", "t", &mut w).unwrap();
assert_eq!(w.len(), 1, "no catch-all must warn: {w:?}");
w.clear();
lint_error_router("TimeoutError:retry,warn;.*:counter", "t", &mut w).unwrap();
assert!(w.is_empty(), "catch-all present must not warn: {w:?}");
}
#[test]
fn bad_router_spec_is_a_load_error_interpolated_is_deferred() {
let mut w = Vec::new();
let err = lint_error_router(".*:sotp", "phase 'p'", &mut w).unwrap_err();
assert!(err.contains("unknown error handler"), "got: {err}");
lint_error_router("{overload_policy}", "t", &mut w)
.expect("interpolated spec must be deferred, not parsed");
}
}