use super::funejson::Value; fn state(phase: &str, since: Option, last: Option, clear_since: Option) -> 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 { 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 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)) }