Functional Weave
Code in Rust

monitor.uptime

Time-weighted uptime of a target over a window from its checks, with gaps longer than a limit counted as unknown.

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

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

What it does

Uptime over a window, weighted by time, from a target's stored checks (`monitor.check-status`'s `Check`). Returns the seconds spent up, degraded, down and unknown, the uptime in basis points (9999 = 99.99%), and how many checks (and down checks) ran inside the window.

## How time is assigned

For example

  • uptime(checks ×3, 0, 180, 120) → up seconds 180, degraded seconds 0, down seconds 0, unknown seconds 0, uptime basis points 100%, checks 3, down checks 0 checks every minute, all up, cover the whole window
  • uptime(checks ×4, 0, 240, 60) → up seconds 120, degraded seconds 60, down seconds 60, unknown seconds 0, uptime basis points 75%, checks 4, down checks 1 each status holds until the next check; degraded counts towards uptime
  • uptime(checks ×2, 0, 100,000, 100,000) → up seconds 99,999, degraded seconds 0, down seconds 1, unknown seconds 0, uptime basis points 99.99%, checks 2, down checks 1 one down second in 100000 floors to 99.99%, never rounds up to 100%

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 uptime(checks: &[Check], from: i64, to: i64, max_gap_seconds: i64) -> UptimeReport
checksCheck[]in strictly ascending time order; the latest one before from carries into the window
fromintthe first second of the window, Unix seconds
tointthe first second after the window; equal to from is an empty window
max_gap_secondsinthow long one check's status is trusted before time counts as unknown, at least 1
returnsUptimeReport

The type it declares, generated into your project

/// Seconds in each state over the window, the uptime and the check counts; the four seconds add up to to - from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UptimeReport {
    pub up_seconds: i64,
    pub degraded_seconds: i64,
    pub down_seconds: i64,
    /// not covered by any check within maxGapSeconds
    pub unknown_seconds: i64,
    /// (up + degraded) / known time, floored; null when nothing is known
    pub uptime_basis_points: Option<i64>,
    /// checks with from <= at < to
    pub checks: i64,
    /// of those, the down ones
    pub down_checks: i64,
}

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

fune!(monitor.uptime@^1);  // then call uptime(…)
impl/rust.rs · 91 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

/// Seconds up, degraded, down and unknown over [from, to). Each check's status
/// holds from its `at` until the next check, but for at most max_gap_seconds:
/// a monitor that stopped reporting has not proved the target was up.
///
/// # Panics
/// Panics when from is after to, max_gap_seconds is below 1, the checks are
/// not strictly ascending, or a status is unknown.
pub fn uptime(checks: &[Check], from: i64, to: i64, max_gap_seconds: i64) -> UptimeReport {
    if from > to {
        panic!("from must not be after to: {} > {}", from, to);
    }
    if max_gap_seconds < 1 {
        panic!("maxGapSeconds must be a whole number of at least 1, received {}", max_gap_seconds);
    }
    let (mut up, mut degraded, mut down) = (0i64, 0i64, 0i64);
    let (mut count, mut down_count) = (0i64, 0i64);
    for (i, c) in checks.iter().enumerate() {
        if c.status != "up" && c.status != "degraded" && c.status != "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 c.at >= from && c.at < to {
            count += 1;
            if c.status == "down" {
                down_count += 1;
            }
        }
        // The span this check vouches for, clipped to the window.
        let mut end = c.at.saturating_add(max_gap_seconds);
        if i + 1 < checks.len() && checks[i + 1].at < end {
            end = checks[i + 1].at;
        }
        let s = c.at.max(from);
        let e = end.min(to);
        if e > s {
            match c.status.as_str() {
                "up" => up += e - s,
                "degraded" => degraded += e - s,
                _ => down += e - s,
            }
        }
    }
    let known = up + degraded + down;
    // Floored in i128: never 100.00% while a down second exists.
    let uptime_basis_points = if known == 0 {
        None
    } else {
        Some(((up + degraded) as i128 * 10000 / known as i128) as i64)
    };
    UptimeReport {
        up_seconds: up,
        degraded_seconds: degraded,
        down_seconds: down,
        unknown_seconds: to - from - known,
        uptime_basis_points,
        checks: count,
        down_checks: down_count,
    }
}

