use super::funejson::Value; use super::net_latency_summary::{latency_summary_from_value, LatencySummary}; // Integer arithmetic only, so the reason text is identical in every language. fn percent(basis_points: i64) -> String { format!("{}.{:02}%", basis_points / 100, basis_points % 100) } fn ms(us: i64) -> String { format!("{}.{:03} ms", us / 1000, us % 1000) } /// Up, degraded or down, and every reason, from one latency summary. /// /// Down wins outright: once a device is down, its p95 and jitter are beside /// the point. Otherwise every threshold crossed is listed, loss first, so an /// alert says all that is wrong at once. /// /// # Panics /// Panics on an inconsistent summary or thresholds out of range. pub fn device_status(summary: &LatencySummary, thresholds: &StatusThresholds) -> StatusResult { let (sent, received, lost) = (summary.sent, summary.received, summary.lost); if sent < 1 || received < 0 || lost < 0 || received + lost != sent { panic!( "summary is inconsistent: sent {}, received {}, lost {}", sent, received, lost ); } let deg_loss = thresholds.degraded_loss_basis_points; let down_loss = thresholds.down_loss_basis_points; let p95_limit = thresholds.degraded_p95_us; let jitter_limit = thresholds.degraded_jitter_us; if !(1..=10000).contains(&down_loss) { panic!("downLossBasisPoints must be 1 to 10000, received {}", down_loss); } if deg_loss < 1 || deg_loss > down_loss { panic!( "degradedLossBasisPoints must be 1 to downLossBasisPoints ({}), received {}", down_loss, deg_loss ); } if p95_limit < 1 { panic!("degradedP95Us must be at least 1, received {}", p95_limit); } if let Some(j) = jitter_limit { if j < 1 { panic!("degradedJitterUs must be null or at least 1, received {}", j); } } if received == 0 { return StatusResult { status: "down".to_string(), reasons: vec![format!("no replies: {} of {} probes lost", lost, sent)], }; } let loss = summary.loss_basis_points; if loss >= down_loss { return StatusResult { status: "down".to_string(), reasons: vec![format!( "loss {} at or above the down threshold {}", percent(loss), percent(down_loss) )], }; } let mut reasons: Vec = Vec::new(); if loss >= deg_loss { reasons.push(format!("loss {} at or above {}", percent(loss), percent(deg_loss))); } if let Some(p95) = summary.p95_us { if p95 >= p95_limit { reasons.push(format!("p95 {} at or above {}", ms(p95), ms(p95_limit))); } } if let (Some(limit), Some(jitter)) = (jitter_limit, summary.jitter_us) { if jitter >= limit { reasons.push(format!("jitter {} at or above {}", ms(jitter), ms(limit))); } } let status = if reasons.is_empty() { "up" } else { "degraded" }; StatusResult { status: status.to_string(), reasons, } } pub fn status_result_to_value(r: &StatusResult) -> Value { Value::obj(vec![ ("status", Value::str(&r.status)), ( "reasons", Value::Arr(r.reasons.iter().map(|s| Value::str(s)).collect()), ), ]) } pub fn fune_vector(args: &[Value]) -> Value { let summary = latency_summary_from_value(&args[0]); let t = &args[1]; let jitter = t.get("degradedJitterUs"); let thresholds = StatusThresholds { degraded_loss_basis_points: t.get("degradedLossBasisPoints").as_i64(), down_loss_basis_points: t.get("downLossBasisPoints").as_i64(), degraded_p95_us: t.get("degradedP95Us").as_i64(), degraded_jitter_us: if jitter.is_null() { None } else { Some(jitter.as_i64()) }, }; status_result_to_value(&device_status(&summary, &thresholds)) }