use super::funejson::Value; use super::monitor_check_status::{checks_from_value, Check}; /// The incidents in a check series. A run of consecutive bad checks is one /// incident, from its first bad check to the first good check after it, but /// only when it holds at least confirm_checks checks: a single failed probe is /// noise, not an outage, and would otherwise page and skew MTTR. /// /// # Panics /// Panics on an incident status other than down or degraded, confirm_checks /// below 1, an unknown or misordered check, or now before the last check. pub fn find_incidents(checks: &[Check], incident_status: &str, confirm_checks: i64, now: i64) -> Vec { if incident_status != "down" && incident_status != "degraded" { panic!("incidentStatus must be down or degraded, received {}", incident_status); } if confirm_checks < 1 { panic!("confirmChecks must be at least 1, received {}", confirm_checks); } for (i, c) in checks.iter().enumerate() { if !matches!(c.status.as_str(), "up" | "degraded" | "down") { panic!("unknown check status: {}", c.status); } if i > 0 && c.at <= checks[i - 1].at { panic!("checks must be in strictly ascending time order: {} follows {}", c.at, checks[i - 1].at); } } if let Some(last) = checks.last() { if now < last.at { panic!("now {} is before the last check at {}", now, last.at); } } let bad = |s: &str| s == "down" || (incident_status == "degraded" && s == "degraded"); let mut out = Vec::new(); let mut i = 0; while i < checks.len() { if !bad(&checks[i].status) { i += 1; continue; } let start = checks[i].at; let mut count = 0i64; let mut worst = "degraded"; while i < checks.len() && bad(&checks[i].status) { if checks[i].status == "down" { worst = "down"; } count += 1; i += 1; } let end = if i < checks.len() { Some(checks[i].at) } else { None }; if count >= confirm_checks { out.push(Incident { start, end, duration_seconds: end.unwrap_or(now) - start, worst: worst.to_string(), checks: count, }); } } out } /// An `Incident` from its JSON form, for adapters of capabilities built on this one. pub fn incident_from_value(v: &Value) -> Incident { let end = v.get("end"); Incident { start: v.get("start").as_i64(), end: if end.is_null() { None } else { Some(end.as_i64()) }, duration_seconds: v.get("durationSeconds").as_i64(), worst: v.get("worst").as_str().to_string(), checks: v.get("checks").as_i64(), } } pub fn incidents_from_value(v: &Value) -> Vec { v.as_arr().iter().map(incident_from_value).collect() } pub fn incident_to_value(x: &Incident) -> Value { Value::obj(vec![ ("start", Value::Int(x.start)), ("end", match x.end { Some(e) => Value::Int(e), None => Value::Null }), ("durationSeconds", Value::Int(x.duration_seconds)), ("worst", Value::str(&x.worst)), ("checks", Value::Int(x.checks)), ]) } fn whole(v: &Value, message: &str) -> i64 { match v { Value::Int(i) => *i, _ => panic!("{}", message), } } pub fn fune_vector(args: &[Value]) -> Value { let checks = checks_from_value(&args[0]); let confirm = match &args[2] { Value::Int(i) => *i, other => panic!("confirmChecks must be at least 1, received {:?}", other), }; let now = whole(&args[3], "now must be a whole number of seconds"); let out = find_incidents(&checks, args[1].as_str(), confirm, now); Value::Arr(out.iter().map(incident_to_value).collect()) }