use super::funejson::Value; use super::monitor_check_status::{checks_from_value, Check}; /// Seconds up, degraded, down and unknown over [from, to). Each check's status /// holds from its `at` until the next check, but for at most max_gap_seconds: /// a monitor that stopped reporting has not proved the target was up. /// /// # Panics /// Panics when from is after to, max_gap_seconds is below 1, the checks are /// not strictly ascending, or a status is unknown. pub fn uptime(checks: &[Check], from: i64, to: i64, max_gap_seconds: i64) -> UptimeReport { if from > to { panic!("from must not be after to: {} > {}", from, to); } if max_gap_seconds < 1 { panic!("maxGapSeconds must be a whole number of at least 1, received {}", max_gap_seconds); } let (mut up, mut degraded, mut down) = (0i64, 0i64, 0i64); let (mut count, mut down_count) = (0i64, 0i64); for (i, c) in checks.iter().enumerate() { if c.status != "up" && c.status != "degraded" && c.status != "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 c.at >= from && c.at < to { count += 1; if c.status == "down" { down_count += 1; } } // The span this check vouches for, clipped to the window. let mut end = c.at.saturating_add(max_gap_seconds); if i + 1 < checks.len() && checks[i + 1].at < end { end = checks[i + 1].at; } let s = c.at.max(from); let e = end.min(to); if e > s { match c.status.as_str() { "up" => up += e - s, "degraded" => degraded += e - s, _ => down += e - s, } } } let known = up + degraded + down; // Floored in i128: never 100.00% while a down second exists. let uptime_basis_points = if known == 0 { None } else { Some(((up + degraded) as i128 * 10000 / known as i128) as i64) }; UptimeReport { up_seconds: up, degraded_seconds: degraded, down_seconds: down, unknown_seconds: to - from - known, uptime_basis_points, checks: count, down_checks: down_count, } } pub fn uptime_report_to_value(r: &UptimeReport) -> Value { Value::obj(vec![ ("upSeconds", Value::Int(r.up_seconds)), ("degradedSeconds", Value::Int(r.degraded_seconds)), ("downSeconds", Value::Int(r.down_seconds)), ("unknownSeconds", Value::Int(r.unknown_seconds)), ("uptimeBasisPoints", r.uptime_basis_points.map(Value::Int).unwrap_or(Value::Null)), ("checks", Value::Int(r.checks)), ("downChecks", Value::Int(r.down_checks)), ]) } fn whole(v: &Value, msg: &str) -> i64 { match v { Value::Int(i) => *i, other => panic!("{}{}", msg, other), } } pub fn fune_vector(args: &[Value]) -> Value { let checks = checks_from_value(&args[0]); let from = match &args[1] { Value::Int(i) => *i, _ => panic!("from and to must be whole seconds") }; let to = match &args[2] { Value::Int(i) => *i, _ => panic!("from and to must be whole seconds") }; let gap = whole(&args[3], "maxGapSeconds must be a whole number of at least 1, received "); uptime_report_to_value(&uptime(&checks, from, to, gap)) }