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.
///
/// min_events guards small samples: a rule whose long window saw fewer
/// events stays quiet, since 2 bad checks out of 11 is a 36x burn of a
/// 99.5% budget but proves little. The short window is not guarded: it only
/// confirms the burn is still going on.
///
/// Rust has no default arguments, so minEvents, which 1.1.0 added, travels
/// in `BurnRateAlertOptions` (generated by fune from the manifest, with
/// `Default`), and caps generates `burn_rate_alert(target, windows, rules)`,
/// the 1.0.0 call, as this function with the options at their defaults.
///
/// # Panics
/// Panics on a bad target, window, count or rule, or a rule whose window has no counts.
pub fn burn_rate_alert_with(
target_basis_points: i64,
windows: &[WindowCount],
rules: Option<&[BurnRule]>,
options: BurnRateAlertOptions,
) -> BurnAlert {
let min_events = options.min_events;
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
);
}
if let Some(m) = min_events {
if m < 0 {
panic!("minEvents must be null or a whole number of at least 0, received {}", m);
}
}
let minimum = min_events.unwrap_or(0);
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 enough_events = long_count.total_events >= minimum;
let firing = enough_events
&& 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,
enough_events,
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)),
("enoughEvents", Value::Bool(r.enough_events)),
("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()) };
// minEvents: null, or a whole number (fune verify fills in null when a
// vector leaves it out, as 1.0.0's do); refuse a fractional one with the text TypeScript and Python use.
let min_events: Option<i64> = match &args[3] {
Value::Null => None,
Value::Int(i) => Some(*i),
other => panic!("minEvents must be null or a whole number of at least 0, received {}", other.as_f64()),
};
burn_alert_to_value(&burn_rate_alert_with(args[0].as_i64(), &windows, rules.as_deref(), BurnRateAlertOptions { min_events }))
}