make-series
Creates series of aggregated values along a specified axis, typically time-based.
Each output column is a dynamic array with one element per bin: the axis
column plus one array per aggregate. Bins with no rows are filled from
default= (0 when omitted), so the arrays are dense and all the same length.
An aggregate must return a value a series can carry — a numeric
(int/long/real), bool, datetime or timespan. Those all have a
numeric representation, which is what lets a bin be gap-filled and the
series_* functions do arithmetic over the array. An aggregate returning
string, guid or dynamic is rejected; use summarize for those.
The check is on the aggregate's result, not its name, so the same function
can be valid or not depending on its argument: max(duration) and
max(timestamp) are series, max(service_name) is not. Multi-column
aggregates (percentiles(x, 50, 95), arg_max) return dynamic and are
likewise rejected — take one value per call (percentile(x, 95)) instead.
ADX applies the same restriction.
A dynamic argument is not rejected by this: OTel data reaches aggregates as
$raw.x, and it is cast to the aggregate's expected type first, so
make-series max($raw.duration) builds a numeric series rather than failing.
The exception, for max, min and take_any. Those three accept every
scalar type, so "the expected type" is ambiguous and the cast resolves to
numeric. That is right when the dynamic holds a number — which is how OTel
carries durations and counts — but a dynamic holding a datetime or
timespan extracts as null, and the series comes back empty rather than
erroring. Cast explicitly when that is the shape:
| make-series last_seen = max(asdatetime($raw.event_time)) on timestamp step 1h | make-series slowest = max(astimespan($raw.took)) on timestamp step 1h
This applies to those three only. Every other aggregate either declares
dynamic as its input type and needs no cast at all (otel_rate($raw) and
the rest of the OTel family, which is what the metrics charts use), or is
numeric to begin with (sum, avg, stdev, percentile, …) so the numeric
cast is the correct one. There is no need to cast make-series arguments in
general.
Syntax
make-series aggregation on column [from start] [to end] step interval [by group]Create time series
Parameters
| Name | Description |
|---|---|
| aggregation | Aggregation function to apply |
| column | Column defining the series axis (usually datetime) |
| start | Start of the axis range. When omitted, inferred from data min. (optional) |
| end | End of the axis range. When omitted, inferred from data max. (optional) |
| interval | Bin size for the axis |
| group | Grouping expression (optional) |
Examples
Example 1
datatable(ts:datetime, silver:long, region:string)[
datetime(2024-01-01), 100, "England",
datetime(2024-01-02), 250, "England",
datetime(2024-01-03), 180, "England",
datetime(2024-01-01), 50, "Francia",
datetime(2024-01-02), 300, "Francia",
datetime(2024-01-03), 120, "Francia"
]
| make-series total_loot = sum(silver) on ts
from datetime(2024-01-01) to datetime(2024-01-04) step 1d
by region| region (string) | total_loot (dynamic) | ts (dynamic) |
|---|---|---|
| England | [100,250,180] | [2024-01-01T00:00:00Z,2024-01-02T00:00:00Z,2024-01-03T00:00:00Z] |
| Francia | [50,300,120] | [2024-01-01T00:00:00Z,2024-01-02T00:00:00Z,2024-01-03T00:00:00Z] |
Example 2
datatable(ts:datetime, raids:long)[
datetime(2024-01-01), 3,
datetime(2024-01-02), 5,
datetime(2024-01-03), 2,
datetime(2024-01-04), 7
]
| make-series total = sum(raids) on ts step 1d| total (dynamic) | ts (dynamic) |
|---|---|
| [3,5,2,7] | [2024-01-01T00:00:00Z,2024-01-02T00:00:00Z,2024-01-03T00:00:00Z,2024-01-04T00:00:00Z] |