use std::collections::VecDeque;
use serde_json::{json, Value};
use crate::monitor::record::Severity;
pub const COLLAPSE_WINDOW_MS: u64 = 10_000;
pub const MAX_EVENTS: usize = 200;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventClass {
RankLost,
Drift,
ControlDrop,
Overflow,
}
impl EventClass {
pub fn as_str(self) -> &'static str {
match self {
EventClass::RankLost => "rank_lost",
EventClass::Drift => "drift",
EventClass::ControlDrop => "control_drop",
EventClass::Overflow => "overflow",
}
}
pub fn severity(self) -> Severity {
match self {
EventClass::RankLost | EventClass::ControlDrop => Severity::Critical,
EventClass::Drift | EventClass::Overflow => Severity::Warn,
}
}
}
#[derive(Debug, Clone)]
struct Alert {
class: EventClass,
path: String,
detail: String,
ts: u64,
window_ms: u64,
total: u64,
pending: u64,
}
impl Alert {
fn to_json(&self, count: u64) -> Value {
json!({
"v": 1,
"ts": self.ts,
"sev": self.class.severity().as_str(),
"path": self.path,
"kind": "event",
"class": self.class.as_str(),
"detail": self.detail,
"count": count,
})
}
}
#[derive(Debug)]
pub struct EventLane {
events: VecDeque<Alert>,
collapse_window_ms: u64,
max_events: usize,
dropped_total: u64,
dropped_pending: u64,
dropped_ts: u64,
overflow_window_ms: Option<u64>,
}
impl Default for EventLane {
fn default() -> Self {
Self::new()
}
}
impl EventLane {
pub fn new() -> Self {
Self::with_limits(COLLAPSE_WINDOW_MS, MAX_EVENTS)
}
pub fn with_limits(collapse_window_ms: u64, max_events: usize) -> Self {
EventLane {
events: VecDeque::new(),
collapse_window_ms,
max_events: max_events.max(1),
dropped_total: 0,
dropped_pending: 0,
dropped_ts: 0,
overflow_window_ms: None,
}
}
pub fn dropped(&self) -> u64 {
self.dropped_total
}
pub fn record(
&mut self,
class: EventClass,
path: &str,
detail: impl Into<String>,
now_ms: u64,
) -> Vec<Value> {
let detail = detail.into();
let mut out = Vec::new();
let found = self
.events
.iter()
.rposition(|a| a.class == class && a.path == path);
if let Some(i) = found {
let mut a = self.events.remove(i).expect("index came from rposition");
a.ts = now_ms;
a.total += 1;
a.detail = detail;
if now_ms.saturating_sub(a.window_ms) < self.collapse_window_ms {
a.pending += 1;
} else {
let count = a.pending + 1;
a.pending = 0;
a.window_ms = now_ms;
out.push(a.to_json(count));
}
self.events.push_back(a);
} else {
let alert = Alert {
class,
path: path.to_string(),
detail,
ts: now_ms,
window_ms: now_ms,
total: 1,
pending: 0,
};
let json = alert.to_json(1);
self.push(alert, &mut out);
out.push(json);
}
self.maybe_emit_overflow(now_ms, &mut out);
out
}
pub fn flush(&mut self, now_ms: u64) -> Vec<Value> {
let mut out = Vec::new();
for a in self.events.iter_mut() {
if a.pending > 0 {
out.push(a.to_json(a.pending));
a.pending = 0;
a.window_ms = now_ms;
}
}
if self.dropped_pending > 0 {
out.push(self.overflow_json(self.dropped_pending));
self.dropped_pending = 0;
self.overflow_window_ms = Some(now_ms);
}
out
}
pub fn live(&self) -> Vec<Value> {
let mut out: Vec<Value> =
self.events.iter().map(|a| a.to_json(a.total)).collect();
if self.dropped_total > 0 {
out.push(self.overflow_json(self.dropped_total));
}
out
}
fn push(&mut self, alert: Alert, out: &mut Vec<Value>) {
while self.events.len() >= self.max_events {
let Some(old) = self.events.pop_front() else {
break;
};
if old.pending > 0 {
out.push(old.to_json(old.pending));
}
self.dropped_total += 1;
self.dropped_pending += 1;
self.dropped_ts = alert.ts;
}
self.events.push_back(alert);
}
fn maybe_emit_overflow(&mut self, now_ms: u64, out: &mut Vec<Value>) {
if self.dropped_pending == 0 {
return;
}
let due = self
.overflow_window_ms
.is_none_or(|t| now_ms.saturating_sub(t) >= self.collapse_window_ms);
if !due {
return;
}
out.push(self.overflow_json(self.dropped_pending));
self.dropped_pending = 0;
self.overflow_window_ms = Some(now_ms);
}
fn overflow_json(&self, count: u64) -> Value {
json!({
"v": 1,
"ts": self.dropped_ts,
"sev": EventClass::Overflow.severity().as_str(),
"path": "root",
"kind": "event",
"class": EventClass::Overflow.as_str(),
"detail": format!(
"{} alert(s) dropped by the {}-entry live cap",
self.dropped_total, self.max_events,
),
"count": count,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn counts(records: &[Value], class: &str) -> Vec<u64> {
records
.iter()
.filter(|r| r["class"] == class)
.map(|r| r["count"].as_u64().unwrap())
.collect()
}
#[test]
fn first_occurrence_emits_immediately() {
let mut lane = EventLane::new();
let out = lane.record(EventClass::RankLost, "root/h1/rank2", "stale 34s", 1_000);
assert_eq!(out.len(), 1);
assert_eq!(out[0]["kind"], "event");
assert_eq!(out[0]["class"], "rank_lost");
assert_eq!(out[0]["sev"], "critical");
assert_eq!(out[0]["path"], "root/h1/rank2");
assert_eq!(out[0]["detail"], "stale 34s");
assert_eq!(out[0]["count"], 1);
assert_eq!(out[0]["ts"], 1_000);
}
#[test]
fn repeats_inside_the_window_are_absorbed() {
let mut lane = EventLane::with_limits(10_000, 200);
assert_eq!(lane.record(EventClass::Drift, "root", "d=1", 0).len(), 1);
for t in [100, 500, 9_999] {
assert!(lane.record(EventClass::Drift, "root", "d=1", t).is_empty());
}
let live = lane.live();
assert_eq!(live.len(), 1);
assert_eq!(live[0]["count"], 4);
}
#[test]
fn counts_over_the_stream_are_exact() {
let mut lane = EventLane::with_limits(1_000, 200);
let mut emitted = 0u64;
let mut occurrences = 0u64;
for t in [0, 100, 200, 300, 1_500, 1_600, 3_000] {
let out = lane.record(EventClass::Drift, "root", "d", t);
occurrences += 1;
emitted += counts(&out, "drift").iter().sum::<u64>();
}
emitted += counts(&lane.flush(9_999), "drift").iter().sum::<u64>();
assert_eq!(emitted, occurrences);
assert_eq!(lane.live()[0]["count"], occurrences);
}
#[test]
fn distinct_paths_do_not_collapse_into_each_other() {
let mut lane = EventLane::new();
assert_eq!(lane.record(EventClass::RankLost, "root/rank0", "a", 0).len(), 1);
assert_eq!(lane.record(EventClass::RankLost, "root/rank1", "b", 1).len(), 1);
assert_eq!(lane.live().len(), 2);
}
#[test]
fn distinct_classes_on_one_path_do_not_collapse() {
let mut lane = EventLane::new();
assert_eq!(lane.record(EventClass::Drift, "root", "a", 0).len(), 1);
assert_eq!(lane.record(EventClass::ControlDrop, "root", "b", 1).len(), 1);
assert_eq!(lane.live().len(), 2);
}
#[test]
fn latest_detail_and_ts_win_on_collapse() {
let mut lane = EventLane::with_limits(10_000, 200);
lane.record(EventClass::Drift, "root", "first", 0);
lane.record(EventClass::Drift, "root", "latest", 500);
let live = lane.live();
assert_eq!(live[0]["detail"], "latest");
assert_eq!(live[0]["ts"], 500);
}
#[test]
fn cap_evicts_least_recently_active_and_says_so() {
let mut lane = EventLane::with_limits(10_000, 2);
lane.record(EventClass::RankLost, "root/rank0", "a", 0);
lane.record(EventClass::RankLost, "root/rank1", "b", 1);
let out = lane.record(EventClass::RankLost, "root/rank2", "c", 2);
assert_eq!(out.len(), 2);
assert_eq!(out[1]["class"], "overflow");
assert_eq!(out[1]["sev"], "warn");
assert_eq!(out[1]["path"], "root");
assert_eq!(out[1]["count"], 1);
assert_eq!(lane.dropped(), 1);
let live = lane.live();
assert_eq!(live.len(), 3);
assert_eq!(live[0]["path"], "root/rank1");
assert_eq!(live[2]["class"], "overflow");
}
#[test]
fn overflow_notice_is_itself_collapsed() {
let mut lane = EventLane::with_limits(10_000, 1);
lane.record(EventClass::RankLost, "root/rank0", "a", 0);
let a = lane.record(EventClass::RankLost, "root/rank1", "b", 1);
assert_eq!(counts(&a, "overflow"), vec![1]);
let b = lane.record(EventClass::RankLost, "root/rank2", "c", 2);
assert!(counts(&b, "overflow").is_empty());
let c = lane.record(EventClass::RankLost, "root/rank3", "d", 20_000);
assert_eq!(counts(&c, "overflow"), vec![2]);
assert_eq!(lane.dropped(), 3);
}
#[test]
fn a_still_firing_alert_outlives_a_stopped_one() {
let mut lane = EventLane::with_limits(10_000, 2);
lane.record(EventClass::RankLost, "root/rank0", "once", 0);
lane.record(EventClass::Drift, "root/rank1", "again", 1);
lane.record(EventClass::Drift, "root/rank1", "again", 2);
lane.record(EventClass::ControlDrop, "root", "new", 3);
let paths: Vec<String> = lane
.live()
.iter()
.filter(|e| e["class"] != "overflow")
.map(|e| e["path"].as_str().unwrap().to_string())
.collect();
assert_eq!(paths, vec!["root/rank1", "root"], "{:?}", lane.live());
assert_eq!(lane.dropped(), 1);
}
#[test]
fn eviction_does_not_swallow_owed_repeats() {
let mut lane = EventLane::with_limits(10_000, 1);
lane.record(EventClass::Drift, "root", "x", 0);
lane.record(EventClass::Drift, "root", "x", 10); lane.record(EventClass::Drift, "root", "x", 20); let out = lane.record(EventClass::RankLost, "root/rank0", "died", 30);
assert_eq!(counts(&out, "drift"), vec![2]);
}
#[test]
fn flush_closes_open_windows_and_is_idempotent() {
let mut lane = EventLane::with_limits(10_000, 200);
lane.record(EventClass::Drift, "root", "x", 0);
lane.record(EventClass::Drift, "root", "x", 10);
let out = lane.flush(100);
assert_eq!(counts(&out, "drift"), vec![1]);
assert!(lane.flush(200).is_empty());
}
#[test]
fn idle_lane_emits_nothing() {
let mut lane = EventLane::new();
assert!(lane.live().is_empty());
assert!(lane.flush(1_000).is_empty());
assert_eq!(lane.dropped(), 0);
}
#[test]
fn severities_follow_the_class() {
assert_eq!(EventClass::RankLost.severity(), Severity::Critical);
assert_eq!(EventClass::ControlDrop.severity(), Severity::Critical);
assert_eq!(EventClass::Drift.severity(), Severity::Warn);
assert_eq!(EventClass::Overflow.severity(), Severity::Warn);
}
}