pub fn uptime_report_to_value(r: &UptimeReport) -> Value {
    Value::obj(vec![
        ("upSeconds", Value::Int(r.up_seconds)),
        ("degradedSeconds", Value::Int(r.degraded_seconds)),
        ("downSeconds", Value::Int(r.down_seconds)),
        ("unknownSeconds", Value::Int(r.unknown_seconds)),
        ("uptimeBasisPoints", r.uptime_basis_points.map(Value::Int).unwrap_or(Value::Null)),
        ("checks", Value::Int(r.checks)),
        ("downChecks", Value::Int(r.down_checks)),
    ])
}

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

pub fn fune_vector(args: &[Value]) -> Value {
    let checks = checks_from_value(&args[0]);
    let from = match &args[1] { Value::Int(i) => *i, _ => panic!("from and to must be whole seconds") };
    let to = match &args[2] { Value::Int(i) => *i, _ => panic!("from and to must be whole seconds") };
    let gap = whole(&args[3], "maxGapSeconds must be a whole number of at least 1, received ");
    uptime_report_to_value(&uptime(&checks, from, to, gap))
}

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.uptime
Download for Rust monitor.uptime-1.0.0-rust.fune · 14,489 bytes sha256 daa326f54cefc0bd1a02212d8b58eb1cd4d385dc37f6da276b76abe888ca7899

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

The whole function, every language, is one file too: monitor.uptime-1.0.0.fune, 19,320 bytes, sha256 915fa80bdd33c7db5b73f1a098649af886ed65ae0cf1e4c06518d7c974a4e134. 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.uptime

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

// fune: after monitor.uptime

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.uptime

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.uptime --steps.

