Functional Weave
Code in TypeScript

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.

export function uptime(checks: readonly Check[], from: number, to: number, maxGapSeconds: number): 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
maxGapSecondsinthow 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. */
export interface UptimeReport {
  readonly upSeconds: number;
  readonly degradedSeconds: number;
  readonly downSeconds: number;
  /** not covered by any check within maxGapSeconds */
  readonly unknownSeconds: number;
  /** (up + degraded) / known time, floored; null when nothing is known */
  readonly uptimeBasisPoints: number | null;
  /** checks with from <= at < to */
  readonly checks: number;
  /** of those, the down ones */
  readonly downChecks: number;
}

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

import { uptime } from "#fune/monitor.uptime@^1";
impl/typescript.ts · 56 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.

import { type Check } from "./monitor_check_status.ts";  ← from monitor.check-status ^1.0.0 · built alongside by fune
import { type UptimeReport } from "./monitor_uptime_types.ts";

/**
 * 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 maxGapSeconds: a
 * monitor that stopped reporting has not proved the target was up.
 */
export function uptime(checks: readonly Check[], from: number, to: number, maxGapSeconds: number): UptimeReport {
  if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to)) throw new RangeError("from and to must be whole seconds");
  if (from > to) throw new RangeError(`from must not be after to: ${from} > ${to}`);
  if (!Number.isSafeInteger(maxGapSeconds) || maxGapSeconds < 1) {
    throw new RangeError(`maxGapSeconds must be a whole number of at least 1, received ${maxGapSeconds}`);
  }
  let up = 0;
  let degraded = 0;
  let down = 0;
  let count = 0;
  let downCount = 0;
  for (let i = 0; i < checks.length; i++) {
    const c = checks[i];
    if (c.status !== "up" && c.status !== "degraded" && c.status !== "down") {
      throw new RangeError(`unknown check status: ${String(c.status)}`);
    }
    if (i > 0 && c.at <= checks[i - 1].at) {
      throw new RangeError(`checks must be in strictly ascending time order: ${c.at} follows ${checks[i - 1].at}`);
    }
    if (c.at >= from && c.at < to) {
      count += 1;
      if (c.status === "down") downCount += 1;
    }
    // The span this check vouches for, clipped to the window.
    let end = c.at + maxGapSeconds;
    if (i + 1 < checks.length && checks[i + 1].at < end) end = checks[i + 1].at;
    const s = Math.max(c.at, from);
    const e = Math.min(end, to);
    if (e > s) {
      if (c.status === "up") up += e - s;
      else if (c.status === "degraded") degraded += e - s;
      else down += e - s;
    }
  }
  const known = up + degraded + down;
  // Floored in BigInt: never 100.00% while a down second exists, and exact
  // for windows whose seconds times 10000 pass 2^53.
  const uptimeBasisPoints = known === 0 ? null : Number((BigInt(up + degraded) * 10000n) / BigInt(known));
  return {
    upSeconds: up,
    degradedSeconds: degraded,
    downSeconds: down,
    unknownSeconds: to - from - known,
    uptimeBasisPoints,
    checks: count,
    downChecks: downCount,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript 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. Or pin a range in fune.project and build in one step:

fune add monitor.uptime
Download for TypeScript monitor.uptime-1.0.0-typescript.fune · 13,255 bytes sha256 92eaffd4612ec2516e0ac1e59742c2762a2ade6ea354e0c2c26640d14ad568cd

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

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