pub mod budgets;
pub mod objectives;
pub mod reports;
use crate::error::Result;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Slo {
pub name: String,
pub description: String,
pub target: f64,
pub sli_query: String,
pub time_window: TimeWindow,
pub error_budget: ErrorBudget,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TimeWindow {
Rolling(Duration),
Calendar(CalendarWindow),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CalendarWindow {
Daily,
Weekly,
Monthly,
Quarterly,
Yearly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorBudget {
pub total: f64,
pub consumed: f64,
pub remaining: f64,
pub burn_rate: f64,
}
impl ErrorBudget {
pub fn from_target(target: f64) -> Self {
let total = 100.0 - target;
Self {
total,
consumed: 0.0,
remaining: total,
burn_rate: 0.0,
}
}
pub fn update(&mut self, consumed: f64) {
self.consumed = consumed;
self.remaining = self.total - consumed;
}
pub fn calculate_burn_rate(&mut self, window_hours: f64) {
if window_hours > 0.0 {
self.burn_rate = self.consumed / window_hours;
}
}
pub fn is_exhausted(&self) -> bool {
self.remaining <= 0.0
}
pub fn exhaustion_percentage(&self) -> f64 {
if self.total > 0.0 {
(self.consumed / self.total) * 100.0
} else {
0.0
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SloStatus {
pub name: String,
pub achievement: f64,
pub target: f64,
pub is_met: bool,
pub error_budget: ErrorBudget,
pub evaluated_at: DateTime<Utc>,
}
impl SloStatus {
pub fn new(name: String, achievement: f64, target: f64, error_budget: ErrorBudget) -> Self {
Self {
name,
achievement,
target,
is_met: achievement >= target,
error_budget,
evaluated_at: Utc::now(),
}
}
}
pub struct SloMonitor {
slos: Vec<Slo>,
}
impl SloMonitor {
pub fn new() -> Self {
Self { slos: Vec::new() }
}
pub fn add_slo(&mut self, slo: Slo) {
self.slos.push(slo);
}
pub fn slos(&self) -> &[Slo] {
&self.slos
}
pub fn evaluate_all(&self) -> Result<Vec<SloStatus>> {
let mut statuses = Vec::with_capacity(self.slos.len());
for slo in &self.slos {
let achievement = (100.0 - slo.error_budget.consumed).clamp(0.0, 100.0);
statuses.push(SloStatus::new(
slo.name.clone(),
achievement,
slo.target,
slo.error_budget.clone(),
));
}
Ok(statuses)
}
}
impl Default for SloMonitor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_budget() {
let mut budget = ErrorBudget::from_target(99.9);
assert!((budget.total - 0.1).abs() < 1e-10);
assert!((budget.remaining - 0.1).abs() < 1e-10);
budget.update(0.05);
assert_eq!(budget.consumed, 0.05);
assert!((budget.remaining - 0.05).abs() < 1e-10);
assert!(!budget.is_exhausted());
budget.update(0.15);
assert!(budget.is_exhausted());
}
#[test]
fn test_slo_status() {
let budget = ErrorBudget::from_target(99.9);
let status = SloStatus::new("test_slo".to_string(), 99.95, 99.9, budget);
assert!(status.is_met);
assert_eq!(status.achievement, 99.95);
}
#[test]
fn test_slo_monitor_evaluate_all() {
let mut monitor = SloMonitor::new();
let empty = monitor
.evaluate_all()
.expect("evaluate_all on empty monitor should succeed");
assert!(empty.is_empty());
let mut breached_budget = ErrorBudget::from_target(99.9);
breached_budget.update(0.5); monitor.add_slo(Slo {
name: "breached".to_string(),
description: "breached SLO".to_string(),
target: 99.9,
sli_query: "rate(errors)".to_string(),
time_window: TimeWindow::Rolling(Duration::days(30)),
error_budget: breached_budget,
});
let healthy_budget = ErrorBudget::from_target(99.9);
monitor.add_slo(Slo {
name: "healthy".to_string(),
description: "healthy SLO".to_string(),
target: 99.9,
sli_query: "rate(errors)".to_string(),
time_window: TimeWindow::Rolling(Duration::days(30)),
error_budget: healthy_budget,
});
let statuses = monitor.evaluate_all().expect("evaluate_all should succeed");
assert_eq!(statuses.len(), 2);
let breached = statuses
.iter()
.find(|s| s.name == "breached")
.expect("breached status missing");
assert!((breached.achievement - 99.5).abs() < 1e-9);
assert!(!breached.is_met);
let healthy = statuses
.iter()
.find(|s| s.name == "healthy")
.expect("healthy status missing");
assert!((healthy.achievement - 100.0).abs() < 1e-9);
assert!(healthy.is_met);
}
}