-
Notifications
You must be signed in to change notification settings - Fork 2
MetroMan Summary
Cumulative _sum/_count with quantile estimates over a sliding
time window.
Reach for Summary when you need exact quantiles (p50/p90/p99) over
a single instance's recent observations. For distributions that
aggregate cleanly across instances, use
Histogram instead — summaries don't
combine across processes.
Memory scales with traffic, not just cardinality. A Summary keeps every raw observation from the last
windowseconds per label combination, so a high-throughput series retains far more data than a low-throughput one — unlike Histogram, whose per-series memory is fixed by its bucket count regardless of traffic. Combine high traffic with high label cardinality and the windowed sample buffer grows accordingly between purges (see Sliding Window). See Labels for the cardinality-growth caveat that applies to every metric kind.
import { Summary } from '@tundralibs/metro-man';
const latency = new Summary({
name: 'http_request_seconds',
quantiles: [0.5, 0.9, 0.99],
window: 60,
});
latency.observe(0.083, { route: '/users' });
latency.observe(0.420, { route: '/users' });
console.log(latency.toPrometheus());Parameters:
-
options.name— Required. Metric identifier. -
options.help— Optional. Human description. -
options.quantiles— Optionalnumber[]. Defaults to[0.5, 0.9, 0.99]. Each value must be a finite number in[0, 1]; duplicate values are silently de-duplicated (see Quantile Calculation) and the result sorted ascending internally. -
options.window— Optional retention window in seconds. Defaults to600(the maximum) so retention is always bounded. Must be a finite number in[1, 600]when provided (NaN/Infinityare rejected — they would disable the purge and let memory grow unbounded).
Throws:
-
InvalidMetricOptionsError— Whennameis missing, not a string, or doesn't match/^[a-zA-Z_:][a-zA-Z0-9_:]*$/; whenhelpis given but not a string; whenquantilesis not an array of finite numbers in[0, 1]; or whenwindowis non-numeric, non-finite, or outside[1, 600].
Record a single observation. The value is added to the series'
cumulative _sum/_count (lifetime totals) and also bucketed by the
current epoch second so the sliding window can
purge old samples from the quantile buffer.
Throws:
-
InvalidMetricOptionsError— whenvalueis non-finite (NaN,±Infinity). -
InvalidLabelError— whenlabelscontainsquantile(reserved for the rendered quantile lines), or a name outside[A-Za-z_][A-Za-z0-9_]*.
reset() drops every series and the cumulative lifetime
sum/count totals — lifetime accounting starts over from zero (see
Cumulative totals vs windowed quantiles).
remove(labels?) drops a single series' totals and windowed quantile
buffer together, returning true if a series was actually removed.
Both overridden from BaseMetric to also clear the raw-sample state.
Throws:
-
InvalidLabelError—remove(labels)rejects a label name outside[A-Za-z_][A-Za-z0-9_]*, the same validationobserveapplies.
Inherited from BaseMetric. All three call _calculate() first so
that quantiles always reflect the freshest window.
_sum and _count are cumulative for the process lifetime
(monotonic — they accumulate on every observe() and are never
purged), matching Prometheus / client_golang summary semantics.
Only the reported quantile estimates slide over the window.
This split matters when a summary is scraped: rate(x_count[5m])
and increase(x_sum[5m]) assume the underlying series only ever
increases. If _sum/_count decreased as samples aged out of the
window, a scraper would misread that decrease as a counter reset and
compute wrong values. Keeping the totals cumulative makes the
scraped output safe to use with rate() / increase(), while the
windowed quantiles still answer "what do recent percentiles look
like?".
reset() clears the cumulative totals (lifetime accounting starts
over); remove(labels) drops a single series' totals and quantile
buffer together.
The quantile estimates are computed only from samples observed in the
last window seconds (default 600 — the maximum); older samples
are dropped from the quantile buffer on the next purge. _sum and
_count are not windowed — see
Cumulative totals vs windowed quantiles.
Purges run:
- At read time — every call to
toJSON()/toPrometheus()/toString()(ordump(...)) triggers_calculate(), which purges first. - At write time —
observe()triggers a purge once at leastwindowseconds have elapsed since the previous purge. This bounds the quantile buffer to roughly two windows' worth of data even if you never read.
The window always applies to the quantiles — omitting it means "keep the last 10 minutes of samples", not "keep everything" — so a long-running summary's quantile buffer cannot grow without bound.
Quantiles use linear interpolation between the two nearest ranked samples:
rank = (n - 1) * q
base = floor(rank)
frac = rank - base
value = sorted[base] + frac * (sorted[base + 1] - sorted[base])
n is the number of observations in the current window. For an
empty window, the quantile is 0 (a finite value — many scrapers
reject NaN).
Duplicate quantiles are silently collapsed.
quantilesis de-duplicated withnew Set()before sorting, so[0.5, 0.5, 0.9]renders two quantile lines, not three. A repeatedquantilevalue would otherwise render the same series twice, which a strict Prometheus scraper rejects outright.
For the Quick Start example above.
# HELP http_request_seconds
# TYPE http_request_seconds summary
http_request_seconds{route="/users",quantile="0.5"} 0.2515
http_request_seconds{route="/users",quantile="0.9"} 0.3863
http_request_seconds{route="/users",quantile="0.99"} 0.41663
http_request_seconds_sum{route="/users"} 0.503
http_request_seconds_count{route="/users"} 2
{
"name": "http_request_seconds",
"help": "",
"type": "SUMMARY",
"labels": ["route"],
"data": {
"route=\"/users\"": {
"quantile": { "0.5": 0.2515, "0.9": 0.3863, "0.99": 0.41663 },
"count": 2,
"sum": 0.503
}
}
}