monitor.counter-increase
How much a monotonic counter grew over a series of samples, counting restarts from zero, and its rate per second.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 14 tests, run in TypeScript, Python and Rust.
What it does
How much a counter (requests served, bytes sent, errors) grew over a series of samples, and its average rate per second. Counters only go up, except when the process that owns them restarts and they begin again at zero.
## Counter resets
For example
counter_increase(samples ×3)→ increase 150, resets 0, rate per second milli 1,250, first at 0, last at 120 a steadily growing counter: 150 more over 120 seconds is 1.25 a secondcounter_increase(samples ×4)→ increase 110, resets 1, rate per second milli 611, first at 0, last at 180 a drop is a restart from zero: 60 + 20 + 30, where last minus first says -50counter_increase(samples ×3)→ increase 5, resets 1, rate per second milli 250, first at 0, last at 20 a restart read at exactly zero adds nothing but still counts as a reset
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 counter_increase(samples: &[MetricSample]) -> CounterIncrease
| samples | MetricSample[] | counter readings in strictly ascending time order, none negative |
| returns | CounterIncrease |
The type it declares, generated into your project
/// The growth of a counter over the samples given.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CounterIncrease {
/// total growth, a drop counted as a restart from zero
pub increase: i64,
/// how many drops (process restarts) were seen
pub resets: i64,
/// increase per second x1000, half-up; null under 2 samples
pub rate_per_second_milli: Option<i64>,
/// at of the first sample; null when there are none
pub first_at: Option<i64>,
/// at of the last sample; null when there are none
pub last_at: Option<i64>,
}
Your code names it in one line, in the file that uses it
fune!(monitor.counter-increase@^1); // then call counter_increase(…)
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_series_window::{samples_from_value, MetricSample}; ← from monitor.series-window ^1.0.0 · built alongside by fune
/// How much a counter grew over the samples, Prometheus-style: a drop means
/// the counter restarted from zero, so the value after it is all increase. No
/// extrapolation to window edges, so the answer is a whole number.
///
/// # Panics
/// Panics on misordered samples or a negative counter value.
pub fn counter_increase(samples: &[MetricSample]) -> CounterIncrease {
let mut increase: i64 = 0;
let mut resets: i64 = 0;
for (i, s) in samples.iter().enumerate() {
if i > 0 && s.at <= samples[i - 1].at {
panic!("samples must be in strictly ascending time order: {} follows {}", s.at, samples[i - 1].at);
}
if s.value < 0 {
panic!("counter value must not be negative: {} at {}", s.value, s.at);
}
if i == 0 {
continue;
}
let prev = samples[i - 1].value;
if s.value >= prev {
increase += s.value - prev;
} else {
resets += 1;
increase += s.value;
}
}
if samples.is_empty() {
return CounterIncrease { increase: 0, resets: 0, rate_per_second_milli: None, first_at: None, last_at: None };
}
let first_at = samples[0].at;
let last_at = samples[samples.len() - 1].at;
let rate = if samples.len() >= 2 {
// increase * 1000 can pass i64 for a large byte counter: i128.
let n = increase as i128 * 1000;
let d = (last_at - first_at) as i128;
Some(((2 * n + d) / (2 * d)) as i64)
} else {
None
};
CounterIncrease { increase, resets, rate_per_second_milli: rate, first_at: Some(first_at), last_at: Some(last_at) }
}
fn opt(v: Option<i64>) -> Value {
match v {
Some(i) => Value::Int(i),
None => Value::Null,
}
}
pub fn counter_increase_to_value(c: &CounterIncrease) -> Value {
Value::obj(vec![
("increase", Value::Int(c.increase)),
("resets", Value::Int(c.resets)),
("ratePerSecondMilli", opt(c.rate_per_second_milli)),
("firstAt", opt(c.first_at)),
("lastAt", opt(c.last_at)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
counter_increase_to_value(&counter_increase(&samples_from_value(&args[0])))
}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.counter-increase
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./monitor.counter-increase-1.0.0-rust.fune, or fetch it from a terminal with fune pull monitor.counter-increase@1.0.0:rust.
The whole function, every language, is one file too: monitor.counter-increase-1.0.0.fune, 13,697 bytes, sha256 72a41bbe93f4216ce749d9f7e52d04f91f95f6126064253f5328ac742ca6b10a. 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.counter-increase
after — your function gets the result and the arguments, and returns the final result.
// fune: after monitor.counter-increase
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.series-window in monitor.counter-increase
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.counter-increase --steps.
// fune: step monitor.counter-increase 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 | |
|---|---|---|---|
| a steadily growing counter: 150 more over 120 seconds is 1.25 a second | samples ×3 | → | increase 150, resets 0, rate per second milli 1,250, first at 0, last at 120 |
| a drop is a restart from zero: 60 + 20 + 30, where last minus first says -50 | samples ×4 | → | increase 110, resets 1, rate per second milli 611, first at 0, last at 180 |
| a restart read at exactly zero adds nothing but still counts as a reset | samples ×3 | → | increase 5, resets 1, rate per second milli 250, first at 0, last at 20 |
| two restarts in a row | samples ×4 | → | increase 7, resets 2, rate per second milli 233, first at 0, last at 30 |
| an unchanged counter grew by nothing | samples ×2 | → | increase 0, resets 0, rate per second milli 0, first at 0, last at 30 |
| no samples: nothing grew and there is no rate or span | → | increase 0, resets 0, rate per second milli —, first at —, last at — | |
| one sample has no interval, so no rate | samples ×1 | → | increase 0, resets 0, rate per second milli —, first at 1,000, last at 1,000 |
| a rate of exactly half a milli rounds up | samples ×2 | → | increase 1, resets 0, rate per second milli 1, first at 0, last at 2,000 |
| a rate just under half a milli rounds down | samples ×2 | → | increase 1, resets 0, rate per second milli 0, first at 0, last at 2,001 |
| a byte counter in the trillions: increase x1000 passes 2^53 and still rounds exactly | samples ×2 | → | increase 10,000,000,000,000, resets 0, rate per second milli 3,333,333,333,333,333, first at 0, last at 3 |
Show the other 4 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| times before 1970 are fine | samples ×2 | → | increase 25, resets 0, rate per second milli 500, first at -100, last at -50 |
| a negative counter value is an error | samples ×2 | → | error: counter value must not be negative: -1 at 10 |
| two samples at the same second are an error | samples ×2 | → | error: samples must be in strictly ascending time order: 10 follows 10 |
| samples out of order are an error | samples ×2 | → | error: samples must be in strictly ascending time order: 10 follows 20 |
More from the author
The rule is Prometheus's: any drop between two consecutive samples means the counter restarted from zero, so the new value is all increase. Samples 100, 160, 20, 50 grew by 60, then 20 (the restart), then 30: 110, with one reset. `last - first` would say -50.
A restart the samples never see is invisible: if a counter at 100 restarts and climbs past 100 before the next sample, the growth before the restart is lost. MetricSample more often than restarts happen.
## No extrapolation
Prometheus's `increase()` and `rate()` extrapolate from the first and last samples out to the edges of the query window, which is why they return fractional increases for a counter of whole requests. This function does not: the increase is exactly what the samples show between the first and the last, and the rate is that increase over `lastAt - firstAt`. Pass the samples of the window you care about (`monitor.series-window` picks them).
`ratePerSecondMilli` is `increase * 1000 / (lastAt - firstAt)` rounded half up, so 1.25 requests a second is 1250. It is null with fewer than two samples, where there is no interval. The multiplication is done in exact integer arithmetic (BigInt, i128), so a byte counter in the trillions still rounds correctly.
## Errors
- `samples must be in strictly ascending time order: 10 follows 10` - `counter value must not be negative: -1 at 10`
## Sources
- Prometheus, Query functions, `resets()`: "Any decrease in the value between two consecutive float samples is interpreted as a counter reset"; `increase()`: "The increase is extrapolated to cover the full time range as specified in the range vector selector", https://prometheus.io/docs/prometheus/latest/querying/functions/ - Prometheus, Metric types: Counter ("a cumulative metric ... whose value can only increase or be reset to zero on restart"), https://prometheus.io/docs/concepts/metric_types/#counter
Files
| Path | Bytes |
|---|---|
| README.md | 2,174 |
| impl/python.py | 1,492 |
| impl/rust.rs | 2,324 |
| impl/typescript.ts | 1,603 |
| vectors.json | 3,174 |