Functional Weave
Code in Python

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 second
  • counter_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 -50
  • counter_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.

def counter_increase(samples: Sequence[MetricSample]) -> CounterIncrease
samplesMetricSample[]counter readings in strictly ascending time order, none negative
returnsCounterIncrease

The type it declares, generated into your project

@dataclass(frozen=True)
class CounterIncrease:
    """The growth of a counter over the samples given."""

    #: total growth, a drop counted as a restart from zero
    increase: int
    #: how many drops (process restarts) were seen
    resets: int
    #: increase per second x1000, half-up; null under 2 samples
    rate_per_second_milli: Optional[int]
    #: at of the first sample; null when there are none
    first_at: Optional[int]
    #: at of the last sample; null when there are none
    last_at: Optional[int]

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

from fune.monitor.counter_increase import counter_increase  # monitor.counter-increase@^1
impl/python.py · 35 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.

from typing import Optional, Sequence

from .monitor_series_window import MetricSample  ← from monitor.series-window ^1.0.0 · built alongside by fune
from .monitor_counter_increase_types import CounterIncrease


def counter_increase(samples: Sequence[MetricSample]) -> CounterIncrease:
    """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."""
    increase = 0
    resets = 0
    for i, s in enumerate(samples):
        if i > 0 and s.at <= samples[i - 1].at:
            raise ValueError("samples must be in strictly ascending time order: %d follows %d" % (s.at, samples[i - 1].at))
        if s.value < 0:
            raise ValueError("counter value must not be negative: %d at %d" % (s.value, s.at))
        if i == 0:
            continue
        prev = samples[i - 1].value
        if s.value >= prev:
            increase += s.value - prev
        else:
            resets += 1
            increase += s.value
    if not samples:
        return CounterIncrease(increase=0, resets=0, rate_per_second_milli=None, first_at=None, last_at=None)
    first_at = samples[0].at
    last_at = samples[-1].at
    rate: Optional[int] = None
    if len(samples) >= 2:
        n = increase * 1000
        d = last_at - first_at
        rate = (2 * n + d) // (2 * d)
    return CounterIncrease(increase=increase, resets=resets, rate_per_second_milli=rate, first_at=first_at, last_at=last_at)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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.counter-increase
Download for Python monitor.counter-increase-1.0.0-python.fune · 9,618 bytes sha256 2ce2a7ddaab202e181f382949573136ede82723b15a250e5edeb33cdc4a2436b

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

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md2,174
impl/python.py1,492
impl/rust.rs2,324
impl/typescript.ts1,603
vectors.json3,174