agentleFS
mattstrublemattstruble/skills/promql/SKILL.md

promql

Skill2 starsChanged 4 months agoMITView on GitHub

You MUST consult this skill when writing or debugging PromQL queries for Prometheus — dashboards, alerting rules, recording rules, or ad-hoc metric exploration. Also trigger on rate vs irate decisions, histogram quantiles, predict_linear for capacity planning, absent() for uptime alerts, or vector matching errors. NOT for Prometheus server deployment (see homelab-monitoring), Loki log queries (see logql), or Grafana datasource configuration.

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Add bool modifier for 0/1 output instead of filtering: up == bool 1

    line 42
  2. Distinctive:

    When combining two metrics, Prometheus matches on all shared labels by default.

    line 48
  3. Very specific:

    Range window rule: use >= 4× scrape interval.

    line 79
  4. Distinctive:

    If you aggregate across jobs/instances and want per-job quantiles:

    line 103
  5. Very specific:

    Run an instant query over a range — useful for max_over_time of a derived metric:

    line 112

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

PromQL

PromQL is Prometheus's functional query language. This skill covers homelab usage with node-exporter, cAdvisor/Docker metrics, and app-specific exporters (Plex, Sonarr, Radarr, Prowlarr, Home Assistant, AdGuard Home).

Data Types

TypeDescriptionWhen you have it
Instant vectorOne sample per series at current timeBare metric name or {} selector
Range vectorMultiple samples over a windowMetric with [5m] appended
ScalarSingle numberscalar(), arithmetic on instant vectors
StringUnused in practice—

Range vectors can only be passed to functions (rate, avg_over_time, etc.) — they can't be graphed directly.

Selectors

node_cpu_seconds_total{mode="idle", instance="homelab:9100"}   # exact match
container_memory_usage_bytes{name=~"sonarr|radarr"}            # regex match
up{job="plex"}[5m]                                             # range vector
up offset 1h                                                   # 1h ago

Label matchers:

  • = exact, != not equal
  • =~ regex (RE2, fully anchored — sonarr matches only sonarr, use .*sonarr.* for substring)
  • !~ negated regex

Operators

Arithmetic: +, -, *, /, %, ^

Comparison: ==, !=, >, <, >=, <=

  • Add bool modifier for 0/1 output instead of filtering: up == bool 1

Logical: and, or, unless

Vector Matching

When combining two metrics, Prometheus matches on all shared labels by default. Mismatches cause empty results or errors.

a / on(job, instance) b          # match only on these labels
a / ignoring(status_code) b      # match on all labels except these
a / on(job) group_left() b       # many-to-one: left side has more series
a / on(job) group_right() b      # one-to-many: right side has more series

Diagnose mismatches: count by (instance) (metric_a) vs count by (instance) (metric_b) — labels must align.

Aggregation

sum by (instance) (rate(node_cpu_seconds_total[5m]))     # keep instance, drop rest
avg without (cpu) (rate(node_cpu_seconds_total[5m]))     # drop cpu, keep rest
topk(5, rate(container_cpu_usage_seconds_total[5m]))     # top 5 containers by CPU

Operators: sum, avg, min, max, count, topk, bottomk, stddev, stdvar, quantile, group.

Rate Functions — Decision Table

FunctionInputUse When
rate(v[d])Counter rangeDefault for counters (_total) — handles resets, good for alerting
irate(v[d])Counter rangeHigh-resolution spikes — not for alerting (too volatile)
increase(v[d])Counter rangeTotal increase over window (e.g., requests in last hour)
delta(v[d])Gauge rangeChange in a gauge over window
deriv(v[d])Gauge rangeRate of change for gauges (linear regression)

Range window rule: use >= 4× scrape interval. With 15s scrape: use [60s] minimum, [5m] is typical.

Aggregation Over Time

For smoothing or summarizing a metric over a window without rate:

avg_over_time(node_load1[30m])          # 30-min average of load
max_over_time(container_memory_usage_bytes{name="plex"}[1h])

Functions: avg_over_time, min_over_time, max_over_time, sum_over_time, count_over_time, quantile_over_time, last_over_time.

Histograms

Classic histograms (most exporters): preserve the le label when aggregating.

# 95th percentile request latency
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
)

If you aggregate across jobs/instances and want per-job quantiles:

histogram_quantile(0.95,
  sum by (le, job) (rate(http_request_duration_seconds_bucket[5m]))
)

Subqueries

Run an instant query over a range — useful for max_over_time of a derived metric:

max_over_time(rate(container_cpu_usage_seconds_total[5m])[1h:1m])
# reads as: "max of the 5m rate, sampled every 1m, over the past 1h"

Label Functions

label_replace(metric, "dst_label", "$1", "src_label", "(.*)")
label_join(metric, "dst_label", "/", "src1", "src2")

Prediction (Homelab Killer Feature)

predict_linear fits a linear regression and extrapolates forward:

predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600) < 0
  • First arg: range vector — the window determines regression accuracy. Use 6h–24h for disk (slow trends). Too short = noisy; too long = misses recent changes.
  • Second arg: seconds to project ahead (24*3600 = 24 hours)
  • Result < 0 means "disk will be full within 24h"

Existence / Uptime Checks

absent(up{job="sonarr"})                   # fires when sonarr disappears from scrape
absent_over_time(up{job="plex"}[5m])       # fires when plex has been down 5+ minutes

absent returns a 1-element vector when the selector matches nothing. Use in alerting rules to detect services that stopped reporting.

Important caveats:

  • absent(up{job="sonarr"}) only fires when the entire job disappears. With multiple instances, it won't fire if one instance is down but others are up. For per-instance alerting, use up{job="sonarr"} == 0 with for: 5m instead.
  • absent_over_time detects missing scrape data (exporter unreachable), not up == 0 (service down but exporter running). For "service down", use up{job="plex"} == 0.

Common Mistakes

MistakeFix
rate(node_memory_MemAvailable_bytes[5m])Memory is a gauge — use it directly: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
sum(http_requests_total) without rateWrap in rate() first: sum(rate(http_requests_total[5m]))
sum(rate(http_request_duration_seconds_bucket[5m]))Missing by (le) — histogram quantile needs le label
rate(metric[15s]) with 15s scrape intervalRange must be >= 4× scrape: use [60s] minimum
irate() in alerting rulesUse rate() — irate is too volatile for reliable alerts
Regex {name="sonarr"} matching nothingRE2 is fully anchored; use {name=~".*sonarr.*"} for substring or exact {name="sonarr"}

Recording Rules Naming

Convention: level:metric:operations

groups:
  - name: homelab.rules
    rules:
      - record: instance:node_cpu:rate5m
        expr: 1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))

Homelab Examples

CPU usage % per node:

1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))

Memory pressure:

1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)

Disk filling prediction (alert when full within 24h):

predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600) < 0

Container down alert (single instance — exporter unreachable):

absent(up{job="sonarr", instance="homelab:8989"})

Container down alert (service down, exporter still running):

up{job="sonarr"} == 0

Error rate % (returns no result — not 0% — when there is no traffic):

sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
/ sum by (job) (rate(http_requests_total[5m])) * 100

Top 5 containers by memory:

topk(5, container_memory_usage_bytes{name!=""})

Alerting rule example:

groups:
  - name: homelab.alerts
    rules:
      - alert: DiskFillingSoon
        expr: predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600) < 0
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "Disk on {{ $labels.instance }} fills within 24h"

Note: keep for: much shorter than the prediction horizon. for: 1h with a 24h prediction means the alert only fires after the condition has been true for 1 hour — by then you may have less than 23h of lead time.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.