use crate::slo::Slo;
use crate::window::Window;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct BurnRate(f64);
impl BurnRate {
pub fn new(value: f64) -> Self {
Self(value)
}
pub fn from_error_ratio(observed_error_ratio: f64, slo: &Slo) -> Self {
let budget = slo.error_budget_ratio();
if budget <= 0.0 {
return Self(f64::INFINITY);
}
Self(observed_error_ratio / budget)
}
pub fn value(&self) -> f64 {
self.0
}
pub fn budget_consumed_over(&self, window: Window, period: Window) -> f64 {
self.0 * (window.as_secs_f64() / period.as_secs_f64())
}
pub fn time_to_exhaustion(
&self,
remaining_budget_ratio: f64,
period: Window,
) -> Option<std::time::Duration> {
if self.0 <= 0.0 || !self.0.is_finite() {
return None;
}
let secs = (period.as_secs_f64() * remaining_budget_ratio / self.0).max(0.0);
Some(std::time::Duration::from_secs_f64(secs))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Page,
Ticket,
}
impl Severity {
pub fn label(&self) -> &'static str {
match self {
Severity::Page => "page",
Severity::Ticket => "ticket",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AlertWindow {
pub severity: Severity,
pub long: Window,
pub short: Window,
pub factor: f64,
}
impl AlertWindow {
pub fn threshold(&self, slo: &Slo) -> f64 {
self.factor * slo.error_budget_ratio()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MwmbrConfig {
pub windows: Vec<AlertWindow>,
}
impl MwmbrConfig {
pub fn sre_default() -> Self {
Self {
windows: vec![
AlertWindow {
severity: Severity::Page,
long: Window::hours(1),
short: Window::minutes(5),
factor: 14.4,
},
AlertWindow {
severity: Severity::Page,
long: Window::hours(6),
short: Window::minutes(30),
factor: 6.0,
},
AlertWindow {
severity: Severity::Ticket,
long: Window::days(1),
short: Window::hours(2),
factor: 3.0,
},
AlertWindow {
severity: Severity::Ticket,
long: Window::days(3),
short: Window::hours(6),
factor: 1.0,
},
],
}
}
pub fn for_severity(&self, severity: Severity) -> impl Iterator<Item = &AlertWindow> {
self.windows.iter().filter(move |w| w.severity == severity)
}
pub fn lookback_windows(&self) -> Vec<Window> {
let mut windows: Vec<Window> = self
.windows
.iter()
.flat_map(|w| [w.short, w.long])
.collect();
windows.sort();
windows.dedup();
windows
}
}
impl Default for MwmbrConfig {
fn default() -> Self {
Self::sre_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::slo::Objective;
fn slo_999() -> Slo {
Slo::new(Objective::percent(99.9).unwrap(), Window::days(30))
}
#[test]
fn budget_consumed_matches_workbook_constants() {
let period = Window::days(30);
let page_fast = BurnRate::new(14.4);
assert!((page_fast.budget_consumed_over(Window::hours(1), period) - 0.02).abs() < 1e-9);
let page_slow = BurnRate::new(6.0);
assert!((page_slow.budget_consumed_over(Window::hours(6), period) - 0.05).abs() < 1e-9);
let ticket_fast = BurnRate::new(3.0);
assert!((ticket_fast.budget_consumed_over(Window::days(1), period) - 0.10).abs() < 1e-9);
let ticket_slow = BurnRate::new(1.0);
assert!((ticket_slow.budget_consumed_over(Window::days(3), period) - 0.10).abs() < 1e-9);
}
#[test]
fn from_error_ratio_inverts_budget() {
let slo = slo_999();
let br = BurnRate::from_error_ratio(0.001, &slo);
assert!((br.value() - 1.0).abs() < 1e-9);
let br = BurnRate::from_error_ratio(0.01, &slo);
assert!((br.value() - 10.0).abs() < 1e-9);
}
#[test]
fn thresholds_are_factor_times_budget() {
let slo = slo_999();
let cfg = MwmbrConfig::sre_default();
let first = cfg.windows[0];
assert_eq!(first.factor, 14.4);
assert!((first.threshold(&slo) - 0.0144).abs() < 1e-9);
}
#[test]
fn default_lookback_windows_are_the_six_sloth_windows() {
let cfg = MwmbrConfig::sre_default();
assert_eq!(
cfg.lookback_windows(),
vec![
Window::minutes(5),
Window::minutes(30),
Window::hours(1),
Window::hours(2),
Window::hours(6),
Window::days(1),
Window::days(3),
]
);
}
#[test]
fn severity_filter_splits_page_and_ticket() {
let cfg = MwmbrConfig::sre_default();
assert_eq!(cfg.for_severity(Severity::Page).count(), 2);
assert_eq!(cfg.for_severity(Severity::Ticket).count(), 2);
}
}