Functional Weave
Code in Rust

monitor.alert-state@1.0.0

impl/rust.rs

5,658 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

fn state(phase: &str, since: Option<i64>, last: Option<i64>, clear_since: Option<i64>) -> AlertState {
    AlertState { phase: phase.to_string(), since, last_notified_at: last, clear_since }
}

fn transition(state: AlertState, notify: Option<&str>, changed: bool) -> AlertTransition {
    AlertTransition { state, notify: notify.map(|n| n.to_string()), changed }
}

/// One step of an alert's lifecycle. Pure: the caller stores the returned
/// state and passes it back at the next evaluation.
///
/// Firing needs the condition to hold for_seconds (Prometheus `for`);
/// resolving needs it to stay false clear_for_seconds, so a flapping check
/// does not send resolved-firing-resolved storms. last_notified_at records
/// the last notification of any kind; it drives the repeat interval while
/// firing.
///
/// # Panics
/// Panics on a negative duration, an unknown phase, a pending or firing state
/// without since, or a now earlier than any time in the previous state.
pub fn next_alert_state(previous: &AlertState, condition: bool, now: i64, policy: &AlertPolicy) -> AlertTransition {
    let for_seconds = policy.for_seconds;
    let clear_for = policy.clear_for_seconds;
    if for_seconds < 0 {
        panic!("forSeconds must not be negative, received {}", for_seconds);
    }
    if clear_for < 0 {
        panic!("clearForSeconds must not be negative, received {}", clear_for);
    }
    if let Some(r) = policy.renotify_seconds {
        if r < 1 {
            panic!("renotifySeconds must be null or at least 1, received {}", r);
        }
    }
    let phase = previous.phase.as_str();
    if !matches!(phase, "inactive" | "pending" | "firing" | "resolved") {
        panic!("unknown alert phase: {}", phase);
    }
    let (since, last, clear_since) = (previous.since, previous.last_notified_at, previous.clear_since);
    if (phase == "pending" || phase == "firing") && since.is_none() {
        panic!("since must be set in phase {}", phase);
    }
    for (name, t) in [("since", since), ("lastNotifiedAt", last), ("clearSince", clear_since)] {
        if let Some(t) = t {
            if now < t {
                panic!("now {} is earlier than {} {}", now, name, t);
            }
        }
    }

    match phase {
        "inactive" | "resolved" => {
            if !condition {
                transition(previous.clone(), None, false)
            } else if for_seconds == 0 {
                transition(state("firing", Some(now), Some(now), None), Some("firing"), true)
            } else {
                transition(state("pending", Some(now), last, None), None, true)
            }
        }
        "pending" => {
            if !condition {
                transition(state("inactive", Some(now), last, None), None, true)
            } else if now - since.unwrap() >= for_seconds {
                transition(state("firing", Some(now), Some(now), None), Some("firing"), true)
            } else {
                transition(previous.clone(), None, false)
            }
        }
        _ => {
            if condition {
                let due = match (policy.renotify_seconds, last) {
                    (Some(_), None) => true,
                    (Some(r), Some(l)) => now - l >= r,
                    (None, _) => false,
                };
                if due {
                    transition(state("firing", since, Some(now), None), Some("repeat"), false)
                } else {
                    transition(state("firing", since, last, None), None, false)
                }
            } else {
                let clearing = clear_since.unwrap_or(now);
                if now - clearing >= clear_for {
                    transition(state("resolved", Some(now), Some(now), None), Some("resolved"), true)
                } else {
                    transition(state("firing", since, last, Some(clearing)), None, false)
                }
            }
        }
    }
}

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 alert_state_from_value(v: &Value) -> AlertState {
    AlertState {
        phase: v.get("phase").as_str().to_string(),
        since: opt_i64(v.get("since")),
        last_notified_at: opt_i64(v.get("lastNotifiedAt")),
        clear_since: opt_i64(v.get("clearSince")),
    }
}

pub fn alert_state_to_value(s: &AlertState) -> Value {
    Value::obj(vec![
        ("phase", Value::str(&s.phase)),
        ("since", opt_to_value(s.since)),
        ("lastNotifiedAt", opt_to_value(s.last_notified_at)),
        ("clearSince", opt_to_value(s.clear_since)),
    ])
}

pub fn alert_policy_from_value(v: &Value) -> AlertPolicy {
    AlertPolicy {
        for_seconds: v.get("forSeconds").as_i64(),
        clear_for_seconds: v.get("clearForSeconds").as_i64(),
        renotify_seconds: opt_i64(v.get("renotifySeconds")),
    }
}

pub fn alert_transition_to_value(t: &AlertTransition) -> Value {
    Value::obj(vec![
        ("state", alert_state_to_value(&t.state)),
        ("notify", match &t.notify { Some(n) => Value::str(n), None => Value::Null }),
        ("changed", Value::Bool(t.changed)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let previous = alert_state_from_value(&args[0]);
    let now = match &args[2] {
        Value::Int(i) => *i,
        _ => panic!("now must be a whole number of seconds"),
    };
    let policy = alert_policy_from_value(&args[3]);
    alert_transition_to_value(&next_alert_state(&previous, args[1].as_bool(), now, &policy))
}