#![cfg(all(feature = "tokio", feature = "flow"))]
use std::borrow::Cow;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use flowscope::Timestamp;
use netring::anomaly::Severity;
use netring::anomaly::sink::AnomalySink;
use netring::layer::{DedupeAnomalies, Layer, MinSeverity, RateLimitAnomalies, Sample, Tee};
#[derive(Default)]
struct CaptureSink {
seen: Arc<Mutex<Vec<(&'static str, Severity)>>>,
}
impl CaptureSink {
fn list(&self) -> Arc<Mutex<Vec<(&'static str, Severity)>>> {
Arc::clone(&self.seen)
}
}
impl AnomalySink for CaptureSink {
fn write(
&mut self,
kind: &'static str,
severity: Severity,
_ts: Timestamp,
_key: Option<&dyn netring::anomaly::Key>,
_observations: &[(&'static str, Cow<'_, str>)],
_metrics: &[(&'static str, f64)],
) {
self.seen.lock().unwrap().push((kind, severity));
}
}
fn compose(layers: Vec<Box<dyn Layer>>, base: Box<dyn AnomalySink>) -> Box<dyn AnomalySink> {
let mut sink = base;
for layer in layers.into_iter().rev() {
sink = layer.wrap(sink);
}
sink
}
#[test]
fn min_severity_warning_only_passes_warning_and_above() {
let base = CaptureSink::default();
let calls = base.list();
let layers: Vec<Box<dyn Layer>> = vec![Box::new(MinSeverity::warning())];
let mut sink = compose(layers, Box::new(base));
sink.begin("I", Severity::Info, Timestamp::new(0, 0)).emit();
sink.begin("W", Severity::Warning, Timestamp::new(0, 0))
.emit();
sink.begin("E", Severity::Error, Timestamp::new(0, 0))
.emit();
sink.begin("C", Severity::Critical, Timestamp::new(0, 0))
.emit();
assert_eq!(
*calls.lock().unwrap(),
vec![
("W", Severity::Warning),
("E", Severity::Error),
("C", Severity::Critical),
]
);
}
#[test]
fn dedupe_drops_repeats_by_kind_plus_key() {
let base = CaptureSink::default();
let calls = base.list();
let layers: Vec<Box<dyn Layer>> =
vec![Box::new(DedupeAnomalies::within(Duration::from_secs(60)))];
let mut sink = compose(layers, Box::new(base));
for _ in 0..3 {
sink.begin("X", Severity::Info, Timestamp::new(0, 0))
.with_key(&7u32)
.emit();
}
for _ in 0..2 {
sink.begin("X", Severity::Info, Timestamp::new(0, 0))
.with_key(&8u32)
.emit();
}
let calls = calls.lock().unwrap();
assert_eq!(calls.len(), 2);
}
#[test]
fn rate_limit_enforces_per_kind_budget() {
let base = CaptureSink::default();
let calls = base.list();
let layers: Vec<Box<dyn Layer>> = vec![Box::new(RateLimitAnomalies::new(
3,
Duration::from_secs(60),
))];
let mut sink = compose(layers, Box::new(base));
for _ in 0..10 {
sink.begin("Burst", Severity::Info, Timestamp::new(0, 0))
.emit();
}
for _ in 0..10 {
sink.begin("Other", Severity::Info, Timestamp::new(0, 0))
.emit();
}
assert_eq!(calls.lock().unwrap().len(), 6, "3+3");
}
#[test]
fn sample_at_rate_zero_drops_everything() {
let base = CaptureSink::default();
let calls = base.list();
let layers: Vec<Box<dyn Layer>> = vec![Box::new(Sample::at_rate(0.0))];
let mut sink = compose(layers, Box::new(base));
for _ in 0..1000 {
sink.begin("X", Severity::Info, Timestamp::new(0, 0)).emit();
}
assert_eq!(calls.lock().unwrap().len(), 0);
}
#[test]
fn sample_at_rate_one_keeps_everything() {
let base = CaptureSink::default();
let calls = base.list();
let layers: Vec<Box<dyn Layer>> = vec![Box::new(Sample::at_rate(1.0))];
let mut sink = compose(layers, Box::new(base));
for _ in 0..100 {
sink.begin("X", Severity::Info, Timestamp::new(0, 0)).emit();
}
assert_eq!(calls.lock().unwrap().len(), 100);
}
#[test]
fn tee_fans_out_to_both_destinations() {
let primary = CaptureSink::default();
let secondary = CaptureSink::default();
let p_list = primary.list();
let s_list = secondary.list();
let layers: Vec<Box<dyn Layer>> = vec![Box::new(Tee::into(secondary))];
let mut sink = compose(layers, Box::new(primary));
for _ in 0..4 {
sink.begin("T", Severity::Info, Timestamp::new(0, 0)).emit();
}
assert_eq!(p_list.lock().unwrap().len(), 4);
assert_eq!(s_list.lock().unwrap().len(), 4);
}
#[test]
fn full_stack_min_severity_then_dedupe_then_rate_limit() {
let base = CaptureSink::default();
let calls = base.list();
let layers: Vec<Box<dyn Layer>> = vec![
Box::new(MinSeverity::warning()),
Box::new(DedupeAnomalies::within(Duration::from_secs(60))),
Box::new(RateLimitAnomalies::new(2, Duration::from_secs(60))),
];
let mut sink = compose(layers, Box::new(base));
sink.begin("X", Severity::Info, Timestamp::new(0, 0))
.with_key(&1u32)
.emit();
sink.begin("X", Severity::Warning, Timestamp::new(0, 0))
.with_key(&1u32)
.emit();
sink.begin("X", Severity::Warning, Timestamp::new(0, 0))
.with_key(&1u32)
.emit();
sink.begin("X", Severity::Warning, Timestamp::new(0, 0))
.with_key(&2u32)
.emit();
sink.begin("X", Severity::Warning, Timestamp::new(0, 0))
.with_key(&3u32)
.emit();
let calls = calls.lock().unwrap();
assert_eq!(calls.len(), 2, "RateLimit at 2/kind/60s");
assert!(calls.iter().all(|(_, s)| *s == Severity::Warning));
}
#[test]
fn tee_inside_a_chain_still_fans_out() {
let primary = CaptureSink::default();
let secondary = CaptureSink::default();
let p_list = primary.list();
let s_list = secondary.list();
let layers: Vec<Box<dyn Layer>> = vec![
Box::new(MinSeverity::warning()),
Box::new(Tee::into(secondary)),
];
let mut sink = compose(layers, Box::new(primary));
sink.begin("I", Severity::Info, Timestamp::new(0, 0)).emit();
sink.begin("W", Severity::Warning, Timestamp::new(0, 0))
.emit();
assert_eq!(p_list.lock().unwrap().len(), 1);
assert_eq!(s_list.lock().unwrap().len(), 1);
}