ObservabilityLXXXV · CI ValidationCIValidation
promtool check config
What you'll learn
- Run promtool check config against prometheus.yml and interpret the exit code
- Distinguish what promtool check config validates from what it does not
- Wire the check into a GitHub Actions workflow with the official action
- Diagnose the four most common failure shapes when the check rejects a file
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A team merges a change to prometheus.yml that adds a new
remote-write queue. The merge happens at 14:09. At 14:14 the
configuration is reloaded, and Prometheus rejects the file
because remote_write accepts only one block per name in this
version — the new entry has a duplicate key. Every other
change in the same reload, including an alert-tuning fix for a
page that was firing too often, is held behind the typo. The
on-call engineer spends the next forty minutes reading the
diff, then the next ten minutes wondering why a five-character
fix took the whole configuration with it. The static check
catches this exact mistake in 200 milliseconds, but it has to
be wired into the pipeline for it to do its job.
What it is
promtool check config <path> parses the named configuration
file as Prometheus would parse it at reload time. It walks the
YAML, resolves the rule_files globs and parses every matched
file as a rule group. It then validates the schema of every
block against the daemon’s compile-time types. The command
exits 0 if every block passes, and exits non-zero with a file
name, line number and error message if any block fails.
Two properties to internalise:
- It uses the same code as the running daemon. promtool
links against
config.DefaultLoadConfigandconfig.DefaultGlobalConfig. What the tool accepts, the daemon accepts at reload. This is why the check is trustworthy — it does not re-implement the schema, it calls into it. - It is read-only. The check parses and validates; it does not touch the running Prometheus and does not need network access. It is safe to run anywhere with read access to the configuration directory.
What the check does not do is also important:
- It does not connect to scrape targets. A typo in a hostname is valid schema.
- It does not evaluate PromQL. An expression that always returns empty is valid syntax.
- It does not check rule semantics.
promtool check rulesandpromtool test rulesare the right tools for that.
Why a sysadmin cares
The check is the cheapest gate that catches a real class of mistake. The four shapes it catches:
- Unknown keys.
scrape_interval_hours: 1hinstead ofscrape_interval: 1h. The daemon ignores unknown keys at reload time; the tool reports the unknown key and exits non-zero. - Wrong type.
evaluation_interval: thirty-secondsinstead ofevaluation_interval: 30s. The tool reports the type mismatch and exits non-zero. - Missing required key. A scrape job without
static_configsor any other discovery block. The tool reports the missing key and exits non-zero. - Duplicate block keys. Two
remote_write:blocks in the same file. The tool reports the duplicate and exits non-zero.
Each of these four mistakes would either fail the reload at production runtime (with the cost described in the opening) or silently no-op (with the cost of a scrape that never happens). The check catches them in 200 milliseconds on the operator’s workstation, before any of those costs are paid.
How it works
The mental model:
promtool check config /etc/prometheus/prometheus.yml
|
v
YAML parser reads the file
|
v
Resolve rule_files globs (each matched file is parsed)
|
v
Validate every block against the daemon's compile-time types
|
v
On any failure: print file:line and error, exit 1
On success: print SUCCESS and exit 0
The schema the tool validates is the schema Prometheus itself
parses. There is no separate validation rule set. This is the
key property: if promtool check config accepts a file,
Prometheus will accept it at reload.
How to configure it
The check is a command, not a configuration file. The two common ways to wire it:
Locally, as a pre-commit check:
#!/usr/bin/env bash
# .git/hooks/pre-commit
# Reject the commit if promtool rejects the configuration.
set -e
promtool check config observability/prometheus/prometheus.yml
In a GitHub Actions workflow:
# .github/workflows/promtool-check.yml
name: promtool-check-config
on:
pull_request:
paths:
- 'observability/prometheus/**'
- '.github/workflows/promtool-check.yml'
permissions:
contents: read
jobs:
check:
name: promtool check config
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: promtool check config
uses: prometheus/promtool-github-action@v0.1.0
with:
arguments: >-
check config
observability/prometheus/prometheus.yml
--lint-fatal
Three things to notice:
--lint-fatalis passed. Lint findings (duplicate rule names, glob that matches no files) become errors and fail the check, instead of printing politely and exiting 0.- Path trigger. The job runs only when files under
observability/prometheus/**change. Saving CI minutes on unrelated PRs. - Official action.
prometheus/promtool-github-actioninstalls a pinned promtool and runs the named command. The image’s promtool version is fixed by the action’s release; pin the action to the release that matches production.
For teams that prefer to install promtool explicitly:
- uses: actions/checkout@v4
- name: Install promtool
run: |
PROMTOOL_VERSION=2.55.1
curl -sSL \
"https://github.com/prometheus/prometheus/releases/download/v${PROMTOOL_VERSION}/prometheus-${PROMTOOL_VERSION}.linux-amd64.tar.gz" \
| tar xz -C /tmp
sudo mv \
/tmp/prometheus-${PROMTOOL_VERSION}.linux-amd64/promtool \
/usr/local/bin/
- name: promtool check config
run: |
promtool check config \
observability/prometheus/prometheus.yml \
--lint-fatal
The explicit install lets the team pin the exact promtool version independently of the action’s release cycle.
How to validate it
Three checks confirm the gate is wired correctly.
1. The check accepts a known-good configuration.
promtool check config observability/prometheus/prometheus.yml --lint-fatal
Expected output, exit 0:
SUCCESS: observability/prometheus/prometheus.yml is valid prometheus config
If the file matches a rule_files glob, the tool also parses
each matched file and reports the rule count:
SUCCESS: observability/prometheus/prometheus.yml is valid prometheus config
observability/prometheus/rules/checkout.yml: 6 rules, 2 alerts
observability/prometheus/rules/platform.yml: 4 rules, 0 alerts
The second line is per-rule-file detail; the first line is the overall verdict. Both must say SUCCESS for the check to pass.
2. The check rejects a known-bad configuration.
The four shapes that should be caught:
# observability/prometheus/bad.yml
global:
scrape_interval: 1m
evaluation_interval: 30s
scrape_configs:
- job_name: 'node'
scrape_interval_hours: 1 # unknown key
static_configs:
- targets: ['localhost:9100']
promtool check config observability/prometheus/bad.yml
Expected output, exit 1:
FAILED: parsing YAML file observability/prometheus/bad.yml:
yaml: line 6: unknown keys found in job:
scrape_interval_hours
# observability/prometheus/bad2.yml
global:
scrape_interval: thirty-seconds # wrong type
Expected output, exit 1:
FAILED: parsing YAML file observability/prometheus/bad2.yml:
yaml: line 2: cannot unmarshal !!str `thirty-seconds`
into time.Duration
3. The CI job is wired correctly.
Open a draft pull request that intentionally introduces one
of the four failure shapes. Confirm the promtool-check-config
job exits non-zero. Revert the bad change; confirm the job
exits 0 and the merge is allowed.
How it can fail
Six failure modes specific to the check itself:
-
Unknown key, no error reported. Symptom: the daemon silently ignores a typo such as
scrape_interval_hours. Cause: an older promtool that does not enforce unknown-key errors. Upgrade promtool to a version that fails on unknown keys (2.5.0and later). -
--lint-fatalis not set. Symptom: a duplicaterecordname across rule files prints as a warning and the check exits 0. Cause: the CI job does not pass--lint-fatal. Add--lint-fatalto the action’s arguments. -
The
rule_filesglob matches no files. Symptom:prometheus.ymlvalidates, but Prometheus at reload time has zero rules loaded. Cause: the glob is wrong (for example*.yamlinstead of*.yml). Set--lint-fatalto make the empty-glob warning fatal, then fix the glob. -
promtool version mismatch with production. Symptom: the check passes in CI, the reload fails in production. Cause: CI uses a promtool that is more permissive than the production daemon. Pin CI to the same version as production.
-
The file references a credential that does not exist in CI. Symptom:
promtool check configfails to load a basic-auth block because the credential file path does not resolve in the CI environment. Cause: the configuration references a credential by file path, and the CI image does not have that file. Either make the credential path parameterised (an environment variable the production daemon resolves at reload time) or generate a dummy credential in the CI step. -
The check runs against the wrong file. Symptom: the CI job passes, but the deployed configuration is a different file. Cause: the
paths:trigger is too narrow (for example,observability/prometheus/prometheus.ymlonly, when the actual deployment target isobservability/prometheus/prometheus-prod.yml). Broaden the trigger or use a matrix.
How to troubleshoot it
In order:
- Read the tool’s error.
promtool check configprints the file name, line number and the specific failure. The fix is in the message. - Re-run with
--debug.--debugincreases verbosity; the tool prints every block it parsed before the error. - Confirm the file matches the daemon’s version.
promtool --version 2>&1and confirm the version is the same as the production daemon. - Confirm the
rule_filesglob expands correctly. Add--lint-fatalif missing; the empty-glob case becomes an error. - Confirm the CI image’s promtool version. The official action’s image changes between releases. Pin the action release that contains the promtool version you need.
Security implications
- The check prints the file content on error. A configuration that contains a basic-auth password or a bearer token will leak the secret into the CI log on parse error. Use a secret manager or a credential file mounted at reload time, not committed credentials.
- The check does not authenticate against scrape targets. It cannot detect a credential in the configuration that grants a broader permission than intended. Secret-scanning tools (gitleaks, trufflehog) are the right gate for committed credentials.
- The check is read-only. It does not connect to the live daemon or expose any new endpoint. It is safe to run from any workstation.
Performance implications
The check is fast. A typical prometheus.yml with twenty
rule files validates in well under a second on commodity
hardware. The cost is dominated by the YAML parse and the
rule-file globs; CI budgets the check at 200–500 ms. There
is no production cost from running the check; it is purely a
pre-merge gate.
Production guidance
- Run
promtool check config --lint-fatalon every pull request that touches the configuration. - Pin the promtool version in CI to the same version as production.
- Treat lint findings as errors. The
--lint-fatalflag is the difference between a polite warning and a failed build. - Wire branch protection to require the check job to pass before merge.
Verification
You should now be able to answer:
- What three classes of mistake does
promtool check configcatch? - Why does the check use the same code as the running daemon?
- What does
--lint-fatalchange in the output? - What is the difference between
promtool check configandpromtool check rules? - Why must the CI’s promtool version match the production daemon’s version?
Quiz
Knowledge check · 8 questions
Q1. promtool check config prometheus.yml exits non-zero when:
Q2. The flag that promotes lint findings (such as duplicate rule names) from warnings to errors is:
Q3. promtool check config can detect a typo in a scrape target hostname because the schema rejects host strings that do not resolve.
Q4. Which of these mistakes are caught by promtool check config?
Q5. Name the property of promtool that makes its check trustworthy against the running daemon.
Q6. The CI image pins promtool v2.54.0 but production Prometheus runs v2.55.1. The most likely consequence of this version drift is:
Q7. A rule_files glob matches no files in the configuration. Which flag makes this a failed check instead of a passing warning?
Q8. promtool check config should be wired into a pre-commit hook or a CI step that runs on every pull request that touches the configuration.
Passing score: 75%. Answers are checked in this browser.