Berserk Docs
Aggregate FunctionsMetrics

otel_sample_interval

How far apart a single series' samples arrive, as a timespan — the emission interval for a pushed metric, the scrape interval for a scraped one (bzrk extension).

The argument is a dynamic of metric shape — the field set produced by the OTel-to-bzrk mapping (timestamp, value, metric_hash, …) — of which $raw over a metric table is the usual source. Only timestamp and metric_hash are read; the value is irrelevant to an arrival pattern.

Answers "how wide must a bin be for this metric", so a caller can size one up front instead of rendering an empty panel. The delta aggregates need two samples of a series inside a bin to say anything: below one sample interval otel_rate returns null and otel_histogram_percentile returns NaN, so a bin of at least twice this value is the floor for a rate or percentile panel.

Returned as a timespan rather than a number of seconds so it composes with the operators that consume one: bin_auto(timestamp, 2 * <interval>), whose second argument is a floor, and make-series … step.

The estimator

Recovered by algebra rather than by remembering anything per series. For S series holding N samples each over span T, rows = S × N and the per-series interval is T / (N − 1), so

interval = span × series / (rows − series)

which needs only min(timestamp), max(timestamp), count() and a distinct-count of metric_hash. The state is therefore bounded by a fixed-size sketch — the same one dcount uses — instead of growing with the number of series, which is what makes this far cheaper than otel_rate and its per-(series, epoch) map.

The (rows − series) denominator is the N − 1 correction and is not optional: using rows is biased low by (N−1)/N — 0.2% at N=480 but 25% at N=5, exactly the short-window case a caller sizing a bin asks about.

What it does not do

The estimate is a mean, so a gap in the data inflates it, and a group spanning two cadences averages into a value that is neither. Real cadences come from a small set (1/5/10/15/20/30/60s), so a caller that wants a clean bin should snap the result to the nearest plausible value — in the caller, not here, so the raw number stays visible when the data is genuinely irregular. Resolve it per metric rather than once per query.

A caller feeding the result to bin_auto's floor or a make-series step has to handle the null: both reject a null timespan, so a fallback belongs in the query. The obvious spellings (iff, coalesce) do not work there yet — they keep the timespan at runtime but const-fold to a real, which those arguments reject — so today the fallback has to be chosen by whatever builds the query.

Returns null whenever no interval was observed: an empty group, a group whose rows all share one timestamp, or one where no series has two samples (rows equals the series count) — which is the very condition that makes a bin too narrow, so a null here is the signal that the bin cannot be sized from this group.

Syntax

otel_sample_interval($raw)

Parameters

Prop

Type

Returns: timespan

Examples

Example 1

// the cadence of one metric over the query's range
OtelMetrics
| where metric_name == "bzrk.query.cache.peer.ops"
| summarize interval = otel_sample_interval($raw)
interval (timespan)
null

Example 2

// per metric, so a mixed-cadence fleet is not averaged into one figure
OtelMetrics
| summarize interval = otel_sample_interval($raw) by metric_name
metric_name (dynamic)interval (timespan)
"cpu.usage"00:01:00
"http.request.duration"00:31:00
"http.requests.total"00:01:00

Example 3

let interval = toscalar(
  OtelMetrics
  | where metric_name == "bzrk.query.cache.peer.ops"
  | summarize otel_sample_interval($raw)
);
OtelMetrics
| where metric_name == "bzrk.query.cache.peer.ops"
| summarize rate = otel_rate($raw) by bin_auto(timestamp, 2 * interval)

On this page