pedantic.run PromQL & DataPrime · A Critical Apparatus Ed. MMXXVI
Plate I · A query, annotated
●  Slow
01
topk1(5, sum by (pod)2 (rate(node_cpu_seconds_total{mode=~".*"3}[3h]4)))

pedantic.run reads a PromQL or DataPrime query and returns, over a single HTTP call, the patterns that get expensive at runtime — negative-regex matchers, unfiltered aggregations, high-cardinality groupings.

Send a query; receive JSON findings. No auth. No state. No opinions it cannot defend.

§ I The instrument

A query, submitted for inspection.

Specimen saturation · topk
$
topk(5, sum by (pod) (rate(node_cpu_seconds_total{mode=~".*"}[3h])))
Findings, marked
01
topk(5, sum by (pod) (rate(node_cpu_seconds_total{mode=~".*"}[3h])))
Response · application/json
{
  "query": "topk(5, sum by (pod) (rate(node_cpu_seconds_total{mode=~\".*\"}[3h])))",
  "summary": { "slow": 3, "moderate": 1 },
  "findings": [
    {
      "selector": "node_cpu_seconds_total{mode=~\".*\"}",
      "category": "aggregation",
      "verdict":  "slow",
      "codes":    [ { "code": "HIGH_CARD_GROUPING" }, { "code": "SORT_AGGREGATION" } ]
    },
    {
      "selector": "node_cpu_seconds_total{mode=~\".*\"}",
      "category": "matchers",
      "verdict":  "slow",
      "codes":    [ { "code": "REGEX_MATCHER" }, { "code": "CATCHALL_MATCHER" }, { "code": "UNANCHORED_REGEX" } ]
    },
    { "category": "range_vectors", "verdict": "slow",     "codes": [ { "code": "LONG_RANGE_VECTOR" } ] },
    { "category": "range_vectors", "verdict": "moderate", "codes": [ ] }
  ]
}
§ II Send a query

One POST. application/json.

$ curl -s https://pedantic.run/api/promql/analyze \
  -H 'content-type: application/json' \
  -d '{"query":"topk(5, rate(container_memory_usage_bytes{pod=~\".*\"}[10m]))"}'

The query is read from the first of these that is present — in order, no negotiation:

?q= param
POST query string.
POST /api/promql/analyze?q=rate(…)
query field
JSON or form body.
{"query": "rate(…)"}
raw body
text/plain request body.
POST body: rate(…)
§ III The instrument, at hand · a terminal client

The same apparatus, in your terminal.

A terminal client for the same endpoint. Type a query, press ctrl+enter, and read a colour-coded breakdown of where it will hurt — same verdicts, same objections, never touching your Prometheus.

Install · from source
$ go install github.com/schultyy/pedantic.run_cli@latest
$ pedantic

Or download a prebuilt binary (Linux & macOS, amd64 & arm64) from the releases page.

A session
topk(10, sum by (pod) (rate(container_cpu_usage_seconds_total{env="prod"}[5m])))

Cost breakdown
████████████████   1 slow   1 moderate   2 fast

│ SLOW  ·  aggregation
 container_cpu_usage_seconds_total{env="prod"}
 • High-cardinality grouping  (HIGH_CARD_GROUPING)
   Grouping by an unbounded label (request_id, trace_id, pod, …).
 • Sorting aggregation  (SORT_AGGREGATION)
   topk / bottomk / quantile must sort the full result set.
§ IV The verdicts · worst first

How worried to be.

invalid Doesn't type-check — won't run.
slow Expensive whatever your data.
runtime_dependent Cost depends on cardinality.
moderate Noticeable but usually fine.
fast No concerns found.
§ V Status codes

Three outcomes.

200 Analyzed — findings returned.
400 Empty query — nothing to analyze.
422 Could not parse or analyze the query.
§ VI The codes · PromQL

Every objection, by name.

matchers
BARE_SELECTOR
Selector with no label filters — scans every series for the metric.
REGEX_MATCHER
Uses =~ / !~ instead of equality; harder to index.
NEGATIVE_REGEX
!~ must scan all series just to exclude some.
CATCHALL_MATCHER
A .* / .+ value matches everything — no real filtering.
UNANCHORED_REGEX
Regex not anchored at both ends; can't shortcut the lookup.
range_vectors
LONG_RANGE_VECTOR
Window long enough to load a lot of samples (> 59m / > 2h).
SHORT_RANGE_VECTOR
Sub-minute window that may under-sample the metric.
TEMPLATED_RANGE
Window is a dashboard variable (${__range}); cost depends on the selected range.
NO_AGGREGATION_HIGH_CARD
A high-cardinality selector returned raw, with no aggregation.
SUBQUERY
A subquery re-evaluates the inner query at every step.
NESTED_SUBQUERY
A subquery inside a subquery multiplies that work.
aggregation
HIGH_CARD_GROUPING
Grouping by an unbounded label (request_id, trace_id, pod, …).
SORT_AGGREGATION
topk / bottomk / quantile must sort the full result set.
structure & type_error
VECTOR_JOIN
A binary op with empty on() matching.
REDUNDANT_SUBEXPR
The same sub-expression is computed more than once.
ARITY_MISMATCH
A function called with the wrong number of arguments.
ARG_TYPE_MISMATCH
An argument has the wrong type (e.g. instant where a range is required).
§ VII The same apparatus · DataPrime

A pipeline, read stage by stage.

A DataPrime query is a pipeline of stages. Each finding is anchored to the stage (groupby, orderby, …) it applies to — same verdict scale, same JSON shape, same single POST.

$
source logs | groupby path aggregate avg(duration) | orderby _avg desc
Response · application/json
{
  "query": "source logs | groupby path aggregate avg(duration) | orderby _avg desc",
  "summary": { "slow": 2, "runtime_dependent": 1 },
  "findings": [
    {
      "stage":   "groupby",
      "verdict": "slow",
      "detail":  "path",
      "code":    { "code": "HIGH_CARD_GROUPING" }
    },
    {
      "stage":   "orderby",
      "verdict": "slow",
      "detail":  null,
      "code":    { "code": "SORT_MATERIALIZE" }
    },
    {
      "stage":   "groupby",
      "verdict": "runtime_dependent",
      "detail":  null,
      "code":    { "code": "MISSING_FILTER_BEFORE_AGG" }
    }
  ]
}
DataPrime codes
REGEX_MATCH
A regex match on a field — runs a regex per row.
UNANCHORED_REGEX
Leading .* / .+ or no start anchor — no index shortcut.
HIGH_CARD_GROUPING
groupby / multigroupby on a likely-unbounded field.
HIGH_CARD_DISTINCT
distinct on an unbounded field — one row per value.
NEGATIVE_FILTER
Negation / not-contains / negative regex — scan, then exclude.
FULLTEXT_SCAN
Free-text / lucene / wildfind — full scan of message bodies.
SORT_MATERIALIZE
orderby / top / bottom — materialize and sort the result set.
EXPENSIVE_JOIN
join / enrich / union / stitch — cost scales with both sides.
MISSING_FILTER_BEFORE_AGG
groupby / aggregate with no upstream filter — touches everything.