Functional Weave
Code in Rust

monitor.burn-rate-alert@2.0.0

impl/typescript.ts

4,577 bytes · the TypeScript implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { burnRate } from "./monitor_burn_rate.ts";  ← from monitor.burn-rate ^1.0.0 · built alongside by fune
import { SRE_WORKBOOK } from "./monitor_burn_rate_alert_data.ts";  ← this capability’s own data, compiled from data/sre-workbook.json into the same file by fune build
import { type BurnAlert, type BurnRule, type BurnRuleResult, type WindowCount } from "./monitor_burn_rate_alert_types.ts";

function whole(value: number): boolean {
  return Number.isSafeInteger(value);
}

// 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.
function atOrAbove(w: WindowCount, thresholdMilli: number, targetBasisPoints: number): boolean {
  if (w.totalEvents === 0) return false;
  return BigInt(w.badEvents) * 10000000n >= BigInt(thresholdMilli) * BigInt(w.totalEvents) * BigInt(10000 - targetBasisPoints);
}

/**
 * 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 null it uses the workbook's Table 5-8.
 *
 * minEvents 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.
 */
export function burnRateAlert(
  targetBasisPoints: number,
  windows: readonly WindowCount[],
  rules: readonly BurnRule[] | null,
  minEvents: number | null,
): BurnAlert {
  if (!whole(targetBasisPoints) || targetBasisPoints < 1 || targetBasisPoints > 9999) {
    throw new RangeError(`targetBasisPoints must be a whole number from 1 to 9999 (10000 leaves no error budget), received ${targetBasisPoints}`);
  }
  if (minEvents !== null && (!whole(minEvents) || minEvents < 0)) {
    throw new RangeError(`minEvents must be null or a whole number of at least 0, received ${minEvents}`);
  }
  const minimum = minEvents ?? 0;
  const counts = new Map<number, { count: WindowCount; burn: number }>();
  for (const w of windows) {
    if (!whole(w.windowSeconds) || w.windowSeconds < 1) throw new RangeError(`windowSeconds must be at least 1, received ${w.windowSeconds}`);
    if (counts.has(w.windowSeconds)) throw new RangeError(`duplicate counts for a ${w.windowSeconds}-second window`);
    // burnRate checks the counts, so every window is checked, used or not.
    counts.set(w.windowSeconds, { count: w, burn: burnRate(targetBasisPoints, w.totalEvents, w.badEvents) });
  }
  const chosen: readonly BurnRule[] = rules ?? SRE_WORKBOOK.map((r) => ({
    severity: r.severity,
    longWindowSeconds: r.longWindowSeconds,
    shortWindowSeconds: r.shortWindowSeconds,
    burnRateMilli: r.burnRateMilli,
  }));
  if (chosen.length === 0) throw new RangeError("rules must not be empty; pass null for the SRE workbook rules");
  const results: BurnRuleResult[] = [];
  let severity: string | null = null;
  for (const rule of chosen) {
    if (rule.severity === "") throw new RangeError("severity must not be empty");
    if (!whole(rule.shortWindowSeconds) || rule.shortWindowSeconds < 1) {
      throw new RangeError(`shortWindowSeconds must be at least 1, received ${rule.shortWindowSeconds}`);
    }
    if (!whole(rule.longWindowSeconds) || rule.shortWindowSeconds > rule.longWindowSeconds) {
      throw new RangeError(`shortWindowSeconds must not exceed longWindowSeconds: ${rule.shortWindowSeconds} > ${rule.longWindowSeconds}`);
    }
    if (!whole(rule.burnRateMilli) || rule.burnRateMilli < 1) throw new RangeError(`burnRateMilli must be at least 1, received ${rule.burnRateMilli}`);
    const long = counts.get(rule.longWindowSeconds);
    if (long === undefined) throw new RangeError(`no counts for a ${rule.longWindowSeconds}-second window`);
    const short = counts.get(rule.shortWindowSeconds);
    if (short === undefined) throw new RangeError(`no counts for a ${rule.shortWindowSeconds}-second window`);
    const enoughEvents = long.count.totalEvents >= minimum;
    const firing = enoughEvents && atOrAbove(long.count, rule.burnRateMilli, targetBasisPoints) && atOrAbove(short.count, rule.burnRateMilli, targetBasisPoints);
    if (firing && severity === null) severity = rule.severity;
    results.push({
      severity: rule.severity,
      longWindowSeconds: rule.longWindowSeconds,
      shortWindowSeconds: rule.shortWindowSeconds,
      thresholdMilli: rule.burnRateMilli,
      longBurnMilli: long.burn,
      shortBurnMilli: short.burn,
      enoughEvents,
      firing,
    });
  }
  return { firing: severity !== null, severity, rules: results };
}