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 };
}