Which targets are down
Empty means every target answers. Why a target does not answer is in
the lastError field of the targets API (prometheus).
The Prometheus query language: select series by labels, turn a counter into a rate, add up and compare.
q() { promtool query instant http://localhost:9090 "$1"; }
q 'up'
1: target alive, 0: not
q 'up == 0'
only the targets that are down
q 'node_memory_MemAvailable_bytes'
a metric's series by name
q 'up{job="node"}'
series with a given label
q 'rate(node_cpu_seconds_total[1m])'
counter → per-second rate
q 'increase(prometheus_http_requests_total[5m])'
growth
q 'sum by (mode) (rate(node_cpu_seconds_total[1m]))'
a sum
q 'count by (job) (up)'
targets in each job
PromQL is the Prometheus query language. A query selects time series from the Prometheus database and computes over them: a growth rate, a sum, a ratio, a comparison with a threshold. Alerting rule expressions (prometheus) and grafana panels are written in PromQL.
The Prometheus web UI on :9090 draws the result as a graph. From the
terminal, promtool from the prometheus package runs the query:
Each line is one series: the metric name, labels in curly braces, the
value after =>, and a timestamp in Unix seconds after @
(date -d @1792643816 turns it into a date, see date). Empty
output with exit code 0 means "no such series", which is not an error.
A metric is a named quantity: node_memory_MemAvailable_bytes is the
available memory in bytes. Labels are name="value" pairs that say
what the number is about: mountpoint="/", cpu="0", mode="idle".
The name together with the full label set defines a time series, a
sequence of values with timestamps. There are as many series as there
are distinct label sets: on the training machine
node_filesystem_avail_bytes is three series, for /, /boot and
/boot/efi.
Prometheus adds the job and instance labels itself when it
collects metrics: job is the job name from scrape_configs,
instance is the target address (prometheus).
A selector picks series by name and labels:
A label value goes in double quotes. The regular expression after
=~ has to match the whole value: job=~"node" will not select
node2, you need job=~"node.*" (regex).
up is an instant vector: one latest value per series. A window in
square brackets, up{job="node"}[1m], gives a range vector: every
point of every series over the last minute. With a scrape every 15
seconds that is four points. Window units are s, m, h, d.
A range vector cannot be graphed on its own: it is passed to a
function that reduces the window's points to one number, such as
rate, increase, avg_over_time or max_over_time.
The metric type shows in the exporter output, on the # TYPE line
(node-exporter):
| Type | Behaviour | How to read it |
|---|---|---|
counter |
only grows from process start and starts from zero again after a restart; the name usually ends in _total |
through rate or increase |
gauge |
the current value, goes up and down | as is, compare with a threshold |
histogram |
_bucket{le="…"}, _sum and _count series: how many observations fell into each bucket |
_sum and _count through rate |
The raw value of a counter says nothing about load: 23.82 seconds of CPU idle time since boot could be a lot or a little. What matters is how fast it grows.
rate(counter[window]) is the average growth rate over the window,
per second. increase(counter[window]) is the growth over the window,
which is rate multiplied by the window length.
CPU seconds per second is a share of time: 0.9 for mode="idle"
means the core was idle 90% of the time. Both functions handle counter
resets: when the value drops, the process has restarted, and growth is
counted from zero without a negative rate.
The window has to hold at least two points; four scrape intervals is
the usual choice: [1m] with scrape_interval: 15s. [10s] with a
scrape every 15 seconds returns nothing. rate is also empty during
the first tens of seconds after Prometheus starts: there is no second
point yet.
rate and increase are for counters only. For a gauge, the growth
rate is deriv, the change over the window is delta, the average
and the maximum are avg_over_time and max_over_time.
sum, avg, min, max and count reduce series to one or to
groups by label:
After by (…) the result keeps only the listed labels; after
without (…) it keeps all but the listed ones:
rate first, then sum: a sum of counters from different processes
resets whenever any one of them restarts.
+ - * / between two sets of series work pairwise, for series with the
same labels (the metric name does not count). Between a number and a
series they apply to every series:
After arithmetic the result has no metric name, only labels:
The comparisons >, <, >=, <=, == and != act as a filter:
they keep the series for which the condition holds, with their own
value, and drop the rest. An empty answer means the condition holds
for no series. With the word bool a comparison returns every series
with the value 1 or 0:
% in PromQL is the remainder of a division (7 % 3 is 1); the
language has no percent. "Below 10%" is written as a ratio, < 0.1, or
the ratio is multiplied by 100 and compared with 10. … * 100 < 10%
does not parse: parse error: unexpected end of input.
An alerting rule expression is the same kind of comparison: the alert fires for every series that passes the filter (prometheus).
When the label sets of the two sides differ, no pairs are found and
the answer is empty: node_filesystem_avail_bytes has mountpoint,
device and fstype, node_memory_MemTotal_bytes does not.
on (instance) compares only the listed labels, ignoring (…) all
but those. When several series on one side match one series on the
other, you need group_left:
and, or and unless work on sets of series: a unless b is the
series of a that have no series with the same labels in b.
absent(…) returns 1 when there are no series at all:
absent(up{job="app"}) means there is no app job.
promtool query instant evaluates the expression at one moment, now.
promtool query range evaluates it at every point of a period with a
--step, the way Grafana draws a graph:
The API does the same: /api/v1/query and /api/v1/query_range. Pass
the expression with --data-urlencode: it contains curly braces,
quotes and spaces. The value in the answer is a string next to the
timestamp:
Long output lines are wrapped here. To get the value as a number in
jq, use .value[1] | tonumber.
Empty means every target answers. Why a target does not answer is in
the lastError field of the targets API (prometheus).
On the training machine the answer is about 42: the root is 42% full.
For a "disk almost full" rule, add a comparison: … > 90.
avg by (instance) averages the machine's cores: a four-core machine
has four mode="idle" series, and you want one number per machine.
The metric name and labels depend on the application: look them up in
its /metrics. sum on both sides drops the labels, so the division
is a number by a number.
Returns 1 when there are no series: the job was removed from the
config, or there is a typo in the name. up == 0 does not catch this
case: there is simply no up series.
On the training machine one node_exporter gives about 1300 series. Every new label value is a new series: a label with a request ID or a client address bloats the database.
promtool printed nothing and exited with code 0: no series matched
the expression:
curl -s localhost:9090/api/v1/label/job/values; which series a
metric has: /api/v1/series with --data-urlencode 'match[]=up';> …;rate window is shorter than two scrape intervals, or
Prometheus started less than a minute ago;node_network_*: it only has lo, and
the exporter skips it (node-exporter).rate has no window: rate(x) instead of rate(x[1m]).
The first is a label value without quotes: up{job=node} instead of
up{job="node"}. The second is grouping labels without parentheses:
sum by job instead of sum by (job). 1:8 is the line and the
position of the error in the expression.
After on (…) or ignoring (…), one series on one side matches
several on the other. If that is intended, add group_left (many
series on the left) or group_right (on the right).
The raw counter value dropped to zero: the process restarted. rate
and increase account for that. delta and the subtraction
x - x offset 5m do not: at a reset they give a negative number.
Prometheus keeps the selected points in memory while it evaluates a
query. {__name__=~".+"} over [1d] or a rate over every series for
a week can take all the memory of a 512 MiB machine, and the kernel
will kill Prometheus (oom-killer). Start with a narrow selector
and a short window.
@[…] timestamp into a date.=~.Found an inaccuracy? Report an error in the article