use super::funejson::Value; use super::monitor_series_window::{samples_from_value, series_window, MetricSample}; 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 = 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 { if v.is_null() { None } else { Some(v.as_i64()) } } fn opt_to_value(v: Option) -> 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)) }