PromQL
Scope: the query language — just enough of it, properly understood, to answer the questions a small system actually asks. Every query in this chapter was run against the live two-target setup from the previous chapters; values are the real returns, lightly trimmed.
Two kinds of answer
An instant query evaluates at one moment:
node_load1
→ instance="127.0.0.1:9100", job="node" ⇒ 0.97
A range query evaluates the same expression at many moments and returns a series of points. The web UI's graph view is a range query; so is every Grafana panel. Over four minutes spanning the load spike in the worked example, the CPU-busy expression returned 13 points (at a 15 s step) climbing from the 40–75 range to 80.9 — the whole spike, as data.
In the HTTP API these are /api/v1/query and
/api/v1/query_range, returning vector and
matrix results respectively. Everything else in
this chapter is expressions — the API just carries
them.
Selectors: asking for series by name
A bare metric name returns every series with that name. Label selectors narrow it:
node_cpu_seconds_total{mode="idle"}
There are four matchers: = exact, != not
equal, =~ regex, !~ not-regex. One warning
that saves real pain: a selector that matches
nothing is not an error. node_cpu_seconds_total{mode="idl"}
returns status: "success" and an empty result —
verbatim from the live server. PromQL cannot know
you typo'd. Chapter 11 has the debugging routine.
Why counters need rate()
Counters only ever rise, so their raw values are
meaningless across restarts and uptimes. rate()
takes a range vector — a selector with a time
window — and returns the per-second increase,
handling resets:
rate(node_cpu_seconds_total{mode="idle"}[5m])
→ cpu="0" ⇒ 0.126 cpu="1" ⇒ 0.129
Read: on each core, the idle-seconds counter grew
by about 0.13 s per second — the cores were idle
~13% of the time in that window. The window needs
at least two samples inside it: at a 15 s scrape
interval, [5m] holds ~20 samples, [2m] about
8, and [30s] is skating on two. Prefer [5m]
for graphs, [2m] when an alert must react
sooner — and know the trade you're making.
The greatest hits, run for real
CPU busy % — the complement of idle:
100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
avg() collapses the per-core series into one
number. The alerting-shaped version keeps one row
per machine: swap avg(...) for
avg by (instance) (...) and give the window as
[2m]. On the test machine that expression read
51.4 at rest and 80.9 mid-spike.
Where did the CPU time go? Aggregation with
by:
sum by (mode) (rate(node_cpu_seconds_total[5m]))
→ idle 0.256 · user 0.011 · system 0.015 · iowait 0.016 · softirq 0.001 · steal 0.000
(On a 2-core box the modes sum to at most 2.0.) This is the query to run before believing a "CPU problem": user time means your programs; iowait means the disk is the queue; steal means the host is oversubscribed and your neighbours are eating your lunch.
Memory used %:
100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes))
→ 90.3
The test machine was busy with other work — that
number is honest, not broken. Note the formula
uses MemAvailable, the kernel's own estimate of
what's freeable (it credits the page cache), not
MemFree, which would cry wolf on any healthy
Linux box.
Disk used %:
100 - (100 * node_filesystem_avail_bytes{mountpoint="/"}
/ node_filesystem_size_bytes{mountpoint="/"})
(Shown with the reader's root filesystem; the live run targeted the test machine's data volume and returned 7.5.) Add the oldest trick in operations — extrapolate the disk's own trend:
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*24*3600) < 0
This is true when free space, falling at its 6-hour rate, hits zero within four days: the "disk filling" alert that has saved more weekends than any other in this guide.
Percentiles from histograms:
histogram_quantile(0.9, sum by (le) (rate(prometheus_http_request_duration_seconds_bucket[5m])))
→ 0.09
The 90th percentile of Prometheus's own query
latency: 0.09 s. Two non-obvious parts: you
sum by (le) first (the function needs the
bucket series grouped by their le label), and
the input is a rate over buckets — percentiles of
recent observations, not of all time.
Check: the full set lives in
examples/promql-cheatsheet.txt. Run each against
your own node target; the values will differ, the
shapes shouldn't.