Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type MetricSample } from "./monitor_series_window_types.ts";
import { seriesWindow } from "./monitor_series_window.ts"; ← from monitor.series-window ^1.0.0 · built alongside by fune
import { type CompareOp, type RuleEvaluation, type ThresholdRule } from "./monitor_alert_rule_types.ts";
function beyond(op: CompareOp, value: number, limit: number): boolean {
switch (op) {
case "gt": return value > limit;
case "gte": return value >= limit;
case "lt": return value < limit;
case "lte": return value <= limit;
default: throw new RangeError(`unknown comparison: ${String(op)}`);
}
}
/**
* The state of a threshold rule at `now`, Prometheus style: a breach must
* hold for `forSeconds` before it fires, and until then it is pending.
*
* With a clearThreshold a breach clears only once the value passes it the
* other way, so a metric wobbling around the threshold does not flap between
* firing and inactive. The breach clock (`since`) restarts after a clear.
*/
export function evaluateAlertRule(samples: readonly MetricSample[], rule: ThresholdRule, now: number): RuleEvaluation {
if (!Number.isSafeInteger(now)) throw new RangeError("now must be a whole number of seconds");
const { op, threshold, forSeconds } = rule;
const clear = rule.clearThreshold ?? threshold;
beyond(op, 0, 0);
const upward = op === "gt" || op === "gte";
if (upward ? clear > threshold : clear < threshold) {
throw new RangeError(`clearThreshold must be on the non-breaching side of threshold: ${op} ${threshold}, clear ${clear}`);
}
if (forSeconds < 0) throw new RangeError(`forSeconds must not be negative, received ${forSeconds}`);
const stale = rule.staleAfterSeconds ?? null;
if (stale !== null && stale < 0) throw new RangeError(`staleAfterSeconds must not be negative, received ${stale}`);
// seriesWindow checks the order of the whole series and keeps at <= now.
const from = samples.length > 0 ? Math.min(samples[0].at, now + 1) : now + 1;
const seen = seriesWindow(samples, from, now + 1);
let since: number | null = null;
for (const s of seen) {
if (since === null) {
if (beyond(op, s.value, threshold)) since = s.at;
} else if (!beyond(op, s.value, clear)) {
since = null;
}
}
if (seen.length === 0) return { state: "no-data", since: null, value: null, heldSeconds: 0 };
const latest = seen[seen.length - 1];
if (stale !== null && now - latest.at > stale) {
return { state: "no-data", since: null, value: latest.value, heldSeconds: 0 };
}
if (since === null) return { state: "inactive", since: null, value: latest.value, heldSeconds: 0 };
const held = now - since;
return { state: held >= forSeconds ? "firing" : "pending", since, value: latest.value, heldSeconds: held };
}