Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::monitor_burn_rate::burn_rate; ← from monitor.burn-rate ^1.0.0 · built alongside by fune
use super::monitor_burn_rate_alert_data::SRE_WORKBOOK; ← this capability’s own data, compiled from data/sre-workbook.json into the same file by fune build
// bad/total >= threshold/1000 x (10000 - target)/10000, cross-multiplied so a
// burn of 14.3995x never fires a 14.4x rule by rounding up.
fn at_or_above(w: &WindowCount, threshold_milli: i64, target_basis_points: i64) -> bool {
if w.total_events == 0 {
return false;
}
w.bad_events as i128 * 10_000_000
>= threshold_milli as i128 * w.total_events as i128 * (10000 - target_basis_points) as i128
}
/// Multiwindow, multi-burn-rate SLO alerting, as the Google SRE workbook
/// recommends: a rule fires only when both its long window (enough budget
/// spent to matter) and its short window (still happening now) burn at or
/// above its threshold. With rules None it uses the workbook's Table 5-8.
///
/// # Panics
/// Panics on a bad target, window, count or rule, or a rule whose window has no counts.
pub fn burn_rate_alert(target_basis_points: i64, windows: &[WindowCount], rules: Option<&[BurnRule]>) -> BurnAlert {
if !(1..=9999).contains(&target_basis_points) {
panic!(
"targetBasisPoints must be a whole number from 1 to 9999 (10000 leaves no error budget), received {}",
target_basis_points
);
}
let mut counts: Vec<(&WindowCount, i64)> = Vec::new();
for w in windows {
if w.window_seconds < 1 {
panic!("windowSeconds must be at least 1, received {}", w.window_seconds);
}
if counts.iter().any(|(c, _)| c.window_seconds == w.window_seconds) {
panic!("duplicate counts for a {}-second window", w.window_seconds);
}
// burn_rate checks the counts, so every window is checked, used or not.
counts.push((w, burn_rate(target_basis_points, w.total_events, w.bad_events)));
}
let workbook: Vec<BurnRule>;
let chosen: &[BurnRule] = match rules {
Some(r) => r,
None => {
workbook = SRE_WORKBOOK
.iter()
.map(|r| BurnRule {
severity: r.severity.to_string(),
long_window_seconds: r.long_window_seconds,
short_window_seconds: r.short_window_seconds,
burn_rate_milli: r.burn_rate_milli,
})
.collect();
&workbook
}
};
if chosen.is_empty() {
panic!("rules must not be empty; pass null for the SRE workbook rules");
}
let find = |seconds: i64| -> (&WindowCount, i64) {
match counts.iter().find(|(c, _)| c.window_seconds == seconds) {
Some((c, burn)) => (*c, *burn),
None => panic!("no counts for a {}-second window", seconds),
}
};
let mut results: Vec<BurnRuleResult> = Vec::new();
let mut severity: Option<String> = None;
for rule in chosen {
if rule.severity.is_empty() {
panic!("severity must not be empty");
}
if rule.short_window_seconds < 1 {
panic!("shortWindowSeconds must be at least 1, received {}", rule.short_window_seconds);
}
if rule.short_window_seconds > rule.long_window_seconds {
panic!(
"shortWindowSeconds must not exceed longWindowSeconds: {} > {}",
rule.short_window_seconds, rule.long_window_seconds
);
}
if rule.burn_rate_milli < 1 {
panic!("burnRateMilli must be at least 1, received {}", rule.burn_rate_milli);
}
let (long_count, long_burn) = find(rule.long_window_seconds);
let (short_count, short_burn) = find(rule.short_window_seconds);
let firing = at_or_above(long_count, rule.burn_rate_milli, target_basis_points)
&& at_or_above(short_count, rule.burn_rate_milli, target_basis_points);
if firing && severity.is_none() {
severity = Some(rule.severity.clone());
}
results.push(BurnRuleResult {
severity: rule.severity.clone(),
long_window_seconds: rule.long_window_seconds,
short_window_seconds: rule.short_window_seconds,
threshold_milli: rule.burn_rate_milli,
long_burn_milli: long_burn,
short_burn_milli: short_burn,
firing,
});
}
BurnAlert { firing: severity.is_some(), severity, rules: results }
}
pub fn window_count_from_value(v: &Value) -> WindowCount {
WindowCount {
window_seconds: v.get("windowSeconds").as_i64(),
total_events: v.get("totalEvents").as_i64(),
bad_events: v.get("badEvents").as_i64(),
}
}
pub fn burn_rule_from_value(v: &Value) -> BurnRule {
BurnRule {
severity: v.get("severity").as_str().to_string(),
long_window_seconds: v.get("longWindowSeconds").as_i64(),
short_window_seconds: v.get("shortWindowSeconds").as_i64(),
burn_rate_milli: v.get("burnRateMilli").as_i64(),
}
}
pub fn burn_rule_result_to_value(r: &BurnRuleResult) -> Value {
Value::obj(vec![
("severity", Value::str(&r.severity)),
("longWindowSeconds", Value::Int(r.long_window_seconds)),
("shortWindowSeconds", Value::Int(r.short_window_seconds)),
("thresholdMilli", Value::Int(r.threshold_milli)),
("longBurnMilli", Value::Int(r.long_burn_milli)),
("shortBurnMilli", Value::Int(r.short_burn_milli)),
("firing", Value::Bool(r.firing)),
])
}
pub fn burn_alert_to_value(a: &BurnAlert) -> Value {
Value::obj(vec![
("firing", Value::Bool(a.firing)),
("severity", match &a.severity { Some(s) => Value::str(s), None => Value::Null }),
("rules", Value::Arr(a.rules.iter().map(burn_rule_result_to_value).collect())),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let windows: Vec<WindowCount> = args[1].as_arr().iter().map(window_count_from_value).collect();
let rules: Option<Vec<BurnRule>> =
if args[2].is_null() { None } else { Some(args[2].as_arr().iter().map(burn_rule_from_value).collect()) };
burn_alert_to_value(&burn_rate_alert(args[0].as_i64(), &windows, rules.as_deref()))
}