Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::monitor_series_window::{samples_from_value, series_window, MetricSample}; ← from monitor.series-window ^1.0.0 · built alongside by fune
fn beyond(op: &str, value: i64, limit: i64) -> bool {
match op {
"gt" => value > limit,
"gte" => value >= limit,
"lt" => value < limit,
"lte" => value <= limit,
other => panic!("unknown comparison: {}", other),
}
}
/// The state of a threshold rule at `now`, Prometheus style: a breach must
/// hold for `for_seconds` before it fires, and until then it is pending.
///
/// With a clear_threshold a breach clears only once the value passes it the
/// other way, so a metric wobbling around the threshold does not flap between
/// firing and inactive. The breach clock (`since`) restarts after a clear.
///
/// # Panics
/// Panics on an unknown comparison, a clear threshold on the breaching side,
/// negative durations, or samples out of time order.
pub fn evaluate_alert_rule(samples: &[MetricSample], rule: &ThresholdRule, now: i64) -> RuleEvaluation {
let op = rule.op.as_str();
let threshold = rule.threshold;
let clear = rule.clear_threshold.unwrap_or(threshold);
beyond(op, 0, 0);
let upward = op == "gt" || op == "gte";
if if upward { clear > threshold } else { clear < threshold } {
panic!(
"clearThreshold must be on the non-breaching side of threshold: {} {}, clear {}",
op, threshold, clear
);
}
if rule.for_seconds < 0 {
panic!("forSeconds must not be negative, received {}", rule.for_seconds);
}
if let Some(stale) = rule.stale_after_seconds {
if stale < 0 {
panic!("staleAfterSeconds must not be negative, received {}", stale);
}
}
// series_window checks the order of the whole series and keeps at <= now.
let from = if samples.is_empty() { now + 1 } else { samples[0].at.min(now + 1) };
let seen = series_window(samples, from, now + 1);
let mut since: Option<i64> = None;
for s in &seen {
match since {
None => {
if beyond(op, s.value, threshold) {
since = Some(s.at);
}
}
Some(_) => {
if !beyond(op, s.value, clear) {
since = None;
}
}
}
}
let latest = match seen.last() {
None => return RuleEvaluation { state: "no-data".to_string(), since: None, value: None, held_seconds: 0 },
Some(s) => s,
};
if let Some(stale) = rule.stale_after_seconds {
if now - latest.at > stale {
return RuleEvaluation { state: "no-data".to_string(), since: None, value: Some(latest.value), held_seconds: 0 };
}
}
match since {
None => RuleEvaluation { state: "inactive".to_string(), since: None, value: Some(latest.value), held_seconds: 0 },
Some(start) => {
let held = now - start;
let state = if held >= rule.for_seconds { "firing" } else { "pending" };
RuleEvaluation { state: state.to_string(), since: Some(start), value: Some(latest.value), held_seconds: held }
}
}
}
fn opt_i64(v: &Value) -> Option<i64> {
if v.is_null() { None } else { Some(v.as_i64()) }
}
fn opt_to_value(v: Option<i64>) -> Value {
match v {
Some(i) => Value::Int(i),
None => Value::Null,
}
}
pub fn threshold_rule_from_value(v: &Value) -> ThresholdRule {
ThresholdRule {
op: v.get("op").as_str().to_string(),
threshold: v.get("threshold").as_i64(),
clear_threshold: opt_i64(v.get("clearThreshold")),
for_seconds: v.get("forSeconds").as_i64(),
stale_after_seconds: opt_i64(v.get("staleAfterSeconds")),
}
}
pub fn rule_evaluation_to_value(r: &RuleEvaluation) -> Value {
Value::obj(vec![
("state", Value::str(&r.state)),
("since", opt_to_value(r.since)),
("value", opt_to_value(r.value)),
("heldSeconds", Value::Int(r.held_seconds)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let samples = samples_from_value(&args[0]);
let rule = threshold_rule_from_value(&args[1]);
let now = match &args[2] {
Value::Int(i) => *i,
_ => panic!("now must be a whole number of seconds"),
};
rule_evaluation_to_value(&evaluate_alert_rule(&samples, &rule, now))
}