Functional Weave
Code in Rust

monitor.alert-rule@1.0.0

impl/rust.rs

4,383 bytes · the Rust implementation · view raw

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))
}