Berserk Docs

Time-Series Analysis

Binning, aggregation, make-series, and visualization patterns for time-based data.

Most observability queries are time-series queries — counting events per interval, tracking latency over time, or comparing periods. This guide covers the core patterns and where Berserk differs from Microsoft KQL.

Binning with bin() and bin_auto()

The simplest time-series query groups events into fixed-width time buckets:

logs
| summarize count() by bin(timestamp, 5m)

This produces one row per 5-minute bucket. The bucket timestamp is the start of each interval, rounded down.

Automatic bin sizing with bin_auto()

When you don't know the right interval — or want the UI to pick one that matches the visible time range — use bin_auto():

logs
| summarize count() by bin_auto(timestamp)

bin_auto() reads the query's time range (from the time picker or CLI --since/--until) and selects a human-friendly interval that produces 750-1500 data points. The algorithm picks from a fixed set of intervals:

Time RangeBin Size
1 second1ms
1 minute100ms
1 hour10s
1 day1m
7 days10m
30 days30m
1 year6h

This is deterministic — the same time range always produces the same bin size.

Differs from Microsoft KQL

Microsoft's bin_auto uses an opaque server-side algorithm controlled by set query_bin_auto_size. In Berserk, bin_auto selects from 21 fixed human-friendly intervals targeting 750-1500 bins. The result is more predictable: you always know what bin size you'll get for a given time range.

Setting a minimum bin size

If your data is sparse and you don't want bins smaller than a threshold, pass a second argument:

logs
| summarize count() by bin_auto(timestamp, 1h)

The final bin size is max(min_span, computed_span). If the time range is 7 days (computed: 10m), the override forces 1h bins. If the time range is 1 year (computed: 6h), 6h wins because it's already larger.

Differs from Microsoft KQL

In Microsoft KQL, the minimum bin size is set via set query_bin_auto_size = 1h as a query option. Berserk uses a second positional argument instead: bin_auto(timestamp, 1h).

Aggregating time series with summarize

The summarize operator groups rows and applies aggregate functions. Combined with bin() or bin_auto(), it produces time series:

logs
| summarize
    total = count(),
    errors = countif(severity_number >= 17),
    p99_latency = percentile(duration_ms, 99)
  by bin(timestamp, 5m), service

This produces one row per (5-minute bucket, service) pair with three metrics.

Choosing between bin() and bin_auto()

Use bin() when:

  • You need a specific, fixed interval (e.g., "hourly report")
  • The query runs in automation or alerting where consistency matters
  • You're comparing two time windows and need matching bucket sizes

Use bin_auto() when:

  • The query backs a chart in the UI
  • Users control the time range with the time picker
  • You want zoom-friendly granularity (wider range = larger bins)

Building time series with make-series

summarize ... by bin() produces sparse output — if no events fall in a bucket, that bucket is missing. make-series fills gaps and returns arrays:

logs
| make-series
    request_count = count()
    default = 0
  on timestamp
    from datetime(2024-01-01) to datetime(2024-01-02) step 1h
    by service

This always produces exactly 24 entries per service, even if some hours had zero events. The result columns are:

  • timestamp — array of bucket start times
  • request_count — array of counts (with 0 for empty buckets)
  • service — the group key

When to use make-series vs summarize

Use make-series when:

  • You need gap-filling (zero-fill, forward-fill)
  • Downstream functions operate on arrays (series_fir, series_decompose_anomalies)
  • You're building data for render timechart

Use summarize when:

  • You want tabular output
  • Gaps should be omitted, not filled
  • You're aggregating without time (e.g., summarize count() by service)

Metrics: rate(), deriv(), and counter_rate()

Berserk adds three aggregate functions for working with OpenTelemetry metrics. These are not available in Microsoft KQL.

rate() — counter rate with reset detection

Computes per-second rate of change, automatically handling counter resets (where the value drops back to zero):

metrics
| summarize rate(value, timestamp) by bin(timestamp, 1m), service

Counters only go up (requests served, bytes sent). When a process restarts, the counter resets to zero. rate() detects this and doesn't produce a negative spike.

deriv() — gauge derivative

Computes rate of change for gauge metrics (values that go up and down, like memory usage or temperature):

metrics
| summarize deriv(value, timestamp) by bin(timestamp, 1m), host

Unlike rate(), this preserves negative changes — if memory drops, you see a negative derivative.

counter_rate() — OpenTelemetry cumulative counters

Designed for OTEL cumulative counters that include a start_time field. Uses start_time for reset detection instead of inferring resets from value drops:

metrics
| make-series r = counter_rate(value, start_time)
  on timestamp
    from ago(1h) to now() step 1m
    by service

Changing the rate duration

By default, rates are per second. Pass a third argument to change the unit:

metrics
| summarize rate(value, timestamp, 1m) by bin(timestamp, 5m), service

This produces per-minute rates instead of per-second.

Series analysis functions

After make-series produces arrays, series functions process them:

Smoothing and filtering

logs
| make-series requests = count() default = 0 on timestamp step 5m
| extend smoothed = series_fir(requests, repeat(1, 5), true, true)

series_fir applies a finite impulse response filter. The example uses a 5-point moving average (repeat(1, 5) with normalization).

Anomaly detection

logs
| make-series requests = count() default = 0 on timestamp step 5m by service
| extend (anomalies, score, baseline) = series_decompose_anomalies(requests)

Decomposes the series into trend + seasonal + residual components and flags anomalous points. The anomalies array contains -1 (low), 0 (normal), or 1 (high) for each point.

Trend fitting

metrics
| make-series cpu = avg(value) default = 0 on timestamp step 1h by host
| extend (rsquare, slope, variance, rvariance, intercept, fitted) = series_fit_line(cpu)
| where slope > 0.5

Fits a linear regression to each series. Filter on slope to find hosts with increasing CPU usage.

Visualization

render timechart

Appending | render timechart tells the UI to display results as a line chart:

logs
| summarize count() by bin_auto(timestamp), service
| render timechart

The render operator is a hint — it doesn't change the data, only how the UI displays it. Properties control appearance:

| render timechart with (title="Request Rate", ytitle="req/s", legend=hidden)

Other chart types

  • render barchart — categorical comparison (e.g., errors by service)
  • render areachart — stacked area (e.g., traffic by region)
  • render anomalychart — time series with anomaly bands

All chart types work with the same summarize ... by bin() pattern.

On this page