What SignalDB's /prometheus query API supports today, and what it doesn't
yet. See Query metrics with PromQL for usage.
SignalDB lowers a parsed PromQL expression to a DataFusion plan over the
metrics Iceberg table, filtered by metric_type (gauge/sum/histogram).
Range aggregations bucket samples with date_bin(step) rather than a sliding
window — exact when the query step equals the range, an approximation
otherwise. Anything listed as unsupported returns a clear error rather than a
wrong result.
Aggregation operators (sum, avg, min, max, count, … with or without
by/without) first reduce each series to its latest sample in the step,
then aggregate across series, as Prometheus does. Only the _over_time and
range functions fold across time within a series.
✅ (all four operators on service_name and on materialized labels; on map-typed tables any attribute supports all four; legacy JSON tables: =/!= substring only)
__name__ matcher
✅ (=, !=, =~, !~; regex patterns are fully anchored, as in Prometheus)
Range vector selector metric[5m] (as a function argument)
✅ (pins to the instant, 5-min lookback, replicated across steps)
Subqueries expr[5m:1m]
✅ (under an _over_time reducer; inner evaluated at the resolution)
SignalDB stores metric names in their OTel dotted form (signaldb.wal.entries_pending, process.memory.usage), which is what discover_metrics and /prometheus/api/v1/label/__name__/values return. A dotted name can be used bare — signaldb.wal.entries_pending, process.memory.usage{service_name="signaldb"}, rate(signaldb.ingest.spans_received[5m]) — and SignalDB rewrites it to the quoted forms standard PromQL already supports: {"signaldb.wal.entries_pending"} or {__name__="signaldb.wal.entries_pending"}.
Tracked under epic #328 and #336. The full PromQL function and operator
surface is now supported. Remaining differences from upstream Prometheus are
backend-specific rather than missing features: service_name and any
configured materialized labels
are queryable metric labels — matchers filter on them exactly, by/without
group on them, and they are part of each series' natural identity (a bare
selector or rate() emits one series per label combination). Other labels
live in JSON attributes. Label-writing (label_replace/label_join),
count_values, and vector matching (on/ignoring/group_left) still
operate over service_name/__name__ only; and range aggregations use fixed
date_bin step buckets rather than a sliding window (exact when the query
step equals the range).