use crate::burn_rate::MwmbrConfig;
use super::{Spec, DEFAULT_PERIOD};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LintLevel {
Warning,
Info,
}
impl LintLevel {
pub fn label(&self) -> &'static str {
match self {
LintLevel::Warning => "WARN",
LintLevel::Info => "INFO",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lint {
pub level: LintLevel,
pub code: &'static str,
pub location: String,
pub message: String,
}
pub fn lint(spec: &Spec) -> Vec<Lint> {
let mut out = Vec::new();
let longest_window = MwmbrConfig::sre_default()
.windows
.iter()
.map(|w| w.long)
.max()
.unwrap_or(DEFAULT_PERIOD);
for slo in &spec.slos {
let loc = format!("slo '{}'", slo.name);
if slo.objective >= 100.0 {
out.push(Lint {
level: LintLevel::Warning,
code: "OBJECTIVE_100",
location: loc.clone(),
message:
"objective is 100%: there is no error budget, so burn-rate alerts can never fire"
.to_string(),
});
} else if slo.objective > 0.0 && slo.objective < 50.0 {
out.push(Lint {
level: LintLevel::Warning,
code: "OBJECTIVE_LOW",
location: loc.clone(),
message: format!(
"objective {}% is implausibly low; confirm this is intended",
slo.objective
),
});
}
if let Ok(period) = slo.resolve_period(DEFAULT_PERIOD) {
if period <= longest_window {
out.push(Lint {
level: LintLevel::Warning,
code: "PERIOD_TOO_SHORT",
location: loc.clone(),
message: format!(
"period {period} is not longer than the longest burn-rate window ({longest_window}); long-window alerts will not be meaningful"
),
});
}
}
let page_disabled = slo.alerting.page_alert.disable;
let ticket_disabled = slo.alerting.ticket_alert.disable;
if page_disabled && ticket_disabled {
out.push(Lint {
level: LintLevel::Warning,
code: "ALL_ALERTS_DISABLED",
location: loc.clone(),
message:
"both page and ticket alerts are disabled; no burn-rate alerts will be generated for this SLO"
.to_string(),
});
} else {
let has_shared_labels = !slo.alerting.labels.is_empty();
if !page_disabled && slo.alerting.page_alert.labels.is_empty() && !has_shared_labels {
out.push(Lint {
level: LintLevel::Warning,
code: "NO_ALERT_LABELS",
location: loc.clone(),
message:
"page alert has no labels (e.g. `severity`); Alertmanager routing may not match it"
.to_string(),
});
}
if !ticket_disabled && slo.alerting.ticket_alert.labels.is_empty() && !has_shared_labels
{
out.push(Lint {
level: LintLevel::Warning,
code: "NO_ALERT_LABELS",
location: loc.clone(),
message:
"ticket alert has no labels (e.g. `severity`); Alertmanager routing may not match it"
.to_string(),
});
}
}
if slo.description.trim().is_empty() {
out.push(Lint {
level: LintLevel::Info,
code: "NO_DESCRIPTION",
location: loc.clone(),
message:
"SLO has no description; add one so generated alerts and dashboards are self-explanatory"
.to_string(),
});
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn codes(spec: &Spec) -> Vec<&'static str> {
lint(spec).into_iter().map(|l| l.code).collect()
}
const CLEAN: &str = r#"
service: api
slos:
- name: availability
objective: 99.9
description: "99.9% of requests succeed"
sli:
events:
error_query: sum(rate(err[{{.window}}]))
total_query: sum(rate(tot[{{.window}}]))
alerting:
labels: { severity: page }
"#;
#[test]
fn clean_spec_has_no_findings() {
let spec = Spec::from_yaml(CLEAN).unwrap();
assert!(lint(&spec).is_empty(), "{:?}", lint(&spec));
}
#[test]
fn objective_100_warns() {
let yaml = r#"
service: api
slos:
- name: a
objective: 100
description: d
sli: { raw: { error_ratio_query: "r[{{.window}}]" } }
alerting: { labels: { severity: page } }
"#;
let spec = Spec::from_yaml(yaml).unwrap();
assert!(codes(&spec).contains(&"OBJECTIVE_100"));
}
#[test]
fn low_objective_warns() {
let yaml = r#"
service: api
slos:
- name: a
objective: 40
description: d
sli: { raw: { error_ratio_query: "r[{{.window}}]" } }
alerting: { labels: { severity: page } }
"#;
let spec = Spec::from_yaml(yaml).unwrap();
assert!(codes(&spec).contains(&"OBJECTIVE_LOW"));
}
#[test]
fn short_period_warns() {
let yaml = r#"
service: api
slos:
- name: a
objective: 99.0
period: 1d
description: d
sli: { raw: { error_ratio_query: "r[{{.window}}]" } }
alerting: { labels: { severity: page } }
"#;
let spec = Spec::from_yaml(yaml).unwrap();
assert!(codes(&spec).contains(&"PERIOD_TOO_SHORT"));
}
#[test]
fn long_period_does_not_warn_on_period() {
let yaml = r#"
service: api
slos:
- name: a
objective: 99.0
period: 30d
description: d
sli: { raw: { error_ratio_query: "r[{{.window}}]" } }
alerting: { labels: { severity: page } }
"#;
let spec = Spec::from_yaml(yaml).unwrap();
assert!(!codes(&spec).contains(&"PERIOD_TOO_SHORT"));
}
#[test]
fn missing_alert_labels_warn() {
let yaml = r#"
service: api
slos:
- name: a
objective: 99.0
description: d
sli: { raw: { error_ratio_query: "r[{{.window}}]" } }
"#;
let spec = Spec::from_yaml(yaml).unwrap();
let n = lint(&spec)
.iter()
.filter(|l| l.code == "NO_ALERT_LABELS")
.count();
assert_eq!(n, 2, "expected page + ticket findings");
}
#[test]
fn all_alerts_disabled_warns_and_skips_label_check() {
let yaml = r#"
service: api
slos:
- name: a
objective: 99.0
description: d
sli: { raw: { error_ratio_query: "r[{{.window}}]" } }
alerting:
page_alert: { disable: true }
ticket_alert: { disable: true }
"#;
let spec = Spec::from_yaml(yaml).unwrap();
let c = codes(&spec);
assert!(c.contains(&"ALL_ALERTS_DISABLED"));
assert!(!c.contains(&"NO_ALERT_LABELS"));
}
#[test]
fn missing_description_is_info() {
let yaml = r#"
service: api
slos:
- name: a
objective: 99.0
sli: { raw: { error_ratio_query: "r[{{.window}}]" } }
alerting: { labels: { severity: page } }
"#;
let spec = Spec::from_yaml(yaml).unwrap();
let found = lint(&spec)
.into_iter()
.find(|l| l.code == "NO_DESCRIPTION")
.expect("expected NO_DESCRIPTION");
assert_eq!(found.level, LintLevel::Info);
}
}