Functional Weave
Code in Rust

monitor.incidents

Turn a series of up, degraded and down checks into incidents, ignoring runs too short to confirm.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

What it does

Finds the incidents in a stored series of checks (`monitor.check-status`'s `Check`): each run of consecutive bad checks is one incident.

## Rules

For example

  • find_incidents(checks ×5, down, 1, 300) → ×1 two down checks in a row are one incident ending at the first up check
  • find_incidents(checks ×5, down, 2, 300) → ×1 a run exactly confirmChecks long is an incident
  • find_incidents(checks ×5, down, 3, 300) → a run shorter than confirmChecks is ignored as a flap

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

pub fn find_incidents(checks: &[Check], incident_status: &str, confirm_checks: i64, now: i64) -> Vec<Incident>
checksCheck[]in strictly ascending time order
incident_statusCheckStatusdown: only down checks are bad; degraded: degraded or down checks are bad
confirm_checksinthow many bad checks in a row make an incident, at least 1
nowintUnix seconds, not before the last check; ends the duration of an ongoing incident
returnsIncident[]

The type it declares, generated into your project

/// One run of bad checks, from the first bad check to the first good one after it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Incident {
    /// at of the first bad check
    pub start: i64,
    /// at of the first good check after the run; null while ongoing
    pub end: Option<i64>,
    /// (end, or now while ongoing) - start
    pub duration_seconds: i64,
    /// down if any check in the run was down, else degraded
    pub worst: String,
    /// how many bad checks the run holds
    pub checks: i64,
}

Your code names it in one line, in the file that uses it

fune!(monitor.incidents@^1);  // then call find_incidents(…)
impl/rust.rs · 106 lines · open · raw

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_check_status::{checks_from_value, Check};  ← from monitor.check-status ^1.0.0 · built alongside by fune

/// The incidents in a check series. A run of consecutive bad checks is one
/// incident, from its first bad check to the first good check after it, but
/// only when it holds at least confirm_checks checks: a single failed probe is
/// noise, not an outage, and would otherwise page and skew MTTR.
///
/// # Panics
/// Panics on an incident status other than down or degraded, confirm_checks
/// below 1, an unknown or misordered check, or now before the last check.
pub fn find_incidents(checks: &[Check], incident_status: &str, confirm_checks: i64, now: i64) -> Vec<Incident> {
    if incident_status != "down" && incident_status != "degraded" {
        panic!("incidentStatus must be down or degraded, received {}", incident_status);
    }
    if confirm_checks < 1 {
        panic!("confirmChecks must be at least 1, received {}", confirm_checks);
    }
    for (i, c) in checks.iter().enumerate() {
        if !matches!(c.status.as_str(), "up" | "degraded" | "down") {
            panic!("unknown check status: {}", c.status);
        }
        if i > 0 && c.at <= checks[i - 1].at {
            panic!("checks must be in strictly ascending time order: {} follows {}", c.at, checks[i - 1].at);
        }
    }
    if let Some(last) = checks.last() {
        if now < last.at {
            panic!("now {} is before the last check at {}", now, last.at);
        }
    }
    let bad = |s: &str| s == "down" || (incident_status == "degraded" && s == "degraded");
    let mut out = Vec::new();
    let mut i = 0;
    while i < checks.len() {
        if !bad(&checks[i].status) {
            i += 1;
            continue;
        }
        let start = checks[i].at;
        let mut count = 0i64;
        let mut worst = "degraded";
        while i < checks.len() && bad(&checks[i].status) {
            if checks[i].status == "down" {
                worst = "down";
            }
            count += 1;
            i += 1;
        }
        let end = if i < checks.len() { Some(checks[i].at) } else { None };
        if count >= confirm_checks {
            out.push(Incident {
                start,
                end,
                duration_seconds: end.unwrap_or(now) - start,
                worst: worst.to_string(),
                checks: count,
            });
        }
    }
    out
}

/// An `Incident` from its JSON form, for adapters of capabilities built on this one.
pub fn incident_from_value(v: &Value) -> Incident {
    let end = v.get("end");
    Incident {
        start: v.get("start").as_i64(),
        end: if end.is_null() { None } else { Some(end.as_i64()) },
        duration_seconds: v.get("durationSeconds").as_i64(),
        worst: v.get("worst").as_str().to_string(),
        checks: v.get("checks").as_i64(),
    }
}

pub fn incidents_from_value(v: &Value) -> Vec<Incident> {
    v.as_arr().iter().map(incident_from_value).collect()
}

pub fn incident_to_value(x: &Incident) -> Value {
    Value::obj(vec![
        ("start", Value::Int(x.start)),
        ("end", match x.end { Some(e) => Value::Int(e), None => Value::Null }),
        ("durationSeconds", Value::Int(x.duration_seconds)),
        ("worst", Value::str(&x.worst)),
        ("checks", Value::Int(x.checks)),
    ])
}

