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 windowuptime(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 uptimeuptime(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
| checks | Check[] | in strictly ascending time order; the latest one before from carries into the window |
| from | int | the first second of the window, Unix seconds |
| to | int | the first second after the window; equal to from is an empty window |
| max_gap_seconds | int | how long one check's status is trusted before time counts as unknown, at least 1 |
| returns | UptimeReport |
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(…)
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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 2,001 |
| impl/python.py | 2,318 |
| impl/rust.rs | 3,507 |
| impl/typescript.ts | 2,320 |
| vectors.json | 5,400 |