use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::RwLock;
#[derive(Debug, Clone)]
pub struct SloConfig {
pub target_success_rate: f64,
pub long_window: Duration,
pub short_window: Duration,
pub burn_rate_threshold: f64,
pub ticket_long_window: Duration,
pub ticket_short_window: Duration,
pub ticket_burn_rate_threshold: f64,
}
impl Default for SloConfig {
fn default() -> Self {
Self {
target_success_rate: 0.999,
long_window: Duration::from_secs(3600),
short_window: Duration::from_secs(300),
burn_rate_threshold: 14.4,
ticket_long_window: Duration::from_secs(6 * 3600),
ticket_short_window: Duration::from_secs(30 * 60),
ticket_burn_rate_threshold: 6.0,
}
}
}
#[derive(Debug, Clone)]
pub struct SloBurnRate {
pub short_success_rate: f64,
pub long_success_rate: f64,
pub short_burn_rate: f64,
pub long_burn_rate: f64,
pub error_budget_remaining: f64,
pub alerting: bool,
pub ticket_short_success_rate: f64,
pub ticket_long_success_rate: f64,
pub ticket_short_burn_rate: f64,
pub ticket_long_burn_rate: f64,
pub page_alerting: bool,
pub ticket_alerting: bool,
}
impl Default for SloBurnRate {
fn default() -> Self {
Self {
short_success_rate: 1.0,
long_success_rate: 1.0,
short_burn_rate: 0.0,
long_burn_rate: 0.0,
error_budget_remaining: 1.0,
alerting: false,
ticket_short_success_rate: 1.0,
ticket_long_success_rate: 1.0,
ticket_short_burn_rate: 0.0,
ticket_long_burn_rate: 0.0,
page_alerting: false,
ticket_alerting: false,
}
}
}
impl std::fmt::Display for SloBurnRate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SloBurnRate{{page[short={:.4}/{:.2}x, long={:.4}/{:.2}x, alert={}], ticket[short={:.4}/{:.2}x, long={:.4}/{:.2}x, alert={}], budget={:.2}%}}",
self.short_success_rate,
self.short_burn_rate,
self.long_success_rate,
self.long_burn_rate,
self.page_alerting,
self.ticket_short_success_rate,
self.ticket_short_burn_rate,
self.ticket_long_success_rate,
self.ticket_long_burn_rate,
self.ticket_alerting,
self.error_budget_remaining * 100.0,
)
}
}
struct WindowedCounter {
window: Duration,
success: Arc<AtomicU64>,
failure: Arc<AtomicU64>,
window_start: Arc<RwLock<Instant>>,
}
impl WindowedCounter {
fn new(window: Duration) -> Self {
Self {
window,
success: Arc::new(AtomicU64::new(0)),
failure: Arc::new(AtomicU64::new(0)),
window_start: Arc::new(RwLock::new(Instant::now())),
}
}
fn record_success(&self) {
self.rotate_if_needed();
self.success.fetch_add(1, Ordering::Relaxed);
}
fn record_failure(&self) {
self.rotate_if_needed();
self.failure.fetch_add(1, Ordering::Relaxed);
}
fn rotate_if_needed(&self) {
let start = self.window_start.read();
if start.elapsed() >= self.window {
drop(start);
let mut start = self.window_start.write();
if start.elapsed() >= self.window {
self.success.store(0, Ordering::Relaxed);
self.failure.store(0, Ordering::Relaxed);
*start = Instant::now();
}
}
}
fn success_rate(&self) -> f64 {
let s = self.success.load(Ordering::Relaxed);
let f = self.failure.load(Ordering::Relaxed);
let total = s + f;
if total == 0 {
return 1.0;
}
s as f64 / total as f64
}
}
pub struct SloMonitor {
config: SloConfig,
page_short_counter: WindowedCounter,
page_long_counter: WindowedCounter,
ticket_short_counter: WindowedCounter,
ticket_long_counter: WindowedCounter,
total_success: AtomicU64,
total_failure: AtomicU64,
}
impl SloMonitor {
pub fn new(config: SloConfig) -> Self {
let page_short_counter = WindowedCounter::new(config.short_window);
let page_long_counter = WindowedCounter::new(config.long_window);
let ticket_short_counter = WindowedCounter::new(config.ticket_short_window);
let ticket_long_counter = WindowedCounter::new(config.ticket_long_window);
Self {
config,
page_short_counter,
page_long_counter,
ticket_short_counter,
ticket_long_counter,
total_success: AtomicU64::new(0),
total_failure: AtomicU64::new(0),
}
}
pub fn record_success(&self) {
self.page_short_counter.record_success();
self.page_long_counter.record_success();
self.ticket_short_counter.record_success();
self.ticket_long_counter.record_success();
self.total_success.fetch_add(1, Ordering::Relaxed);
}
pub fn record_failure(&self) {
self.page_short_counter.record_failure();
self.page_long_counter.record_failure();
self.ticket_short_counter.record_failure();
self.ticket_long_counter.record_failure();
self.total_failure.fetch_add(1, Ordering::Relaxed);
}
pub fn burn_rate(&self) -> SloBurnRate {
self.page_short_counter.rotate_if_needed();
self.page_long_counter.rotate_if_needed();
self.ticket_short_counter.rotate_if_needed();
self.ticket_long_counter.rotate_if_needed();
let page_short_rate = self.page_short_counter.success_rate();
let page_long_rate = self.page_long_counter.success_rate();
let ticket_short_rate = self.ticket_short_counter.success_rate();
let ticket_long_rate = self.ticket_long_counter.success_rate();
let allowed_error_rate = 1.0 - self.config.target_success_rate;
let page_short_error = 1.0 - page_short_rate;
let page_long_error = 1.0 - page_long_rate;
let ticket_short_error = 1.0 - ticket_short_rate;
let ticket_long_error = 1.0 - ticket_long_rate;
let page_short_burn = if allowed_error_rate > 0.0 {
page_short_error / allowed_error_rate
} else {
0.0
};
let page_long_burn = if allowed_error_rate > 0.0 {
page_long_error / allowed_error_rate
} else {
0.0
};
let ticket_short_burn = if allowed_error_rate > 0.0 {
ticket_short_error / allowed_error_rate
} else {
0.0
};
let ticket_long_burn = if allowed_error_rate > 0.0 {
ticket_long_error / allowed_error_rate
} else {
0.0
};
let total_success = self.total_success.load(Ordering::Relaxed);
let total_failure = self.total_failure.load(Ordering::Relaxed);
let total = total_success + total_failure;
let error_budget_remaining = if total == 0 {
1.0
} else {
let actual_error_rate = total_failure as f64 / total as f64;
let consumed = (actual_error_rate / allowed_error_rate).min(1.0);
1.0 - consumed
};
let page_alerting = page_short_burn > self.config.burn_rate_threshold
&& page_long_burn > self.config.burn_rate_threshold;
let ticket_alerting = ticket_short_burn > self.config.ticket_burn_rate_threshold
&& ticket_long_burn > self.config.ticket_burn_rate_threshold;
SloBurnRate {
short_success_rate: page_short_rate,
long_success_rate: page_long_rate,
short_burn_rate: page_short_burn,
long_burn_rate: page_long_burn,
error_budget_remaining,
alerting: page_alerting,
ticket_short_success_rate: ticket_short_rate,
ticket_long_success_rate: ticket_long_rate,
ticket_short_burn_rate: ticket_short_burn,
ticket_long_burn_rate: ticket_long_burn,
page_alerting,
ticket_alerting,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn test_slo_no_data() {
let monitor = SloMonitor::new(SloConfig::default());
let rate = monitor.burn_rate();
assert_eq!(rate.short_success_rate, 1.0);
assert_eq!(rate.long_success_rate, 1.0);
assert!(!rate.alerting);
assert!(!rate.page_alerting);
assert!(!rate.ticket_alerting);
}
#[test]
fn test_slo_all_success() {
let monitor = SloMonitor::new(SloConfig::default());
for _ in 0..1000 {
monitor.record_success();
}
let rate = monitor.burn_rate();
assert_eq!(rate.short_success_rate, 1.0);
assert!(!rate.alerting);
assert!(!rate.page_alerting);
assert!(!rate.ticket_alerting);
}
#[test]
fn test_slo_with_failures() {
let monitor = SloMonitor::new(SloConfig {
target_success_rate: 0.99,
short_window: Duration::from_millis(100),
long_window: Duration::from_millis(200),
burn_rate_threshold: 1.0,
ticket_short_window: Duration::from_millis(300),
ticket_long_window: Duration::from_millis(500),
ticket_burn_rate_threshold: 1.0,
});
for _ in 0..1000 {
monitor.record_success();
}
for _ in 0..100 {
monitor.record_failure();
}
let rate = monitor.burn_rate();
assert!(rate.short_success_rate < 0.99);
assert!(rate.short_burn_rate > 1.0);
assert!(rate.error_budget_remaining < 1.0);
assert!(rate.ticket_short_burn_rate > 1.0);
}
#[test]
fn test_slo_page_alerting() {
let monitor = SloMonitor::new(SloConfig {
target_success_rate: 0.999,
short_window: Duration::from_millis(100),
long_window: Duration::from_millis(200),
burn_rate_threshold: 1.0,
ticket_short_window: Duration::from_millis(300),
ticket_long_window: Duration::from_millis(500),
ticket_burn_rate_threshold: 100.0, });
for _ in 0..100 {
monitor.record_success();
}
for _ in 0..10 {
monitor.record_failure();
}
let rate = monitor.burn_rate();
assert!(rate.page_alerting, "Page should be alerting: {:?}", rate);
assert!(rate.alerting);
assert!(
!rate.ticket_alerting,
"Ticket should NOT alerting: {:?}",
rate
);
}
#[test]
fn test_slo_ticket_alerting_independent() {
let monitor = SloMonitor::new(SloConfig {
target_success_rate: 0.999,
short_window: Duration::from_millis(50),
long_window: Duration::from_millis(80),
burn_rate_threshold: 1.0,
ticket_short_window: Duration::from_millis(500),
ticket_long_window: Duration::from_millis(1000),
ticket_burn_rate_threshold: 1.0,
});
for _ in 0..100 {
monitor.record_success();
}
for _ in 0..10 {
monitor.record_failure();
}
let rate = monitor.burn_rate();
assert!(
rate.ticket_alerting,
"Ticket should be alerting: {:?}",
rate
);
sleep(Duration::from_millis(200));
let rate2 = monitor.burn_rate();
assert!(
!rate2.page_alerting,
"Page should NOT alerting after window rotation: {:?}",
rate2
);
assert!(
rate2.ticket_alerting,
"Ticket should still alerting: {:?}",
rate2
);
}
#[test]
fn test_window_rotation() {
let monitor = SloMonitor::new(SloConfig {
target_success_rate: 0.99,
short_window: Duration::from_millis(50),
long_window: Duration::from_millis(100),
burn_rate_threshold: 1.0,
ticket_short_window: Duration::from_millis(150),
ticket_long_window: Duration::from_millis(200),
ticket_burn_rate_threshold: 1.0,
});
for _ in 0..10 {
monitor.record_failure();
}
let rate1 = monitor.burn_rate();
assert!(rate1.short_burn_rate > 0.0);
sleep(Duration::from_millis(250));
let rate2 = monitor.burn_rate();
assert_eq!(rate2.short_success_rate, 1.0); assert_eq!(rate2.long_success_rate, 1.0);
assert_eq!(rate2.ticket_short_success_rate, 1.0);
assert_eq!(rate2.ticket_long_success_rate, 1.0);
}
#[test]
fn test_default_config_is_google_sre_recommended() {
let config = SloConfig::default();
assert_eq!(config.short_window, Duration::from_secs(300)); assert_eq!(config.long_window, Duration::from_secs(3600)); assert!((config.burn_rate_threshold - 14.4).abs() < 0.01);
assert_eq!(config.ticket_short_window, Duration::from_secs(30 * 60)); assert_eq!(config.ticket_long_window, Duration::from_secs(6 * 3600)); assert!((config.ticket_burn_rate_threshold - 6.0).abs() < 0.01);
}
#[test]
fn test_display_includes_both_alerts() {
let monitor = SloMonitor::new(SloConfig::default());
let rate = monitor.burn_rate();
let s = format!("{}", rate);
assert!(s.contains("page["), "display should include page: {}", s);
assert!(
s.contains("ticket["),
"display should include ticket: {}",
s
);
}
}