Prometheus sources
A source has a kind (Source kinds). A TimescaleDB/PostgreSQL source answers SQL. A Prometheus source answers PromQL: Prometheus itself, or anything that speaks its HTTP query API, such as Thanos, Mimir or VictoriaMetrics. Everything the app guarantees about a SQL panel holds for a PromQL one. The model writes a spec and never data. The query is untrusted and guarded. The server owns the time window. A spec names its source by an opaque id and nothing else.
The source
Section titled “The source”A Prometheus source’s config is its endpoint and its catalog:
{ "kind": "prometheus", "url": "https://prometheus.example.com", "auth": "bearer", "metrics": [ { "name": "http_requests_total", "type": "counter", "help": "Requests served.", "labels": ["code", "instance", "job", "route"] }, { "name": "up", "type": "gauge", "labels": ["instance", "job"] } ], "rowFilter": { "label": "tenant", "claim": "tenant" }}urlis where the server sends queries. A source admin chooses it, so it is held to the same rules as a model base URL: publichttpsunless the operator’sSOURCE_URL_ALLOWLISTnames the host, no credentials in it, and no redirects followed. See Prometheus operations.authisnone,bearerorbasic. The credential is never in the config. A source withbearerorbasicnames asecret_ref, and the server resolves<REF>_TOKENor<REF>_USERNAME/<REF>_PASSWORDwhen it connects.noneis for an in-cluster endpoint and names no ref.metricsis the allowlist, the PromQL counterpart of a SQL source’s tables. Each metric has a type, an optional help line and the labels its series carry. A histogram is listed as its series:…_bucket(withle),…_countand…_sum. An expression may select only listed metrics.rowFilteris optional. It names a tenant label every listed metric carries, and the identity claim whose value the viewer’s queries are narrowed to. See row-level filters.
The Data sources page registers one through the same dialog as a database, with Prometheus (PromQL) as the kind. Discover metrics reads the endpoint’s metadata and offers a searchable menu, since a real server has thousands of metrics, and ticking one asks the endpoint for its labels. Test checks the connection, the auth, the build and the write surface, read from the endpoint’s flags and never probed, then confirms that each allowlisted metric has series. Refresh re-reads the labels and types without adding or dropping a metric: the allowlist stays the author’s.
A PromQL panel
Section titled “A PromQL panel”A panel on a Prometheus source carries query.promql instead of query.sql,
and no timeField:
{ "id": "rps", "title": "Requests per second by route", "viz": "line", "query": { "sourceId": "prom-prod", "promql": "sum by (route) (rate(http_requests_total[5m]))", "minStep": "1m" }, "layout": { "x": 0, "y": 0, "w": 6, "h": 4 }}A panel’s language is its source’s. A PromQL panel pointed at a SQL source is refused by name (“source answers SQL; this query is PromQL”) wherever it would run, and so is the reverse. The editor never saves one: moving a panel to a source of the other kind restarts its query from that source’s starter.
instant asks for one sample per series at the window’s end instead of a
range. It is for kinds that draw one value per series: a stat, a gauge, a
table, a pie or donut, a status grid, a histogram, a treemap, and a bar
compared across a label. minStep is a
floor on the step. The server picks the step and only ever raises it.
The result’s shape is what the panel kinds read:
- A range query answers a
timecolumn and one numeric column per series, named the way the Prometheus UI names a legend entry:{route="/api"}, ormetric{…}when the series have different names. So a line has one line per series, andsum by (route) (…)decides what those are. - An instant query answers one row per series, one column per label, then
value.
heatmap, scatter, state-timeline and logs read row shapes a PromQL
result does not have, and the model is told not to use them for a Prometheus source.
The guard
Section titled “The guard”validatePromql (src/lib/promql/safety.ts) is the PromQL counterpart of the
SQL guard, and Executing a panel
lists its rules. In prose:
- One expression, made of what the walker allows. The parser is the
Prometheus project’s own grammar. Comments, the experimental functions and
info()are refused, as is anything the walker does not know. - Only listed metrics. Every selector names exactly one metric, and that
metric is on the allowlist. A selector without a name, or with a
__name__pattern, could read any metric, so it is refused. - No time of its own. The
@modifier is refused in every spelling, and a range, subquery oroffsetis bounded byPROMQL_MAX_RANGE. The[5m]inrate(…[5m])is a lookback, not a window, and is fine. - Bounded. At most 8,000 characters, 32 selectors and 64 levels of nesting. A subquery evaluates at most 11,000 points.
The guard also gives hints that never refuse: a label the catalog does not
list for a metric, rate() over a gauge, and a counter drawn without rate()
or increase(). The editor underlines them in place.
How time becomes start, end and step
Section titled “How time becomes start, end and step”A SQL panel is windowed by a WHERE the server adds on timeField. A PromQL
panel has no WHERE: the server asks the endpoint for the window itself.
- A range query goes to
/api/v1/query_range.startandendare the dashboard’s (or the panel’s) window, resolved on the server and aligned down to a multiple of the step, so two polls a few seconds apart ask for the same points. The step is the window divided byPROMQL_MAX_POINTS(1,000 by default), never finer thanPROMETHEUS_MIN_STEP_MS(15 s), and raised to the panel’sminStepwhen it sets one. When the point cap is what widened the step, the result says so and the panel shows a badge with the count the finest step would have drawn. - An instant query goes to
/api/v1/querywithtimeat the window’s end.
The plan dialog (What runs in the editor, Show query on a panel) shows the expression as the endpoint receives it, each parameter with what the server resolved it from, and the limits. See Seeing what actually runs.
Variables and the row filter as matchers
Section titled “Variables and the row filter as matchers”Both enter a PromQL expression the only way that cannot change what it is: as a label matcher’s value.
- A variable is written as a matcher’s whole value,
{host=":host"}, or{host=~":host"}for several values. The server writes the picked value in as an escaped string literal, re-parses the result and checks the tree is the original with only those literals changed. A variable anywhere else is refused. A Prometheus source’s variable is usually label values:{ "sourceId": "prom-prod", "label": "instance", "match": "up{job=\"node\"}" }. See Dashboard variables. - The row filter adds
tenant="<the viewer's claim>"to every selector, including those inside functions and subqueries, before the query runs. The viewer’s queries cannot read another tenant’s series, however they are written.
A drilldown can read one label of a clicked series with { "label": "instance" }:
see Drilldown.
What is refused, and why
Section titled “What is refused, and why”| Refused | Because |
|---|---|
| A metric not on the allowlist, or a selector that names none | The allowlist is the catalog. A query reads what it lists and nothing else. |
@, or a range or offset past PROMQL_MAX_RANGE |
The server owns time. |
| PromQL against a SQL source, or SQL against a Prometheus one | A query is in its source’s language. |
More than PROMETHEUS_MAX_SERIES series in a result |
A panel aggregates. A thousand lines read as one smear. |
| A native histogram in a result | Its buckets do not fit a row, so the panel says so instead of drawing something wrong. |
A source URL that is private, loopback or plain http and not on SOURCE_URL_ALLOWLIST |
The server would otherwise make requests, possibly with a credential, to any address a source admin typed. |