// fune: step monitor.uptime 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
checks every minute, all up, cover the whole window checks ×3, 0, 180, 120 → up seconds 180, degraded seconds 0, down seconds 0, unknown seconds 0, uptime basis points 100%, checks 3, down checks 0
each status holds until the next check; degraded counts towards uptime checks ×4, 0, 240, 60 → up seconds 120, degraded seconds 60, down seconds 60, unknown seconds 0, uptime basis points 75%, checks 4, down checks 1
one down second in 100000 floors to 99.99%, never rounds up to 100% checks ×2, 0, 100,000, 100,000 → up seconds 99,999, degraded seconds 0, down seconds 1, unknown seconds 0, uptime basis points 99.99%, checks 2, down checks 1
a gap longer than maxGapSeconds is unknown after the cap, not up checks ×2, 0, 1,060, 300 → up seconds 360, degraded seconds 0, down seconds 0, unknown seconds 700, uptime basis points 100%, checks 2, down checks 0
the latest check before the window carries into it but is not counted as a check checks ×2, 1,000, 1,200, 300 → up seconds 100, degraded seconds 0, down seconds 100, unknown seconds 0, uptime basis points 50%, checks 1, down checks 0
a carried-in check still stops at maxGapSeconds after it ran checks ×1, 1,000, 1,200, 600 → up seconds 0, degraded seconds 0, down seconds 100, unknown seconds 100, uptime basis points 0%, checks 0, down checks 0
a check that expired before the window leaves it all unknown and uptime null checks ×1, 1,000, 1,200, 400 → up seconds 0, degraded seconds 0, down seconds 0, unknown seconds 200, uptime basis points —, checks 0, down checks 0
no checks at all is all unknown , 0, 3,600, 60 → up seconds 0, degraded seconds 0, down seconds 0, unknown seconds 3,600, uptime basis points —, checks 0, down checks 0
an empty window when from equals to checks ×1, 100, 100, 60 → up seconds 0, degraded seconds 0, down seconds 0, unknown seconds 0, uptime basis points —, checks 0, down checks 0
time before the first check is unknown, not assumed up checks ×1, 0, 200, 1,000 → up seconds 100, degraded seconds 0, down seconds 0, unknown seconds 100, uptime basis points 100%, checks 1, down checks 0
Show the other 11 tests
CaseArgumentsExpected
a check exactly at to belongs to the next window and adds nothing checks ×2, 0, 100, 1,000 → up seconds 100, degraded seconds 0, down seconds 0, unknown seconds 0, uptime basis points 100%, checks 1, down checks 0
degraded the whole time is still 100% up checks ×1, 0, 100, 100 → up seconds 0, degraded seconds 100, down seconds 0, unknown seconds 0, uptime basis points 100%, checks 1, down checks 0
down the whole time is 0% checks ×2, 0, 20, 10 → up seconds 0, degraded seconds 0, down seconds 20, unknown seconds 0, uptime basis points 0%, checks 2, down checks 2
a window so long that seconds times 10000 passes 2^53 still floors exactly checks ×2, 0, 1,000,000,000,001, 1,000,000,000,000 → up seconds 1,000,000,000,000, degraded seconds 0, down seconds 1, unknown seconds 0, uptime basis points 99.99%, checks 2, down checks 1
from after to is an error , 200, 100, 60 → error: from must not be after to: 200 > 100
a maxGapSeconds of zero is an error , 0, 100, 0 → error: maxGapSeconds must be a whole number of at least 1, received 0
a fractional maxGapSeconds is an error , 0, 100, 0.5 → error: maxGapSeconds must be a whole number of at least 1, received 0.5
a fractional bound is an error , 0.5, 100, 60 → error: from and to must be whole seconds
two checks at the same second are an error checks ×2, 0, 1,000, 60 → error: checks must be in strictly ascending time order: 100 follows 100
misordered checks outside the window are still an error checks ×2, 0, 50, 60 → error: checks must be in strictly ascending time order: 100 follows 300
an unknown status is an error checks ×1, 0, 10, 60 → error: unknown check status: offline

More from the author

Each check's status holds from its `at` until the next check, but for at most `maxGapSeconds`. Whatever is left of a longer gap is **unknown**: a monitor that stopped reporting has not proved the target was up. Set `maxGapSeconds` to a little more than the check interval (two intervals is common).

- The latest check before `from` carries into the window, with the same cap, so a window does not start with a hole just because no check landed exactly on its first second. It is not counted in `checks`. - Time before any check is unknown. - The window is half-open, `from <= t < to`; a check exactly at `to` belongs to the next window. - `upSeconds + degradedSeconds + downSeconds + unknownSeconds` is always `to - from`.

## The uptime figure

`uptimeBasisPoints = floor((up + degraded) * 10000 / (up + degraded + down))`.

- Degraded counts as up: the target was serving, slowly. - Unknown time is left out of both sides rather than guessed. - Floored, so it never reads 100.00% while any second was down (99,999 up seconds and 1 down second is 9999, not 10000). - `null` when nothing in the window is known. - Computed exactly (BigInt in TypeScript, i128 in Rust), so long windows do not lose precision past 2^53.

`checks` and `downChecks` are the event counts an event-based SLO wants (`monitor.error-budget`, `monitor.burn-rate`); the seconds are the time-based view.

## Errors

- `from and to must be whole seconds` - `from must not be after to: A > B` - `maxGapSeconds must be a whole number of at least 1, received X` - `checks must be in strictly ascending time order: A follows B` (checked over the whole list, not just the window) - `unknown check status: X`

Files

PathBytes
README.md2,001
impl/python.py2,318
impl/rust.rs3,507
impl/typescript.ts2,320
vectors.json5,400