Functional Weave
Code in Python

monitor.alert-rule@1.0.0

impl/typescript.ts

2,721 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 { 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 };
}