Usage
checkfleet check <all|certs|http|nats|haproxy|stream|patroni|consul|postgres|dns> --config checkfleet.yml [--output text|markdown|json] [--exit-on-bad]
checkfleet version
Scaffolding a config with init
Start from a ready-to-edit config instead of a blank file:
checkfleet init # a starter config (certs + http)
checkfleet init --modules certs,http,dns,tls # pick the modules you want
checkfleet init --list # list the modules init can scaffold
init writes a commented checkfleet.yml with placeholder targets that already
load and validate — edit the values, keep secrets in the environment, then run
checkfleet check all. It refuses to overwrite an existing file unless you pass
--force.
Recipes: start from a stack, not a module list
checkfleet init --recipe web # http + certs + dns
checkfleet init --recipe db # postgres + redis + tcp
checkfleet init --recipe edge # haproxy + certs + tcp + ntp
checkfleet init --recipe media # stream + ingest + http
| Recipe | Why those modules |
|---|---|
web |
is it answering, is the certificate alive, do the records still point at us |
db |
connectivity, replication lag, memory pressure |
edge |
backend health, TLS expiry, reachability and clock skew |
media |
manifest freshness, live-edge age, and the ingest endpoints behind it |
The value isn’t saving typing. It’s that someone new to this doesn’t yet know a
web tier needs dns (records drift after a migration and nothing else notices)
or that an edge tier needs ntp (clock skew breaks TLS and tokens long before
anyone suspects the clock).
From an Ansible inventory
The inventory already knows every host. Generate the targets from it instead of retyping hostnames:
checkfleet init --from-inventory hosts.ini # certs + http
checkfleet init --from-inventory hosts.ini --modules certs,http,tcp
checkfleet init --from-inventory hosts.ini --group web
Addresses come from ansible_host where the inventory sets one, otherwise from
the host name. For http/certs you may want the public DNS name instead — SNI
and the certificate CN follow the name, not the IP — which is why the generated
file says so in a comment.
Only modules whose target can be derived from a hostname alone are available
here (certs, tls, http, tcp, dns, ntp, redis, memcached,
haproxy, consul, nats). Ask for postgres and it refuses with an
explanation rather than inventing a DSN: a config that looks ready and fails on
its first run is worse than being told up front.
Output is deterministic (hosts sorted by name), so regenerating after an inventory change produces a diff of the change and nothing else.
The check command
checkfleet check all --config checkfleet.yml # every configured module
checkfleet check certs --config checkfleet.yml # a single module
checkfleet check http --config checkfleet.yml --output json # machine-readable
all runs every module present in the config. Naming a single module runs only
that one; if it isn’t configured, the command fails.
Flags
| Flag | Default | Meaning |
|---|---|---|
--config |
checkfleet.yml |
Path to the YAML config. |
--stack |
— | Overlay a per-stack profile checkfleet.<stack>.yml on the base config. See Configuration → multi-stack. |
--output |
text |
Output format: text, markdown, json, junit, prometheus, slack, or webhook. See Output formats. |
--out-file |
— | Write the output atomically to this file instead of stdout (e.g. a node_exporter .prom file). |
--no-color |
— | Disable ANSI colour in the text output. Colour is on automatically only when writing to a terminal; it is always off when piped, redirected to a file, or when NO_COLOR is set. |
--webhook-env |
SLACK_WEBHOOK |
Env var holding the webhook URL (used by --output slack and --output webhook). |
--only |
— | Show only these checks (comma-separated, e.g. --only certs,http). |
--min-severity |
— | Show only findings at or above ok|warn|bad|error. |
--target |
— | Show only targets matching this glob (e.g. --target '*.example.com'). |
--history |
— | JSONL file to append each run to; enables flap detection across runs. |
--flap-changes |
3 |
Minimum status changes in the window to flag flapping. |
--flap-window |
10 |
Number of recent runs to evaluate flapping over. |
--ping-url-env |
— | Env var with a dead-man’s-switch URL (e.g. Healthchecks.io) to ping at the end of the run. |
--exit-on-bad |
off | Exit 2 when any BAD/ERROR finding is present. For CI gates. |
With --history <file>, each run is appended to a JSONL log and any
check/target that changed status at least --flap-changes times over the last
--flap-window runs gets an extra flap WARN finding — useful to spot
unstable targets that pass/fail intermittently. Zero dependencies (plain JSONL).
Filters apply to the rendered output (and therefore to --exit-on-bad and the
JSON worst), so --min-severity bad --exit-on-bad gates only on real
problems.
With --ping-url-env, checkfleet pings a dead-man’s-switch at the end of the
run: the base URL on success, <url>/fail when the worst finding is BAD/ERROR.
Combined with cron it also catches the case where checkfleet didn’t run at all.
The serve command
Run checkfleet as a Prometheus exporter: it re-runs the configured checks on an
interval and exposes the latest findings as metrics on /metrics.
checkfleet serve --config checkfleet.yml --listen :9876 --interval 60s
| Flag | Default | Meaning |
|---|---|---|
--config |
checkfleet.yml |
Path to the YAML config. |
--listen |
:9876 |
Address to listen on. |
--interval |
60s |
How often to re-run the checks. |
--log-format |
text |
text (human) or json (structured) logs for run start/end and errors. |
Metrics exposed:
| Metric | Meaning |
|---|---|
checkfleet_finding_status{check,target} |
Severity of each finding: 0=OK, 1=WARN, 2=BAD, 3=ERROR (worst wins per check/target). |
checkfleet_findings_total{status} |
Count of findings per status. |
checkfleet_worst_status |
Worst severity across the run. |
checkfleet_run_duration_seconds |
Duration of the last run. |
checkfleet_last_run_timestamp_seconds |
Unix time of the last run. |
checkfleet_module_findings{module} |
Findings produced by each module in the last run. |
checkfleet_module_errors{module} |
ERROR findings (measurement failures) per module. |
The server also exposes /healthz (liveness — the process is up) and
/readyz (readiness — returns 503 until the first run completes, then
200), for Kubernetes/Nomad probes.
This is the bridge to Grafana/alerting: checkfleet keeps the domain logic, and Prometheus does the graphing and alerting — it doesn’t replace them.
The report-issues command
Turn BAD/ERROR findings into tracker issues: one issue per check/target, opened
when it fails and closed automatically when it recovers. Idempotent — safe
to run on a schedule. Works with GitHub (via gh) or GitLab (via glab).
checkfleet report-issues --config checkfleet.yml # GitHub (default)
checkfleet report-issues --config checkfleet.yml --forge gitlab
checkfleet report-issues --config checkfleet.yml --dry-run # preview, no changes
--forge github|gitlab picks the tracker; the matching CLI (gh/glab) must be
installed and authenticated.
| Flag | Default | Meaning |
|---|---|---|
--config |
checkfleet.yml |
Path to the YAML config. |
--stack |
— | Overlay a stack profile (see multi-stack). |
--dry-run |
off | Print what would open/close without touching any issue. |
Managed issues carry the checkfleet-finding label and a [checkfleet] check/target
title (the dedup key). Requires the matching CLI authenticated: gh for GitHub
(in CI provide GH_TOKEN) or glab for GitLab (--forge gitlab).
The validate command
Check the config without running any check — useful in CI or a pre-commit hook.
It reports missing targets/URLs/DSNs, incoherent thresholds (e.g. warn past
crit), an empty checks, and misspelled keys — with the fix, not just the
complaint.
checkfleet validate --config checkfleet.yml # exit 0 if usable
checkfleet validate --config checkfleet.yml --stack prod
checkfleet.yml: 3 problem(s):
- unknown key "postgress" at `checks` — it is ignored, so nothing you configured under it runs → did you mean "postgres"?
- unknown key "timeout_second" at top level — it is ignored, so nothing you configured under it runs → did you mean "timeout_seconds"?
- certs: warn_days (5) should be >= crit_days (30) → the warn threshold must trigger before the crit one — check the two values are not swapped
Why misspelled keys matter most
YAML unmarshalling silently drops anything it doesn’t recognise. Write
postgress: instead of postgres: and the module simply never runs — no error,
no warning, and a check all that reports a healthy fleet because it checked
nothing. validate compares the keys in your file against the ones the config
actually accepts (read off the structs, so the list can’t drift) and suggests the
nearest match by edit distance, or by prefix (elastic → elasticsearch).
When nothing is close enough, no suggestion is offered: a confidently wrong “did you mean” is worse than none.
You don’t have to remember to run validate to hear about it. Every command that
acts on a config — check, serve, report-issues, alert, targets — prints
the same notice on stderr, following include: chains and --stack overlays:
checkfleet: warning: unknown key "postgress" at `checks` — it is ignored, so nothing you configured under it runs → did you mean "postgres"?
It stays a warning, not an error: the run continues and the exit code does not change, because a config written for a newer checkfleet has to keep working on an older one. Stderr rather than stdout so the notice can never end up inside a JSON document or a webhook payload. See Compatibility for why that tolerance is a deliberate guarantee rather than an oversight.
Notes vs problems
Some findings are about this machine, not about the config:
checkfleet: checkfleet.yml is valid ✅
note: environment variable PG_PASSWORD is not set, so it expands to an empty value → export PG_PASSWORD=... before running, or write ${PG_PASSWORD:-default} to make the fallback explicit
An unset ${VAR} is reported as a note and does not fail validate,
because a laptop running a pre-commit hook legitimately doesn’t have production
secrets exported — failing there would just teach people to skip the hook. Use
doctor when the environment is the subject: it treats
the same unset variable as BAD.
Exit 1 on any real config defect; 0 when only notes remain. A config that
fails to load is reported with its raw-level problems, which usually explain it.
The doctor command
Preflight: why isn’t this working? — about your environment, not your services.
checkfleet doctor --config checkfleet.yml
checkfleet doctor --config checkfleet.yml --no-probe # config + variables only
checkfleet doctor --config checkfleet.yml --output json
⛔ ERROR network 10.20.30.11:5432 resolves but refuses TCP: connection refused
⛔ ERROR network db-old.internal:3306 does not resolve: no such host
🔴 BAD env ${PG_PASSWORD} environment variable PG_PASSWORD is not set — it expands to an empty value
🔴 BAD target redis 127.0.0.1:65999 implausible port 65999
🟡 WARN target http duplicate target https://example.com/ (×2)
🟢 OK config checkfleet.yml valid
What it reports:
| Check | Finds |
|---|---|
env |
${VAR} referenced by the config but not set (BAD), set only via a :-default (WARN), and ${file:…} secrets that can’t be read (BAD) |
config |
everything validate reports, plus a config that fails to load |
target |
addresses with no derivable host, implausible ports, duplicate targets |
network |
per host: does it resolve, and does the port accept a TCP connection (ERROR when not) |
Three things worth knowing:
An unset ${VAR} is BAD, not a warning. It expands to the empty string,
silently — the config parses, the check runs, and it fails against an empty
password with an error that blames the database. Naming the variable is the whole
reason this command exists.
It works on a config that won’t load. The variable scan reads the raw file before any parsing, and a load failure is reported as a finding rather than an abort — a broken config is exactly when you need a diagnostic.
Network problems are ERROR, not BAD, the same distinction the check modules
make: “we could not measure from here” is not “the target is unhealthy”. Probes
are deduplicated per host:port, so 40 URLs on one host is one line.
doctor exits 0 whatever it finds. It is a diagnostic, not a gate — use
check --exit-on for that.
The targets command
What is this config actually watching? targets flattens every target across
every module — the answer to “did anyone add the new database to monitoring?”.
checkfleet targets --config checkfleet.yml
checkfleet targets --config checkfleet.yml --module certs
checkfleet targets --config checkfleet.yml --output json
14 target(s) across 3 module(s)
certs (2)
github.com
api.example.com
http (11)
https://example.com/health → example.com
Coverage against an Ansible inventory
Point it at the inventory your playbooks already use and it tells you which hosts are unmonitored:
checkfleet targets --config checkfleet.yml --against hosts.ini
checkfleet targets --config checkfleet.yml --against hosts.ini --group db
coverage vs hosts.ini: 4/5 inventory host(s) covered
not monitored (1)
db-99
targeted but not in the inventory (2)
github.com
0.pool.ntp.org
The last section is not an error: external dependencies legitimately aren’t in your inventory. It’s shown because a typo in a target looks exactly the same — a host you meant to watch, silently watching nothing.
Matching is by hostname, case-insensitive, against both the inventory name and
its ansible_host (so web2 ansible_host=10.0.0.5 matches a target naming
either). One target can cover several hosts: a MongoDB replica-set URI names
every member, and all of them count as covered.
Credentials never appear in the output. Targets for postgres/mysql/mongodb are DSNs with passwords in them; only the extracted hostname is ever printed, so this is safe to pipe into a CI log or commit as a JSON artifact.
Like doctor, this is a diagnostic: it exits 0 even when hosts are uncovered.
A coverage gap is something for a human to decide about, not a build failure.
Finding statuses
| Status | Meaning |
|---|---|
OK |
Healthy. |
WARN |
A soft threshold was crossed (e.g. cert near expiry, slow response). |
BAD |
The target is unhealthy (e.g. cert expired, wrong HTTP status). |
ERROR |
The check itself could not measure — network failure, TLS handshake error. Not the same as BAD. |
Findings are always sorted worst-first, stable per check/target — the first line is the thing you must look at.
Exit codes
checkfleet distinguishes “a check found a problem” from “checkfleet itself failed”. A check that ran is a success.
| Code | When |
|---|---|
0 |
The run completed — even with WARN/BAD/ERROR findings, unless a gate was set. Diagnostic commands (targets, validate’s clean case) also exit 0. |
2 |
--exit-on warn\|bad\|error was set and a finding reached that severity. --exit-code N changes this number. |
64 |
Usage error (missing/unknown subcommand). |
1 |
Systemic error: unreadable config, unknown module, unknown output format, invalid flag. |
This semantics is intentional and stable — see CI integration for how to gate on it.
Explain a module
checkfleet explain <module> prints what a module checks and its key
thresholds; with no argument it lists all modules.
checkfleet explain # list modules
checkfleet explain postgres # what the postgres check verifies
Shell completion
checkfleet completion bash > /etc/bash_completion.d/checkfleet # bash
checkfleet completion zsh > "${fpath[1]}/_checkfleet" # zsh
checkfleet completion fish > ~/.config/fish/completions/checkfleet.fish
Completes subcommands, module names (after check/explain) and --output
formats.
Live watch
--watch <interval> re-runs the checks on a timer and redraws a live terminal
view (text output), handy during an incident. Ctrl-C to stop.
checkfleet check all --config checkfleet.yml --watch 5s
Diff vs the previous run
With --history <file>, --diff prints only what changed since the previous
recorded run — new / resolved / worsened / improved findings — instead of the
full table. Great for a cron that only reports deltas.
checkfleet check all --config checkfleet.yml --history runs.jsonl --diff
The alert command
Create/resolve on-call alerts from BAD/ERROR findings (dedup by check/target)
on PagerDuty (Events API v2) or Opsgenie. With --history it resolves
alerts that recovered since the previous run. The key is read from an env var.
checkfleet alert --config checkfleet.yml --provider pagerduty --key-env PD_ROUTING_KEY --history runs.jsonl
checkfleet alert --config checkfleet.yml --provider opsgenie --key-env OPSGENIE_KEY --dry-run
AWS SNS (--provider sns) publishes each BAD/ERROR finding to an SNS topic —
a stateless sink, so it only publishes (no resolve). Requests are signed with
AWS Signature V4 written by hand (no AWS SDK); the region is parsed from the
topic ARN and credentials come from the environment.
export AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=…
checkfleet alert --config checkfleet.yml --provider sns \
--sns-topic-arn arn:aws:sns:eu-west-1:123456789012:checkfleet-alerts