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)