fn whole(v: &Value, message: &str) -> i64 {
    match v {
        Value::Int(i) => *i,
        _ => panic!("{}", message),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let checks = checks_from_value(&args[0]);
    let confirm = match &args[2] {
        Value::Int(i) => *i,
        other => panic!("confirmChecks must be at least 1, received {:?}", other),
    };
    let now = whole(&args[3], "now must be a whole number of seconds");
    let out = find_incidents(&checks, args[1].as_str(), confirm, now);
    Value::Arr(out.iter().map(incident_to_value).collect())
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add monitor.incidents
Download for Rust monitor.incidents-1.0.0-rust.fune · 14,692 bytes sha256 d27245738d70b5b92998a838b51cc595caebf2c6d5fcb4969b13de93fff27546

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./monitor.incidents-1.0.0-rust.fune, or fetch it from a terminal with fune pull monitor.incidents@1.0.0:rust.

The whole function, every language, is one file too: monitor.incidents-1.0.0.fune, 19,417 bytes, sha256 4664c6be7bf3e0139623ab73f3d74becc8b401bdee0de17e21fa7824a05124a8. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before monitor.incidents

after — your function gets the result and the arguments, and returns the final result.

// fune: after monitor.incidents

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace monitor.check-status in monitor.incidents

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show monitor.incidents --steps.

// fune: step monitor.incidents after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
two down checks in a row are one incident ending at the first up check checks ×5, down, 1, 300 → ×1
a run exactly confirmChecks long is an incident checks ×5, down, 2, 300 → ×1
a run shorter than confirmChecks is ignored as a flap checks ×5, down, 3, 300 →
a run still bad at the last check is ongoing, measured to now checks ×3, down, 1, 200 → ×1
degraded mode joins degraded and down checks into one incident, worst down checks ×5, degraded, 1, 240 → ×1
down mode: a degraded check is not bad, so it ends the down incident checks ×5, down, 1, 240 → ×1
a degraded-only incident has worst degraded checks ×3, degraded, 1, 120 → ×1
short blips around a real outage are dropped, and an unconfirmed ongoing run too checks ×8, down, 2, 480 → ×1
no checks, no incidents , down, 1, 1,000 →
a series that starts bad starts its incident at the first check checks ×2, down, 1, 60 → ×1
Show the other 9 tests
CaseArgumentsExpected
all up has no incidents checks ×3, down, 1, 180 →
an ongoing incident with now at its only check lasts 0 seconds checks ×1, down, 1, 100 → ×1
two separate incidents come out in time order checks ×4, down, 1, 45 → ×2
checks out of time order are an error checks ×2, down, 1, 100 → error: checks must be in strictly ascending time order
an unknown check status is an error checks ×1, down, 1, 100 → error: unknown check status: offline
up is not an incident status checks ×5, up, 1, 300 → error: incidentStatus must be down or degraded, received up
confirmChecks 0 is an error checks ×5, down, 0, 300 → error: confirmChecks must be at least 1
now before the last check is an error checks ×5, down, 1, 200 → error: now 200 is before the last check at 240
a fractional now is an error checks ×5, down, 1, 300.5 → error: now must be a whole number of seconds

More from the author

- **Bad** depends on `incidentStatus`: with `down`, only `down` checks are bad (a slow site is not an outage); with `degraded`, `degraded` and `down` both are, and one incident can mix them. `up` is refused: an "incident of being up" is a configuration mistake. - A run counts only when it has at least `confirmChecks` checks. This is flap suppression, the same idea as "alert after N consecutive failures" in most uptime monitors: one lost probe does not make an outage, and would drag MTTR down if it did. Shorter runs are dropped entirely, including an unconfirmed run still in progress at the last check. - `start` is the `at` of the first bad check; `end` is the `at` of the first check after the run that is not bad (with `incidentStatus: down`, a `degraded` check ends a down incident). An incident still bad at the last check is ongoing: `end` is null and `durationSeconds` runs to `now`. - The start is the first check that *saw* the failure, not a guess at when it really began between two checks; and the end is the first check that saw recovery. So durations are accurate to the check interval, and err long. - `worst` is `down` if any check in the run was down, else `degraded`. - `checks` counts the bad checks in the run.

Incidents come out in time order and never overlap, which is what `monitor.mttr` expects.

## Errors

- `checks must be in strictly ascending time order` - `unknown check status: X` - `incidentStatus must be down or degraded, received up` - `confirmChecks must be at least 1` - `now 200 is before the last check at 240`: now ends ongoing incidents, so it cannot be earlier than the data. - `now must be a whole number of seconds`

Files

PathBytes
README.md1,863
impl/python.py2,276
impl/rust.rs3,891
impl/typescript.ts2,253
vectors.json5,284