# checkfleet — full documentation > checkfleet is an open, source-available CLI that runs domain-aware infrastructure health checks — TLS expiry, HTTP probes, NATS, Kafka, PostgreSQL, Consul, HAProxy and 20+ more — from a single Go binary, with text, Markdown, JSON, Slack or Prometheus output. No agents, no server. > Source: https://allan-nava.github.io/checkfleet/ · Repository: https://github.com/Allan-Nava/checkfleet > This file is the plain-text concatenation of every page of the documentation > site. The structured index lives at https://allan-nava.github.io/checkfleet/llms.txt. ================================================================================ # Home URL: https://allan-nava.github.io/checkfleet/ Summary: checkfleet is a source-available CLI that runs domain-aware infrastructure health checks — TLS expiry, HTTP, NATS, Kafka, PostgreSQL, Consul, HAProxy and more — from one static Go binary. No agents, no server. ================================================================================ Quickstart # build from source go build -o checkfleet ./cmd/checkfleet # scaffold a starter config, then edit the placeholders ./checkfleet init --modules certs,http # run every configured check ./checkfleet check all --config checkfleet.yml # just TLS expiry, as a Markdown report ./checkfleet check certs --config checkfleet.yml --output markdown Documentation Installation — install from source or grab a release binary Configuration — the checkfleet.yml reference Usage — commands, flags, output formats, exit codes Modules — what each check knows how to verify Output formats — text, markdown, JSON Desktop app — the Wails GUI over the same engine CI integration — gating a pipeline on findings Development — adding a module Philosophy Don’t rebuild Prometheus or Grafana. checkfleet fills the layer they can’t: checks that need domain knowledge (what “healthy” means for a TLS estate, a NATS cluster, an HLS stream), runnable from CI, cron, or your laptop, with reports you can paste straight into your ops docs. Exit code 0 even on WARN/BAD findings — a check that ran is a success. Gate on the output, or use --exit-on-bad for CI. Worst findings first — the thing you must look at is the first line. Fleet-aware — point the certs check at your Ansible inventory and every host becomes a target. The roadmap of upcoming modules lives in BACKLOG.md. ================================================================================ # Installation URL: https://allan-nava.github.io/checkfleet/installation/ Summary: Install checkfleet with go install, the Homebrew tap, the Docker image, or a release archive for Linux, macOS and Windows — then verify the binary. ================================================================================ With go install go install github.com/Allan-Nava/checkfleet/cmd/checkfleet@latest The binary lands in $(go env GOPATH)/bin. Make sure that directory is on your PATH. With Homebrew brew install Allan-Nava/tap/checkfleet # equivalent, if you prefer to tap first: brew tap Allan-Nava/tap && brew install checkfleet The cask ships the prebuilt release binary (macOS amd64/arm64) and strips the com.apple.quarantine attribute on install, so it runs without a Gatekeeper prompt. Every v* tag refreshes the cask automatically, and a Brew test workflow verifies the install on both Apple Silicon and Intel after each release. Homebrew 6+ may ask you to trust the tap the first time (brew trust --cask Allan-Nava/tap/checkfleet). That’s expected for any third-party tap. From a release archive Each vX.Y.Z tag publishes archives (built by goreleaser) for linux, darwin and windows on both amd64 and arm64, plus a checksums.txt. Download the one for your platform from the releases page, verify it, extract, and drop the binary on your PATH: sha256sum -c checksums.txt --ignore-missing # verify tar xzf checkfleet_*_linux_amd64.tar.gz # extract (zip on Windows) sudo mv checkfleet /usr/local/bin/ The binaries are built with CGO_ENABLED=0 and -trimpath, so they are fully static — no runtime dependencies. From source git clone https://github.com/Allan-Nava/checkfleet cd checkfleet go build -o checkfleet ./cmd/checkfleet Checking the version checkfleet version The version string is injected at build time on tagged releases; a local go build reports dev. Docker Multi-arch images (linux/amd64+arm64) are published to GHCR on every release. The default entrypoint is the Prometheus exporter: # exporter (metrics on :9876) — mount your config docker run -p 9876:9876 -v "$PWD/checkfleet.yml:/checkfleet.yml" \ ghcr.io/allan-nava/checkfleet:latest # one-shot check docker run -v "$PWD/checkfleet.yml:/checkfleet.yml" \ ghcr.io/allan-nava/checkfleet:latest check all --config /checkfleet.yml Verify a release (cosign + SBOM) Release archives ship an SBOM (*.sbom.json, syft) and the checksums.txt is signed with keyless cosign. Verify it (identity = the release workflow): cosign verify-blob \ --certificate checksums.txt.pem \ --signature checksums.txt.sig \ --certificate-identity-regexp 'https://github.com/Allan-Nava/checkfleet/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ checksums.txt Recent cosign versions print a deprecation notice for --certificate and --signature (the newer form is a --bundle). The command above still verifies correctly — the notice is about the flag shape, not the signature — and it is kept as-is because it is what works against the artifacts goreleaser publishes today. Verified against v0.142.0: Verified OK, identity release.yml@refs/tags/v0.142.0. Docker images are signed too: cosign verify ghcr.io/allan-nava/checkfleet:latest \ --certificate-identity-regexp 'https://github.com/Allan-Nava/checkfleet/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ================================================================================ # Configuration URL: https://allan-nava.github.io/checkfleet/configuration/ Summary: The complete checkfleet.yml reference — global timeouts and retries, every module's options and thresholds, stack overlays, and secrets from the environment. ================================================================================ checkfleet reads a single YAML file (default checkfleet.yml, override with --config). A checkfleet.example.yml ships with the repo — copy it and adapt. # checkfleet.yml timeout_seconds: 30 # global deadline for a whole run (default 30) checks: certs: warn_days: 30 # WARN when the cert expires within N days (default 30) crit_days: 7 # BAD when it expires within N days (default 7) port: 443 # default port for targets/inventory hosts (default 443) targets: - example.com # uses the default port - internal.example:8443 ansible_inventory: /path/to/inventory # optional: every host → target on `port` http: targets: - url: https://example.com/ expect_status: 200 # expected HTTP status (default 200) max_latency_ms: 2000 # WARN if the response is slower than this expect_body: "ok" # BAD if this substring is missing from the body Top-level keys Key Type Default Meaning timeout_seconds int 30 Per-check (and per-attempt) deadline. retries int 0 Retry a check that produced an ERROR finding (transient network/handshake), up to this many times. retry_backoff_ms int 500 (when retries>0) Base backoff between attempts; doubles each retry. max_concurrency int 0 (unbounded) Cap on how many checks run at once. Handy for large fleets so a run doesn’t open hundreds of connections at once. --max-concurrency overrides it. labels map — Global key/value labels attached to the outputs — see Global labels. module_overrides map — Per-module override of timeout_seconds/retries/retry_backoff_ms, keyed by module name. A zero field falls back to the global value. include list — Files or directories deep-merged under this config at load time — see Splitting config across files. checks map — One entry per module. A module runs only if its key is present. Per-module tuning is handy when one module is slower or flakier than the rest — give a database check a shorter deadline and a couple of retries without changing the global settings: timeout_seconds: 30 module_overrides: postgres: {timeout_seconds: 10, retries: 2} stream: {timeout_seconds: 15} A module that is not present in checks is skipped by check all, and check for it fails with modulo "" non configurato. checks.certs TLS certificate expiry. See Modules → certs. Key Type Default Meaning warn_days int 30 Days-to-expiry threshold for WARN. crit_days int 7 Days-to-expiry threshold for BAD. port int 443 Default port for targets and inventory hosts without an explicit :port. targets list — host or host:port entries. ansible_inventory string — Path to an Ansible INI inventory (file or directory). Every host becomes a target on port. Targets and inventory hosts are merged and de-duplicated. At least one of targets / ansible_inventory should be set. checks.http HTTP probes. See Modules → http. checks.http.targets is a list of: Key Type Default Meaning url string — The URL to probe. Required. expect_status int 200 Expected status code; a mismatch is BAD. max_latency_ms int — WARN if the response is slower. Omit to skip the latency check. expect_body string — BAD if this substring is absent from the body. Omit to skip. checks.nats NATS JetStream cluster health via the monitoring endpoints. See Modules → nats. Key Type Default Meaning targets list — Monitoring endpoints as host or host:port. port int 8222 Default monitoring port for targets/inventory hosts without one. scheme string http http or https for the monitoring endpoint. ansible_inventory string — Ansible INI inventory; every host becomes a monitoring target on port. expect_meta_leader string — Expected meta-leader server_name; a mismatch is WARN. expect_peers list — Expected peer server_names. Unexpected members → WARN (ghost); expected-but-absent → BAD. lag_warn int 100 Raft peer lag (entries) at/above which a peer is WARN. lag_crit int 1000 Raft peer lag (entries) at/above which a peer is BAD. checks: nats: port: 8222 expect_meta_leader: nats-gw-01 expect_peers: [nats-gw-01, nats-gw-02, nats-gw-03] lag_warn: 100 lag_crit: 1000 targets: - 10.21.10.18 - 10.11.10.18:8222 checks.haproxy HAProxy backend/server health via the CSV stats export. See Modules → haproxy. Key Type Default Meaning targets list — Stats endpoints as host or host:port. port int 8404 Default stats port for targets/inventory hosts without one. scheme string http http or https. path string /stats;csv Path of the CSV stats export. ansible_inventory string — Ansible INI inventory; every host becomes a stats target on port. session_warn_pct int 0 (off) WARN when scur/slim reaches this percent. auth_user string — HTTP basic-auth user (optional). auth_pass_env string — Env var holding the basic-auth password. Never put the password in the config. checks: haproxy: port: 8404 path: /stats;csv session_warn_pct: 80 auth_user: admin auth_pass_env: HAPROXY_STATS_PASS # export HAPROXY_STATS_PASS=... in the environment targets: - 10.15.20.106:8404 checks.stream HLS/DASH stream health from the manifest. See Modules → stream. checks.stream.targets is a list of: Key Type Default Meaning url string — Manifest URL: HLS .m3u8 (master or media) or DASH .mpd. Required. name string the URL Display label for the findings. min_variants int 0 (skip) Expected minimum ladder size (variants/representations). live bool false Expect a live stream: check live-edge freshness, WARN if it’s VOD. max_age_warn_seconds int 30 when live Live-edge age → WARN. max_age_crit_seconds int 60 when live Live-edge age → BAD. checks: stream: targets: - name: canale-live url: https://cdn.example/live/master.m3u8 live: true min_variants: 3 - name: vod-catalogo url: https://cdn.example/vod/movie/master.m3u8 min_variants: 4 checks.patroni Patroni-managed PostgreSQL cluster health via the Patroni REST API. See Modules → patroni. Key Type Default Meaning targets list — Patroni REST endpoints as host or host:port. port int 8008 Default API port for targets/inventory hosts without one. scheme string http http or https. ansible_inventory string — Ansible INI inventory; every host becomes an API target on port. lag_warn_bytes int 33554432 (32 MiB) Replica lag → WARN. lag_crit_bytes int 134217728 (128 MiB) Replica lag → BAD. checks: patroni: port: 8008 lag_warn_bytes: 33554432 lag_crit_bytes: 134217728 targets: - 10.20.30.11 - 10.20.30.12:8008 checks.consul Consul cluster health via the HTTP API. See Modules → consul. Key Type Default Meaning targets list — Consul HTTP API endpoints as host or host:port. port int 8500 Default API port for targets/inventory hosts without one. scheme string http http or https. ansible_inventory string — Ansible INI inventory; every host becomes an API target on port. expect_peers int 0 (skip) Expected raft peers; below quorum → BAD, below expected → WARN. token_env string — Env var holding the ACL token (sent as X-Consul-Token). Never inline the token. kv_keys list — KV keys that must exist; a missing key is BAD. checks: consul: port: 8500 expect_peers: 3 token_env: CONSUL_HTTP_TOKEN kv_keys: - config/checkfleet/enabled targets: - 10.20.30.11 - 10.20.30.12:8500 checks.postgres PostgreSQL health via read-only SQL. See Modules → postgres. Top-level thresholds: Key Type Default Meaning lag_warn_bytes int 33554432 (32 MiB) Replica lag → WARN. lag_crit_bytes int 134217728 (128 MiB) Replica lag → BAD. conn_warn_pct int 80 WARN when connections reach this % of max_connections. wraparound_warn_age int 1500000000 age(datfrozenxid) → WARN. wraparound_crit_age int 1900000000 age(datfrozenxid) → BAD. slot_warn_bytes int 536870912 (512 MiB) Inactive slot retained WAL → WARN. slot_crit_bytes int 2147483648 (2 GiB) Inactive slot retained WAL → BAD. checks.postgres.targets is a list of: Key Type Default Meaning dsn string — libpq DSN or URL, without the password. Required. name string the DSN Display label for the findings. password_env string — Env var holding the password. Never inline it. checks: postgres: conn_warn_pct: 80 targets: - name: pg-prod-primary dsn: "host=10.20.30.11 port=5432 user=monitor dbname=postgres sslmode=require" password_env: PG_PROD_PASS The monitoring role needs only read access (it queries pg_stat_*, pg_database, pg_replication_slots, pg_settings). checks.dns DNS resolution health. See Modules → dns. Top-level keys: Key Type Default Meaning resolvers list system Resolvers as host or host:port (default port 53). Empty → /etc/resolv.conf. min_ttl_seconds int 0 (off) WARN when any answer’s TTL is below this. checks.dns.targets is a list of: Key Type Default Meaning name string — Domain to resolve. Required. type string A Record type: A, AAAA, CNAME, NS, TXT, SOA. expect list — Expected value set; a different answer is BAD (drift). For SOA, compared against the serial. checks: dns: resolvers: [10.20.30.53, 8.8.8.8] min_ttl_seconds: 30 targets: - {name: example.com, type: A, expect: ["203.0.113.10"]} - {name: example.com, type: SOA} Multi-stack profiles --stack overlays a per-stack file on the base config, so you can keep one set of defaults and a small override per environment. Given --config checkfleet.yml --stack prod, checkfleet loads checkfleet.yml then overlays checkfleet.prod.yml (same directory). The merge is per module: a module present in the stack file replaces the base’s module entirely (so the module gets its own defaults again); a module absent from the stack is inherited from the base. Every module can be overridden this way. timeout_seconds is overridden only if the stack sets it. --stack works with both check and serve. Compose several stacks by passing a comma-separated list — they overlay left-to-right, last wins, so you can layer environment on region on base: checkfleet check all --config checkfleet.yml --stack prod checkfleet check all --config checkfleet.yml --stack region-eu,prod # prod wins checkfleet serve --config checkfleet.yml --stack region-eu,prod Each stack file resolves its own include before it overlays the base. # checkfleet.prod.yml — overrides only what differs from the base checks: certs: targets: [edge.example.com] Global labels labels: attaches key/value metadata to a run — which environment, region, cluster — and carries it into the outputs for routing and dashboards: labels: env: prod region: eu Where they surface: Output How labels appear prometheus on every series — checkfleet_finding_status{check="…",target="…",env="prod",region="eu"} (invalid label chars in a key become _). json a top-level "labels": { … } object. otlp as resource attributes on the metrics. webhook (template) available as .Labels (e.g. ``). Labels are operator metadata only — never secrets. Other formats (text, markdown, …) ignore them. Splitting config across files (include) A large fleet reads better split by team or service. include: pulls other files (or a whole directory) into one config at load time: # checkfleet.yml include: - conf.d/ # every *.yml / *.yaml in the directory, in sorted order - ../shared/dns.yml # a single file, relative to THIS file timeout_seconds: 30 checks: http: targets: [{url: https://app.example/health}] Rules: Paths are relative to the file doing the include (absolute paths work too). A directory contributes its *.yml/*.yaml entries, sorted by name — prefix them 10-, 20-, … to control order. The merge is a deep merge: two files can each add different modules under checks:, and they combine. Redefining the same module (or any scalar/list like a module’s targets:) replaces it wholesale. The including file wins over everything it includes; among includes, a later entry wins over an earlier one. Includes may nest. ${…} interpolation runs per file, and an include cycle or a missing file is a clear load error. include composes with --stack: the base and the stack file each resolve their own includes before the stack overlays the base. checks.redis Redis / Valkey health via INFO. See Modules → redis. Key Type Default Meaning targets list — Endpoints as host or host:port. port int 6379 Default port for targets/inventory hosts without one. tls bool false Use TLS (rediss). username string — Optional ACL username. password_env string — Env var holding the password. Never inline it. ansible_inventory string — Ansible INI inventory; every host becomes a target on port. mem_warn_pct int 80 WARN when used_memory reaches this % of maxmemory. lag_warn_bytes int 16777216 (16 MiB) Replica offset lag → WARN. lag_crit_bytes int 134217728 (128 MiB) Replica offset lag → BAD. checks: redis: port: 6379 password_env: REDIS_PASS mem_warn_pct: 80 targets: - 10.20.30.40 - 10.20.30.41:6380 checks.keycloak Keycloak health via HTTP. See Modules → keycloak. Key Type Default Meaning base_url string — Scheme + host (+ /auth prefix on old versions), no trailing slash. health_url string — Optional health endpoint (often on the management port :9000). Checked only when set. realms list — Realm names to verify via their OIDC discovery document. checks: keycloak: base_url: https://auth.example.com health_url: https://auth.example.com:9000/health/ready realms: [main, partners] checks.tcp Generic TCP reachability. See Modules → tcp. checks.tcp.targets is a list of: Key Type Default Meaning address string — host:port to connect to. Required. name string the address Display label. tls bool false TLS handshake instead of a plain connect. expect_banner string — Substring the server banner must contain. max_latency_ms int — WARN if the connect is slower. checks: tcp: targets: - {name: ssh, address: 10.20.30.9:22, expect_banner: "SSH-2.0"} - {name: rtmp, address: ingest.example.com:1935} checks.tls Deep TLS check. See Modules → tls. Key Type Default Meaning targets list — host or host:port (default 443). port int 443 Default port. warn_days int 30 Leaf expiry → WARN. crit_days int 7 Leaf expiry → BAD. ansible_inventory string — Ansible INI inventory; every host becomes a target. checks: tls: targets: [auth.example.com, api.example.com:8443] checks.ntp NTP clock offset. See Modules → ntp. Key Type Default Meaning targets list — host or host:port (default 123). port int 123 Default port. offset_warn_ms int 100 |offset| → WARN. offset_crit_ms int 1000 |offset| → BAD. checks: ntp: targets: [time.example.com, 0.pool.ntp.org] checks.rabbitmq RabbitMQ management API. See Modules → rabbitmq. Key Type Default Meaning targets list — Management API endpoints host or host:port. port int 15672 Default management port. scheme string http http or https. username string guest Basic-auth user. password_env string — Env var holding the password. Never inline. queue_warn_depth int 1000 Queue messages → WARN. queue_crit_depth int 50000 Queue messages → BAD. checks: rabbitmq: username: monitoring password_env: RABBITMQ_PASS targets: [10.20.30.60] checks.grpc gRPC health checking (TLS/h2). See Modules → grpc. checks.grpc.targets is a list of: Key Type Default Meaning address string — host:port of the gRPC TLS endpoint. Required. name string address Display label. service string — gRPC service to check; empty = whole-server. insecure_skip_verify bool false Skip TLS cert verification (internal self-signed). checks: grpc: targets: - {name: api, address: api.example.com:443, service: example.api.v1.API} checks.ldap LDAP bind + search. See Modules → ldap. checks.ldap.targets is a list of: Key Type Default Meaning url string — ldap://host:389 or ldaps://host:636. Required. name string url Display label. start_tls bool false StartTLS on a plain connection. insecure_skip_verify bool false Skip TLS cert verification. bind_dn string — Bind DN; empty = anonymous. password_env string — Env var with the bind password. Never inline. base_dn string — Search base for the sanity search. filter string (objectClass=*) Search filter. min_entries int 1 (when base_dn set) Minimum results, else BAD. checks.kafka Kafka cluster health. See Modules → kafka. Key Type Default Meaning brokers list — Seed brokers host:port. Required. tls bool false Dial over TLS. sasl_user string — SASL username (enables SASL). sasl_mechanism string plain plain, scram-sha-256, scram-sha-512. sasl_password_env string — Env var with the SASL password. Never inline. expect_brokers int 0 Fewer brokers than this → WARN. groups list — Consumer groups whose total lag to check. lag_warn int 1000 Group lag → WARN. lag_crit int 100000 Group lag → BAD. checks: kafka: brokers: [10.20.30.70:9092] expect_brokers: 3 groups: [ingest-consumers] No secrets in config Keep credentials out of checkfleet.yml — checks never log or echo secrets, and example/config files must stay clean. Dynamic values & secrets Config values support ${…} interpolation, expanded before the file is parsed: Token Expands to ${VAR} environment variable VAR (empty if unset) ${VAR:-default} VAR, or default when unset/empty ${file:/path} the trimmed contents of a file — Docker/Kubernetes secrets timeout_seconds: ${CF_TIMEOUT:-30} checks: redis: targets: ["${REDIS_HOST}:6379"] password_env: REDIS_PASSWORD # module secrets still come from env… postgres: targets: - name: primary dsn: "postgres://app:${file:/run/secrets/pg_password}@db:5432/app" A missing ${file:…} is a hard error. Use $${ for a literal ${. This keeps secrets out of checkfleet.yml while staying friendly to *_env module fields. Maintenance windows Suppress or downgrade findings during planned work so they don’t page. Each window matches by check/target glob (empty = all) and an optional from/to (RFC3339) range; the first matching, active window wins. maintenance: - check: postgres # mute (drop) all postgres findings in the range from: 2026-08-01T22:00:00Z to: 2026-08-01T23:30:00Z - target: "cdn.*" # keep visible but cap BAD/ERROR at WARN action: warn # message gets a " [maintenance]" note action: mute (default) drops the finding; action: warn caps BAD/ERROR at WARN. Applies to check (before --exit-on-bad) and to serve. Recurring windows — add daily: "HH:MM-HH:MM" (local clock, wraps past midnight) for a window that repeats every day, optionally restricted to weekdays. from/to still bound the overall validity (e.g. a nightly window only for one month). maintenance: - check: postgres # every night 01:00–03:00 local, muted daily: "01:00-03:00" - target: "cdn.*" # only on weekends, capped at WARN daily: "00:00-23:59" weekdays: [Sat, Sun] action: warn Runbooks and remediation hints A finding tells you what is wrong. runbooks: attaches what to do about it: a procedure URL and a short note, carried into the outputs so whoever is on call does not have to go and find the wiki page. Rules match like maintenance windows — check and target globs, empty meaning all — and are read in order. runbooks: - check: certs runbook: https://wiki.example.com/runbooks/tls-renewal remediation: Renew with certbot, then reload haproxy - check: postgres target: "db-*" remediation: Check replication lag before failing over - runbook: https://wiki.example.com/runbooks/oncall # catch-all The first non-empty value wins per field, so a specific rule can supply the runbook while a catch-all below it still supplies the remediation — as in the example above, where a certs finding gets both from the first rule but a redis finding gets only the catch-all URL. Hints are attached only to findings above OK: there is nothing to do about a green result, and repeating the URL on every healthy target is noise. Where they show up: Output How text an indented ↳ note — url line under the finding markdown a second line in the Detail cell of Needs attention, runbook as a link json the runbook and remediation fields, omitted when unset html a muted line under the message, runbook as a link desktop a What to do block in the finding detail drawer No secrets. This is operational text that travels into every output, including the ones that leave the host (Slack, webhooks, issue trackers). Put a URL and a sentence here — never a token, a password or an internal credential path. ================================================================================ # Usage URL: https://allan-nava.github.io/checkfleet/usage/ Summary: checkfleet commands and flags — check, init, validate, serve and report-issues — plus output formats, severity filters, target globs and the exit-code semantics used to gate CI. ================================================================================ ``` checkfleet check --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: ```bash 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 ```bash 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: ```bash 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 ```bash 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..yml` on the base config. See [Configuration → multi-stack](/checkfleet/configuration/#multi-stack-profiles). | | `--output` | `text` | Output format: `text`, `markdown`, `json`, `junit`, `prometheus`, `slack`, or `webhook`. See [Output formats](/checkfleet/output/). | | `--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 `, 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, `/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`. ```bash 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`). ```bash 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](/checkfleet/configuration/#multi-stack-profiles)). | | `--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. ```bash 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](/checkfleet/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`](#the-doctor-command) 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. ```bash 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?". ```bash 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: ```bash 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](/checkfleet/ci/) for how to gate on it. ## Explain a module `checkfleet explain ` prints what a module checks and its key thresholds; with no argument it lists all modules. ```bash checkfleet explain # list modules checkfleet explain postgres # what the postgres check verifies ``` ## Shell completion ```bash 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 ` re-runs the checks on a timer and redraws a live terminal view (text output), handy during an incident. Ctrl-C to stop. ```bash checkfleet check all --config checkfleet.yml --watch 5s ``` ## Diff vs the previous run With `--history `, `--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. ```bash 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. ```bash 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. ```bash 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 ``` ================================================================================ # Modules URL: https://allan-nava.github.io/checkfleet/modules/ Summary: Every checkfleet module and what it actually verifies — TLS certificates, HTTP, DNS, NATS, Kafka, PostgreSQL, MySQL, Redis, MongoDB, Consul, Vault, HAProxy, S3, SMTP and more. ================================================================================ Each module is a self-contained check that knows what "healthy" means for one kind of target. **{{ site.data.modules | size }} modules ship today**; the [backlog](https://github.com/Allan-Nava/checkfleet/blob/main/BACKLOG.md) tracks what's next. | Module | Checks | What it tells you | |---|---|---| {% for m in site.data.modules -%} | [`{{ m.name }}`](#{{ m.name }}) | {{ m.title }} | {{ m.summary }} | {% endfor %} > Each module also has its own page — `/modules/` — with the same detail > on one screen: [certs](modules/certs), [http](modules/http), [postgres](modules/postgres), > [kafka](modules/kafka), and so on for all 29. ## `certs` — TLS certificates {#certs} TLS certificate expiry across a fleet. - Dials each target with SNI and reads the leaf certificate's `NotAfter`. - Reports `OK`, `WARN` (expires within `warn_days`), or `BAD` (within `crit_days` or already expired). - A dial/handshake failure is `ERROR` (couldn't measure), not `BAD`. - Targets come from the explicit `targets` list **and/or** every host of an Ansible INI inventory (`ansible_inventory`). Probes run concurrently. > The dial uses `InsecureSkipVerify` **on purpose**: we want the expiry date even > when the chain doesn't validate locally. It is an expiry reader, not a chain > validator. See [Configuration → checks.certs](/checkfleet/configuration/#checkscerts). ## `http` — HTTP endpoints {#http} HTTP endpoint probes. - Checks the response status against `expect_status` (mismatch → `BAD`). - `WARN` when the response is slower than `max_latency_ms`. - `BAD` when `expect_body` is set and its substring is missing. - A network/transport error is `ERROR`. See [Configuration → checks.http](/checkfleet/configuration/#checkshttp). ## `nats` — NATS JetStream {#nats} Preflight/health of a NATS JetStream cluster, read from each node's HTTP monitoring port (`/varz` and `/jsz?meta=1`) — the read-only endpoints only, it never mutates the cluster. It encodes the operational signals from the ops runbook: - **Reachability + version** per node (`OK` with `server_name`, version, conns, uptime; `ERROR` if the monitoring port doesn't answer). - **Mixed versions** across the cluster → `WARN` (e.g. mid-upgrade skew). - **Meta-leader**: `BAD` if no meta-leader is elected (quorum lost), `WARN` if the elected leader disagrees across nodes, or if it isn't the `expect_meta_leader` you configured. - **Peer health** (from the meta raft group): `BAD` if a peer is `OFFLINE`, `WARN` if `not current`. - **Peer lag**: `WARN`/`BAD` when a peer's raft lag crosses `lag_warn`/`lag_crit`. - **Ghost / missing peers**: with `expect_peers` set, an unexpected member is a `WARN` (ghost), an expected member absent from the cluster is `BAD`. See [Configuration → checks.nats](/checkfleet/configuration/#checksnats). ## `haproxy` — HAProxy {#haproxy} Backend/server health from the HAProxy **CSV stats export** over HTTP (the `;csv` stats endpoint) — read-only, it never mutates HAProxy. - Per server: `UP` → `OK`, `DOWN` → `BAD`, `MAINT`/`DRAIN`/`NOLB` → `WARN`. - Per backend (the `BACKEND` aggregate row): `DOWN` → `BAD` (no server available). - Optional session saturation: with `session_warn_pct`, a server/backend at or above that percent of its session limit (`scur/slim`) is `WARN`. - An unreachable stats page is `ERROR`. Frontends are skipped to keep the output signal-dense. Findings are labelled `backend/server` (e.g. `web/web2`). Optional HTTP basic auth is supported, with the password read from an env var — never stored in the config. See [Configuration → checks.haproxy](/checkfleet/configuration/#checkshaproxy). ## `stream` — HLS / DASH streams {#stream} HLS and DASH stream health, read from the manifest — it fetches only manifests, never media segments. - **Reachability / validity**: an unreachable manifest is `ERROR`; a manifest that doesn't parse (bad `.m3u8` / `.mpd`) is `BAD`. - **Ladder completeness**: with `min_variants`, a master playlist (HLS) or MPD (DASH) with fewer renditions is `WARN`, with none is `BAD`. - **Live-edge freshness** (when `live: true`): the age of the live edge — from HLS `#EXT-X-PROGRAM-DATE-TIME` advanced by segment durations, or DASH `publishTime` — is `WARN`/`BAD` past `max_age_warn_seconds`/`max_age_crit_seconds`. If `live` is set but the manifest is VOD (HLS `#EXT-X-ENDLIST`, or a static MPD), that's a `WARN`. - Freshness needs a timestamp in the manifest: an HLS live playlist without `#EXT-X-PROGRAM-DATE-TIME` reports `WARN` ("not measurable") rather than a false OK. For an HLS **master** playlist with `live: true`, the check fetches the highest-bandwidth variant to measure its live edge. Findings are labelled `name`, `name [ladder]`, `name [live-edge]`. Format (HLS vs DASH) is detected from the `.mpd` extension, the `dash+xml` content-type, or an `` → 404) → `BAD`. Any agent answers cluster-wide, so one endpoint is enough; extras add redundancy (an unreachable one is `ERROR`). An ACL token can be supplied via `token_env` (read from the environment, never stored in config). See [Configuration → checks.consul](/checkfleet/configuration/#checksconsul). ## `postgres` — PostgreSQL {#postgres} PostgreSQL health via **read-only SQL** (using the `pgx` driver — the module's own dependency). It never runs DDL or writes. - **Reachability**: a failed connect or query is `ERROR`; otherwise `OK` with the role (`primary`/`replica`, from `pg_is_in_recovery()`). - **Transaction wraparound**: `max(age(datfrozenxid))` past `wraparound_warn_age`/`wraparound_crit_age` → `WARN`/`BAD` (wraparound looms near ~2.1e9). - **Connection saturation**: `WARN` when active connections reach `conn_warn_pct`% of `max_connections`. - **Inactive replication slots**: an inactive slot is `WARN`; if it retains WAL past `slot_warn_bytes`/`slot_crit_bytes` → `WARN`/`BAD` (disk-fill risk). - **Replica lag** (primary only, from `pg_stat_replication`): `WARN`/`BAD` past `lag_warn_bytes`/`lag_crit_bytes`. Findings are labelled `name`, `name [wraparound]`, `name [connections]`, `name [slot:]`, `name [repl:]`. The password is read from the target's `password_env` — never stored in the config. See [Configuration → checks.postgres](/checkfleet/configuration/#checkspostgres). ## `dns` — DNS {#dns} DNS resolution health, using a small in-tree DNS client (no third-party dependency) so it can query specific resolvers and read TTLs and SOA serials. - **Resolution**: a name that no resolver answers is `ERROR`; a name that resolves to no record of the requested type is `BAD`. - **Drift**: with `expect`, an answer set different from the expected values is `BAD` (for `SOA` the serial is compared). - **Consistency across resolvers**: when resolvers return different answers — including divergent SOA serials (a propagation lag) — that's `WARN`; so is a resolver that fails to answer while others succeed. - **TTL**: with `min_ttl_seconds`, an answer TTL below the threshold is `WARN`. Supported record types: `A`, `AAAA`, `CNAME`, `NS`, `TXT`, `SOA`. Resolvers default to the system `/etc/resolv.conf` when none are configured. Findings are labelled `name/TYPE`, `name/TYPE [consistency]`, `name/TYPE [ttl]`. See [Configuration → checks.dns](/checkfleet/configuration/#checksdns). ## `redis` — Redis / Valkey {#redis} Redis / Valkey health via a minimal in-tree RESP client (no third-party dependency) reading `INFO` — read-only commands only. - **Reachability**: a failed connect/`PING`/`INFO` is `ERROR`; otherwise `OK` with version and role. `WARN` while the dataset is still `loading`. - **Memory**: with `mem_warn_pct` and a configured `maxmemory`, `used_memory` at or above that percent is `WARN`. - **Replication** (replicas): `master_link_status` not `up` → `BAD`; the master/replica offset lag past `lag_warn_bytes`/`lag_crit_bytes` → `WARN`/`BAD`. - **Persistence**: a failed last RDB bgsave, or AOF last write when AOF is enabled, → `WARN`. Findings are labelled `target`, `target [memory]`, `target [replication]`, `target [persistence]`. TLS (`rediss`) and ACL auth are supported; the password is read from `password_env`, never stored in config. See [Configuration → checks.redis](/checkfleet/configuration/#checksredis). ## `keycloak` — Keycloak {#keycloak} Keycloak health via HTTP/JSON — read-only, no admin credentials. - **Health** (when `health_url` is set): the endpoint (e.g. `/health/ready`, often on the management port) must report `status: UP` → `OK`; `DOWN` → `BAD`; unreachable → `ERROR`. - **Per realm**: the OIDC discovery document (`/realms//.well-known/openid-configuration`) must return `200` with a `token_endpoint` → `OK`. A `404`/invalid document → `BAD` (realm missing); an `issuer` that doesn't end with `/realms/` → `WARN` (usually a proxy/frontend-URL misconfiguration); unreachable → `ERROR`. Findings are labelled `health` and `realm/`. See [Configuration → checks.keycloak](/checkfleet/configuration/#checkskeycloak). ## `tcp` — TCP services {#tcp} Generic TCP reachability for anything that speaks TCP. - Connects to each `address` (optionally over TLS) and measures the connect latency. A failed connect is `ERROR`. - With `expect_banner`, the first bytes the server sends must contain the string, else `BAD` (e.g. `SSH-2.0` on port 22). - With `max_latency_ms`, a slower connect is `WARN`. See [Configuration → checks.tcp](/checkfleet/configuration/#checkstcp). ## `tls` — TLS handshakes {#tls} Deep TLS check — complements `certs` (which only reads leaf expiry). - **Chain**: verifies the presented chain against the trust store (with the hostname); invalid (untrusted, hostname mismatch) → `BAD`. - **Expiry**: leaf days-to-expiry → `OK`/`WARN`/`BAD` (`warn_days`/`crit_days`). - **Protocol**: the negotiated version; below TLS 1.2 → `WARN` (connects permissively down to TLS 1.0 just to observe and flag it). - Unreachable / handshake failure → `ERROR`. Findings are labelled `target [chain]`, `target [expiry]`, `target [protocol]`. See [Configuration → checks.tls](/checkfleet/configuration/#checkstls). ## `ntp` — NTP / clock drift {#ntp} NTP clock-offset check via a hand-rolled SNTP query (UDP, zero dependency). Clock drift silently breaks TLS validation and JWT expiry. - Estimated clock offset past `offset_warn_ms`/`offset_crit_ms` → `WARN`/`BAD`. - An unsynchronized server (stratum 0 kiss-o'-death, or ≥16) → `BAD`. - Unreachable / no reply → `ERROR`. See [Configuration → checks.ntp](/checkfleet/configuration/#checksntp). ## `rabbitmq` — RabbitMQ {#rabbitmq} RabbitMQ health via the management HTTP API — read-only. - **Reachability**: `/api/overview` (basic auth) → `OK` with version, else `ERROR`. - **Nodes** (`/api/nodes`): not running, or a memory / disk-free alarm → `BAD`. - **Queues** (`/api/queues`): depth past `queue_warn_depth`/`queue_crit_depth` → `WARN`/`BAD`; messages present with zero consumers → `WARN` (stuck backlog). Any management node answers cluster-wide, so one endpoint is enough. Findings are labelled `node/` and `queue//`. The password is read from `password_env`, never stored in config. See [Configuration → checks.rabbitmq](/checkfleet/configuration/#checksrabbitmq). ## `grpc` — gRPC services {#grpc} gRPC Health Checking Protocol (`grpc.health.v1.Health/Check`) over **HTTP/2 + TLS**, with the protobuf messages encoded by hand — no gRPC library dependency. (Plaintext h2c isn't supported; TLS endpoints only.) - `SERVING` → `OK`; `NOT_SERVING` / `SERVICE_UNKNOWN` → `BAD`; `UNKNOWN` → `WARN`. - `grpc-status 12` (UNIMPLEMENTED, no health service) → `WARN`; `5` (NOT_FOUND, unknown service) → `BAD`. - Connection/handshake failure → `ERROR`. Set `service` to check a specific gRPC service, or leave empty for whole-server health. `insecure_skip_verify` for internal self-signed endpoints. See [Configuration → checks.grpc](/checkfleet/configuration/#checksgrpc). ## `ldap` — LDAP directories {#ldap} LDAP directory health via bind + an optional sanity search (uses `go-ldap`). - Connect failure → `ERROR`; failed bind (bad credentials) → `BAD`. - With `base_dn`, a search returning fewer than `min_entries` (default 1) or an error → `BAD`. - `ldaps://` and `start_tls` supported; `insecure_skip_verify` for internal self-signed. Bind is anonymous when `bind_dn` is empty; the password comes from `password_env`, never config. See [Configuration → checks.ldap](/checkfleet/configuration/#checksldap). ## `kafka` — Apache Kafka {#kafka} Kafka cluster health via `franz-go`/`kadm` (admin metadata only). - Metadata unreachable → `ERROR`; no controller → `BAD`; fewer than `expect_brokers` → `WARN`. - Any under-replicated partition (ISR `. Optional TLS and SASL (plain/scram); the SASL password comes from `sasl_password_env`, never config. The Kafka I/O is behind an interface, so the finding logic is unit-tested with a fake — no real broker in tests. See [Configuration → checks.kafka](/checkfleet/configuration/#checkskafka). ## `ingest` — RTMP / SRT ingest {#ingest} Answers "can the streamer publish?" by speaking just enough of each protocol to prove a real server is listening, not just an open port: - **RTMP** (`protocol: rtmp`, default): the RTMP simple handshake over TCP (C0/C1 → S0/S1/S2, version checked). - **SRT** (`protocol: srt`): the SRT **induction** handshake over UDP (a best-effort reachability probe — it confirms an SRT listener answers). `OK` on a completed handshake, `WARN` over `max_latency_ms`, `ERROR` if the connection or handshake fails, `BAD` on an unknown protocol. Zero dependencies; tested against in-test fake RTMP/SRT servers. ## `s3` — S3 object storage {#s3} Checks an S3-compatible bucket (AWS S3, MinIO, Ceph): - **Bucket reachable** — `OK` on 200, `BAD` on 404 (missing) / 403 (denied). - **Sentinel object** (optional `object`) — `BAD` if missing, `WARN` if older than `max_age_warn_seconds` (a stale drop/backup), else `OK`. Requests are signed with **AWS Signature V4 written by hand** (zero deps, no AWS SDK); credentials come from `access_key_env`/`secret_key_env` (env only), or it falls back to anonymous for public buckets. `path_style: true` for MinIO/Ceph. Tested against an in-test fake S3 (`httptest`). ## `smtp` — SMTP relays {#smtp} Verifies an SMTP relay is healthy **without ever sending mail**: - **Connection & greeting** — `ERROR` if it can't connect, `BAD` on a non-`220` greeting or a greeting missing the optional `expect_banner` substring. - **EHLO** — `BAD` if `EHLO` is rejected. - **STARTTLS** (`starttls: true`) — `BAD` if not advertised or the upgrade fails. - **Implicit TLS** (`tls: true`, e.g. port 465) — wraps the connection at once. - **Relay certificate** — when TLS is negotiated, reads the leaf and reports `WARN` under `warn_days`, `BAD` under `crit_days` (or expired). - **Latency** — `WARN` over `max_latency_ms`. Default port is `25`, or `465` when `tls: true`. Zero-dep (stdlib `net`/`crypto/tls`); tested against in-test fake relays (plain, STARTTLS, implicit TLS). ```yaml smtp: warn_days: 30 crit_days: 7 targets: - {name: relay, address: mail.example.com:25, starttls: true} - {name: smtps, address: mail.example.com:465, tls: true} ``` ## `elasticsearch` — Elasticsearch / OpenSearch {#elasticsearch} Checks an Elasticsearch or OpenSearch cluster over its HTTP API (any node answers cluster-wide): - **Cluster health** (`/_cluster/health`) — `green` → `OK`, `yellow` → `WARN`, `red` → `BAD`; the message carries node count, unassigned shards and active shard percentage. - **Expected nodes** (`expect_nodes`) — `BAD` if the cluster reports fewer nodes than expected (a shrunk cluster, even when still green). - **Disk watermark** (`/_cat/allocation`) — per node, `WARN` over `disk_warn_pct` (default 85, ES low watermark), `BAD` over `disk_crit_pct` (default 90, high watermark). - `ERROR` if the API is unreachable. Credentials come from env — `username` + `password_env` (basic auth) or `api_key_env` (API key) — never inline; `insecure_skip_verify` for self-signed clusters. Zero-dep (HTTP/JSON); tested against an in-test fake cluster. ```yaml elasticsearch: disk_warn_pct: 85 disk_crit_pct: 90 targets: - {name: logs, url: https://es.example.com:9200, username: elastic, password_env: ES_PASSWORD, expect_nodes: 3} ``` ## `mongodb` — MongoDB {#mongodb} Read-only health check for MongoDB via the **official driver** (any member answers cluster-wide): - **Replica-set status** (`replSetGetStatus`) — `BAD` if there is no healthy `PRIMARY`, `BAD` for any member with health 0 (unreachable), and per-secondary replication lag: `WARN` over `lag_warn_seconds`, `BAD` over `lag_crit_seconds`. A standalone node (not a replica set) is reported as reachable and the replica-set checks are skipped. - **Connections** (`serverStatus`) — `WARN` when in-use connections exceed `conn_warn_pct` of the total available. - `ERROR` if the deployment is unreachable or the status query fails. Credentials come from env (`username` + `password_env`, never in the URI/config); `auth_source` defaults to `admin`. Uses `go.mongodb.org/mongo-driver/v2` (a motivated exception to the zero-dep rule, like `pgx` for Postgres). The finding logic is unit-tested with a fake collector — no real database in tests. ```yaml mongodb: lag_warn_seconds: 10 lag_crit_seconds: 60 targets: - {name: rs0, uri: "mongodb://m1:27017,m2:27017/?replicaSet=rs0", username: monitor, password_env: MONGO_PASSWORD} ``` ## `mysql` — MySQL / MariaDB {#mysql} Read-only health check via the standard `go-sql-driver/mysql` (a motivated exception to the zero-dep rule, like `pgx` for Postgres): - **Reachable & role** — `OK` with the server version; reports whether the server is read-only. - **Connections** — `WARN` when `Threads_connected` exceeds `conn_warn_pct` of `max_connections`. - **Replication** (only on a replica) — `BAD` if the IO or SQL thread is not running or the replica is not replicating (`Seconds_Behind` is NULL); otherwise lag `WARN`/`BAD` over `lag_warn_seconds`/`lag_crit_seconds`. Works with both the modern `SHOW REPLICA STATUS` and the legacy `SHOW SLAVE STATUS` column names. - `ERROR` if the server is unreachable or a query fails. Put the password in the DSN via `${ENV}` interpolation — it is resolved from the environment at config load, never stored inline. The finding logic is unit-tested with a fake collector; no real database in tests. ```yaml mysql: lag_warn_seconds: 10 lag_crit_seconds: 60 targets: - {name: primary, dsn: "monitor:${MYSQL_PASSWORD}@tcp(db-01:3306)/"} ``` ## `etcd` — etcd {#etcd} Checks an etcd v3 cluster over its **HTTP JSON gateway** — no `clientv3` dependency: - **Health** (`/health`) — `ERROR` if unreachable, `BAD` if the endpoint reports unhealthy. - **Leader** (`/v3/maintenance/status`) — `BAD` if there is no leader (the cluster has lost quorum); otherwise reports the etcd version. - **Members** (`/v3/cluster/member/list`) — `BAD` if fewer members than `expect_members` (quorum risk), else the count is shown in the healthy message. Optional token auth (`username` + `password_env`, resolved from the environment) and `insecure_skip_verify` for self-signed clusters. Zero-dep (HTTP/JSON); tested against an in-test fake gateway. ```yaml etcd: expect_members: 3 targets: - {name: etcd-01, url: https://etcd-01:2379, insecure_skip_verify: true} ``` ## `clickhouse` — ClickHouse {#clickhouse} Checks a ClickHouse server over its HTTP interface (zero-dep): - **Reachability** — `/ping` must return `Ok.` (`ERROR` if unreachable, `BAD` otherwise) and `SELECT version()` must answer (`ERROR` on failure); the healthy message carries the server version. - **Replicated tables** (`system.replicas`) — per table, `BAD` if the replica is read-only (usually a lost ZooKeeper/Keeper session), else replication delay `WARN`/`BAD` over `delay_warn_seconds`/`delay_crit_seconds`. Healthy tables produce no finding; a server with no replicated tables adds nothing. Credentials come from env (`username` + `password_env`, sent as HTTP basic auth), never inline; `insecure_skip_verify` for self-signed HTTPS. Tested against an in-test fake HTTP server. ```yaml clickhouse: delay_warn_seconds: 30 delay_crit_seconds: 300 targets: - {name: ch-01, url: http://ch-01:8123, username: monitor, password_env: CLICKHOUSE_PASSWORD} ``` ## `vault` — HashiCorp Vault {#vault} Checks a Vault node over its HTTP API (zero-dep): - **Seal status** (`/v1/sys/seal-status`) — `ERROR` if unreachable, `BAD` if the node is sealed (with unseal progress `n/threshold`) or not initialized. - **Role** (`/v1/sys/health`) — reports `active` or `standby` (both `OK` — standby is normal in an HA cluster) with the Vault version. Both endpoints are unauthenticated; an optional `token_env` sends `X-Vault-Token` for setups that restrict them. `insecure_skip_verify` for self-signed HTTPS. Tested against an in-test fake Vault. ```yaml vault: targets: - {name: vault-01, url: https://vault-01:8200} ``` ## `memcached` — memcached {#memcached} Checks memcached over its text protocol (zero-dep): - **Reachability** — connects and runs `STATS`; `ERROR` if it can't. - **Memory** — `WARN` when `bytes` exceeds `mem_warn_pct` of `limit_maxbytes` (default 90); otherwise `OK` with version, memory percentage and connection count. The percentage is also the target's numeric metric (`%`). - **Evictions** — a `[evictions]` finding carrying the counter as a metric. memcached only exposes a **total since startup**, not a rate, so there is no useful default threshold: the count is published so the history can chart it (a rising line is the real signal), and it only `WARN`s when you set an explicit `evictions_warn`. Omitted when the server doesn't report the stat. Targets are `host[:port]` (default port `11211`). Tested against an in-test fake memcached. ```yaml memcached: mem_warn_pct: 90 evictions_warn: 0 # 0 = report only, no threshold targets: [cache-01, cache-02:11212] ``` ## `cassandra` — Cassandra / ScyllaDB {#cassandra} Reachability check that speaks the **CQL native protocol** handshake directly — no driver, no authentication: - Connects and performs `OPTIONS` → `SUPPORTED` (negotiating the CQL version), then `STARTUP`. A `READY` or `AUTHENTICATE` reply means the node accepts CQL connections → `OK` (with `(auth required)` noted when authentication is on). - `WARN` when the handshake is slower than `max_latency_ms`. Handshake latency is also the node's numeric metric (`ms`). - `BAD` on a protocol `ERROR` reply, `ERROR` if the node is unreachable. - **Cluster state** — a `cluster` finding rolls the nodes up: how many accept CQL out of those configured, as a metric (`nodes`). `BAD` below `expect_nodes` (0 = expect them all), `WARN` when the expectation is met but a configured node is still down. A slow node (`WARN`) still counts as up — it completed the handshake. Skipped for a single target with no `expect_nodes`, where it would only repeat the node's own finding. This is derived from checkfleet's own probes, not from `system.peers`: reading the cluster's view of its membership needs a `QUERY` on an authenticated session, and this module speaks the handshake only. The trade-off is deliberate — it says nothing about nodes absent from your config, but it keeps working on clusters with authentication enabled, with no driver and no credentials. Same shape as `etcd`'s `expect_members`. Targets are `host[:port]` (default CQL port `9042`). Zero-dep; tested against an in-test fake CQL server. ```yaml cassandra: expect_nodes: 3 # 0 = all configured nodes must accept CQL targets: - {name: cass-01, address: cass-01:9042, max_latency_ms: 500} ``` ## Ansible inventory as a target source The `certs`, `nats`, `haproxy`, `patroni`, `consul`, `redis` and `tls` modules can read a standard Ansible **INI** inventory (a file or a directory of files): - host lines and their `ansible_host=` value are used; - `:vars` and `:children` sections are ignored; - hosts are de-duplicated. Every discovered host becomes a target on the module's `port` (443 for `certs`, 8222 for `nats`, 8404 for `haproxy`, 8008 for `patroni`, 8500 for `consul`, 6379 for `redis`, 443 for `tls`). ================================================================================ # Output formats URL: https://allan-nava.github.io/checkfleet/output/ Summary: How checkfleet renders findings — terminal text, an ops-style Markdown report you can paste into a runbook, JSON with a `worst` field for gating, and Slack Block Kit. ================================================================================ Pick one with `--output`. Every format renders the same findings, sorted worst-first. ## Fan out to several sinks `--output` takes a **comma-separated list**, so one run emits to several sinks at once — the checks run only once: ```bash # print JSON locally and push the report to Slack checkfleet check all --config checkfleet.yml --output json,slack # a file format plus two chat sinks checkfleet check all --config checkfleet.yml --output markdown,slack,teams --out-file report.md ``` With multiple sinks each one is **isolated**: an unset env var or a down webhook is reported on stderr but doesn't stop the other sinks or fail the run (the [finding gate](/checkfleet/ci/) is separate). With a single `--output`, a sink error still aborts the command as before. `--out-file` applies to the format renderer; combining several *file* formats in one run isn't meaningful (the last wins). ## `text` (default) For the terminal. One line per finding with a colored status glyph, then a summary line. ``` 🔴 BAD http https://example.com/health HTTP 404 (want 200), 151ms 🟢 OK certs example.com:443 expires in 41 days (2026-09-02, CN=*.example.com) 2 checks: 1 OK, 0 WARN, 1 BAD, 0 ERROR (in 227ms) ``` The finding order is a de-facto API and applies to **every** output format: findings are sorted **worst-first**, then by check, then by target, with a **stable** sort (equal keys keep their original order). Exact-duplicate findings (same check, target, status and message) are **de-duplicated**. Tools that parse the output can rely on this ordering. ## `markdown` An ops-style report you can paste into an incident doc or a PR: a summary, a "Needs attention" section for the non-OK findings, and a full table. ```bash checkfleet check all --config checkfleet.yml --output markdown > report.md ``` ## `json` Machine-readable. Includes a top-level `worst` field with the worst status in the run — the field to gate on in a pipeline. ```bash checkfleet check all --config checkfleet.yml --output json | jq '.worst' ``` ```json { "schema": 1, "worst": "BAD", "findings": [ { "check": "http", "target": "https://example.com/health", "status": "BAD", "message": "HTTP 404 (want 200), 151ms" } ] } ``` The `schema` field is the version of the document itself, so a consumer can tell a format change from a content change. The keys that are safe to depend on — and the fact that `message` is *not* one of them, because its wording improves between releases — are listed in [Compatibility](/checkfleet/compatibility/). See [CI integration](/checkfleet/ci/) for using `worst` or `--exit-on-bad` to fail a build. ## `junit` JUnit XML — one `` per finding, a `` for BAD, an `` for ERROR, WARN kept passing (with a `` note). Feed it to a CI test tab (TeamCity, GitHub Actions test reporters). ```bash checkfleet check all --config checkfleet.yml --output junit > report.xml ``` ## `html` A self-contained static HTML report (styles inlined, no external resources), themed like this site: the worst-status pill, per-status count tiles, a "Needs attention" section, and the full table. Nice to publish as a CI artifact or attach to an incident. ```bash checkfleet check all --config checkfleet.yml --output html --out-file report.html ``` ## `github` For GitHub Actions. Emits the findings as **workflow commands** on stdout, so they become inline annotations on the run and on the PR, and appends the full Markdown report to the job summary (`$GITHUB_STEP_SUMMARY`) when that variable is set. ```bash checkfleet check all --config checkfleet.yml --output github --exit-on bad ``` | Finding | Annotation | |---|---| | BAD, ERROR | `::error` | | WARN | `::warning` | | OK | *(none)* | OK findings are skipped deliberately: GitHub shows at most 10 annotations per level per step, so annotating green targets would hide the real ones. The summary still lists everything. Because the sink writes the summary file itself, you never pipe its output — which is what makes the CI gate reliable, see [CI integration](/checkfleet/ci/#why-not-just-pipe-into-github_step_summary). Outside Actions (no `$GITHUB_STEP_SUMMARY`) it just prints the annotations, which is handy for eyeballing the format locally. ## `sarif` [SARIF 2.1.0](https://sarifweb.azurewebsites.net/), the interchange format for static-analysis results. Upload it and the findings land in GitHub's **Code scanning / Security** tab (and in any other SARIF-aware tool) — see [CI integration](/checkfleet/ci/#code-scanning-sarif). ```bash checkfleet check all --config checkfleet.yml --output sarif --out-file checkfleet.sarif ``` | Finding | SARIF level | |---|---| | BAD, ERROR | `error` | | WARN | `warning` | | OK | `none` | Each **module** becomes a rule (`checkfleet/certs`, `checkfleet/http`, …) with its description taken from `checkfleet explain`; each **finding** becomes a result. Three details that matter when reading the output: - **BAD and ERROR share the level `error`** because SARIF has no third failure level. The engine's own status survives in `properties.status`, so a consumer can still tell "the target is unhealthy" from "the check could not measure". - **Results are anchored to the config file**, line 1. SARIF is file-oriented and a checkfleet finding is about a network target, so there is no source line to blame; the config is the file that makes the target be checked at all. The real subject is in the message and in `properties.target`. Pass a repo-relative `--config` so the alerts attach to the file in the repo. - **Fingerprints ignore severity** (`partialFingerprints` is built from check + target). A certificate going WARN → BAD stays *the same alert getting worse*, instead of appearing as a new one. ## `prometheus` The Prometheus text-exposition format (same metrics as `serve`), for a one-shot run instead of a scrape. With `--out-file` it's written atomically (temp + rename), so it's safe to drop into the node_exporter **textfile collector**: ```bash checkfleet check all --config checkfleet.yml --output prometheus \ --out-file /var/lib/node_exporter/textfile/checkfleet.prom ``` `--out-file` works for any printable format (writes to the file instead of stdout). ## `csv` Emits CSV with a header row — `status,check,target,message` — worst first, one finding per row. Fields are quoted/escaped (commas and newlines in messages are safe), for spreadsheets or ingestion into another system. ```bash checkfleet check all --config checkfleet.yml --output csv --out-file findings.csv ``` ## `slack` Posts a [Block Kit](https://api.slack.com/block-kit) message to a Slack incoming webhook instead of printing: a header, the summary line, then the non-OK findings (worst first, capped). The webhook URL is read from an environment variable — never passed on the command line or stored in config. ```bash export SLACK_WEBHOOK="https://hooks.slack.com/services/…" checkfleet check all --config checkfleet.yml --output slack # or point at a different env var: checkfleet check all --config checkfleet.yml --output slack --webhook-env SLACK_WEBHOOK_OPS ``` If the env var is empty the command errors (nothing is sent). A run that posts successfully prints `report sent to Slack`. ## `discord` / `teams` Post to a **Discord** webhook (a rich embed) or a **Microsoft Teams** incoming webhook (a MessageCard) — the summary plus the non-OK findings (worst first, capped), colored by the worst status. Same model as `slack`: the URL comes from `--webhook-env`, never the command line. ```bash export DISCORD_WEBHOOK="https://discord.com/api/webhooks/…" checkfleet check all --config checkfleet.yml --output discord --webhook-env DISCORD_WEBHOOK export TEAMS_WEBHOOK="https://outlook.office.com/webhook/…" checkfleet check all --config checkfleet.yml --output teams --webhook-env TEAMS_WEBHOOK ``` ## `telegram` Sends a plain-text message via the **Telegram Bot API** (`sendMessage`): the summary line then the non-OK findings (worst first, capped, within the 4096-char limit). The bot token and chat id come from the environment — never the command line — via `--telegram-token-env` (default `TELEGRAM_TOKEN`) and `--telegram-chat-env` (default `TELEGRAM_CHAT_ID`). ```bash export TELEGRAM_TOKEN="123456:ABC-DEF…" export TELEGRAM_CHAT_ID="-1001234567890" checkfleet check all --config checkfleet.yml --output telegram ``` ## `webhook` POSTs the JSON output to a generic webhook (URL from `--webhook-env`), for any system that ingests JSON — a Teams/Discord relay, a custom collector, etc. ```bash export MY_HOOK="https://hooks.example/checkfleet" checkfleet check all --config checkfleet.yml --output webhook --webhook-env MY_HOOK ``` Pass `--template FILE` to shape the payload with a Go [text/template](https://pkg.go.dev/text/template) instead of the default JSON. The template is executed against `{{.Title}}`, `{{.Worst}}`, `{{.Total}}`, `{{.OK}}`/`{{.WARN}}`/`{{.BAD}}`/`{{.ERROR}}`, and `{{range .Findings}}` (each with `.Check`, `.Target`, `.Status`, `.Message`). Unknown fields are an error, so typos don't ship silently. ```bash checkfleet check all --config checkfleet.yml --output webhook \ --webhook-env MY_HOOK --template payload.tmpl ``` ## `otlp` An OTLP/HTTP **metrics** request in JSON encoding — the same gauges as `prometheus` (`checkfleet.finding.status`, `.findings.total`, `.worst.status`), hand-built with **zero dependencies** (no OpenTelemetry SDK). POST it to a collector's `/v1/metrics`: ```bash checkfleet check all --config checkfleet.yml --output otlp \ | curl -s --data-binary @- -H 'content-type: application/json' \ http://otel-collector:4318/v1/metrics ``` ================================================================================ # Desktop app URL: https://allan-nava.github.io/checkfleet/desktop/ Summary: The checkfleet desktop app — a Wails GUI over the same Go engine, with a status dashboard, run history, trends, grouping and muting for macOS, Linux and Windows. ================================================================================ A small desktop GUI over the same engine as the CLI. It’s a Wails app — a single native binary (macOS .app, Linux, Windows) with the web frontend embedded — that reuses internal/engine: the checks, the worst-first sort and the findings are identical to checkfleet check all. The CLI stays the source of truth; the GUI is just another frontend. The desktop app is beta and versioned separately from the CLI. It is not covered by the CLI’s compatibility promise: its views, bindings and stored preferences can change in any release. Two concrete gaps behind that label — the macOS build is not code-signed or notarized, so Gatekeeper will warn on first launch, and there is no menu-bar/tray icon (Wails v2 has no systray; it arrives with the v3 port). Everything the CLI guarantees, the CLI guarantees — if you need stability, script the CLI. Views, command palette & shortcuts The titlebar switches between three views: Fleet — the run summary and findings table (below). Dashboard — charts over the persisted history (see Dashboard). Config — the YAML editor for the selected file (see Edit the config). The command palette (⌘K / Ctrl-K) is a searchable list of every action — Run, Go to Fleet / Dashboard / Config, Focus filter, Validate, Show trend, Export as Markdown / JSON / HTML, Toggle theme — navigable with the arrow keys and Enter. Keyboard shortcuts Key Action ⌘K / Ctrl-K Open the command palette ⌘↵ / Ctrl-↵ Run the checks 1 / 2 / 3 Switch to Fleet / Dashboard / Config / Focus the filter box r Run the checks Esc Close the palette or any open drawer Long actions (a run, a history read) show a thin progress bar under the toolbar and a spinner on Run; the outcome of an action (exported, config saved, validated) pops a non-blocking toast in the corner. Empty and error states are explicit — a first run shows a loading state, a config that can’t run shows an inline error card with Retry and Open config editor. The whole app respects prefers-reduced-motion and is keyboard-navigable (focus rings, focus-trapped dialogs, ARIA roles). The fleet view Everything is one screen, scanned top-to-bottom. Toolbar Config — the checkfleet.yml to run. Type a path or Browse… for a native file picker. Stack — pick a checkfleet..yml profile discovered next to the config (same overlay as the CLI’s --stack); (base) runs the base file. Auto + interval — start a background monitor (see below) on a timer (10s / 30s / 60s / 5m). Notify — pop a native OS notification after a manual run whose worst status is BAD/ERROR. (The background monitor has its own change-only notifications, described below.) Run — execute every configured module now. The app remembers your config path, stack, interval, Auto and Notify between launches. Summary The worst status pill (OK / WARN / BAD / ERROR) — the one thing to read first. Count tiles for OK / WARN / BAD / ERROR. Total findings, run duration, run time, and a chip per configured module. Findings table One row per finding, worst-first (same order as the CLI), with a colored status badge and the Status / Check / Target / Trend / Message columns. The Trend column draws a tiny inline sparkline of that target’s numeric metric (latency, days-to-expiry, lag…) from the history, for the checks that measure one. Filter box — live substring match over check, target and message. Severity dropdown — show all, or only ≥ WARN, ≥ BAD, ERROR. Export Export — pick a format (Markdown, JSON, HTML, JUnit, Prometheus, OTLP) and save the current run via a native save dialog. Same renderers as the CLI’s --output. Send to a chat or webhook Beyond saving a file, Send… posts the current run straight to a chat or webhook, reusing the same renderers as the CLI’s --output. Pick a target — Slack, Discord, Teams or a generic Webhook (JSON) — and click Send…; a toast reports whether it went. The destination URL is never entered in the app — it comes only from an environment variable, so no secret lives in the GUI or its settings: Target Env var Slack SLACK_WEBHOOK Discord DISCORD_WEBHOOK Teams TEAMS_WEBHOOK Webhook (generic JSON) CHECKFLEET_WEBHOOK If the target’s env var isn’t set, the toast tells you which one to set instead of sending anything. Details, validate & explain Click a finding row to open a detail drawer with the full message and a Copy button. Click a module chip in the summary to see what that check verifies (Explain), and the Validate button checks the config without running anything (the same problems as checkfleet validate). Mute a finding Some findings you already know about — a cert you’re rotating tomorrow, a node you took down on purpose. Open the finding’s detail drawer and Snooze it for 1h, 8h, 24h, or until recovery. A muted finding dims in the table with a muted chip, and the status bar shows a running “N muted” count; Hide muted in the toolbar takes them off-screen entirely. Mutes are keyed by config + check + target, so they follow the exact target across runs and don’t leak between fleets. Timed snoozes store an absolute expiry and are pruned on load, so a mute you set at 6pm for 1h is gone by 7pm even if you reopened the app in between; until recovery mutes are cleared automatically the moment the target goes green again. Everything is local (browser storage): a mute is an operator note, never a change to your YAML, and holds no secrets. Muting is more than cosmetic — it feeds the headline and the monitor. The worst-status pill is computed over un-muted findings, so a fleet whose only red items are snoozed reads as its next-worst level, not ERROR: The raw count tiles stay raw (you still see 2 ERROR), and the status bar keeps the honest “N muted” tally — nothing is hidden, it just stops shouting. The background monitor uses the same mute-aware worst: a snoozed finding won’t fire a native notification or raise the ● monitoring badge until its mute lifts. (The mute set is pushed to the Go side so the off-thread monitor honours it too.) Note a finding Muting says “ignore this for now”; a note says why. In the finding drawer, jot an owner (optional) and a line of context — “Marco — ingest pool drained for the edge migration, expected until 18:00”. The finding then carries a note chip in the table (hover for the text), and the note comes back the next time you open it. Notes share a finding’s identity with mutes (config + check + target), so a target can be both muted and annotated. Clearing both fields removes the note. Like mutes, notes are local operator context — never written to your YAML, never sent anywhere, no secrets. Report a finding to your tracker When a finding is a real problem you want tracked, open its drawer and Report issue → GitHub / GitLab. checkfleet opens an issue with a prefilled title ([checkfleet] check/target — STATUS) and body (the message and context), then toasts a link you can Open in your browser. The button only appears for BAD/ERROR findings, and only for a forge that’s configured — the repo/project and token come only from environment variables, never the UI: Forge Repo/project Token API override (optional) GitHub GITHUB_REPO (owner/repo) GITHUB_TOKEN GITHUB_API GitLab GITLAB_PROJECT (id or url-encoded path) GITLAB_TOKEN GITLAB_API This is a one-click “open an issue for this finding”; for bulk open/close reconciliation across a whole run there’s the CLI’s report-issues subcommand (which drives the gh/glab CLIs). No token ever touches the app’s storage or a config file. Action log Every workflow action — a mute, an unmute, a note, an issue opened — drops a line in the Action log (the Actions button in the toolbar). It’s the timeline of what did we actually do about the fleet, newest first, with a UTC timestamp, the target and the detail. Copy JSON or Copy Markdown puts the whole log on your clipboard — the Markdown is a ready table to paste into a handover or a postmortem — and Clear empties it. The log is local and bounded (the last 200 actions); like everything in the incident workflow it holds operator history only, never secrets (an opened issue is logged by its URL, never a token). Dashboard The Dashboard view charts the persisted history (each Run is appended next to the config), so you see how the fleet behaves over time rather than just now: Findings per run — a stacked-area timeline of the OK/WARN/BAD/ERROR counts across recent runs. Current distribution — a donut of the latest run’s status split. Worst status per run — a compact color band, oldest → newest. By module — a module × run heatmap, color-coded by each module’s worst status; click a row to drill into that module’s trend. Availability — fleet uptime (share of runs that were all-OK) over the window, how long the current status has held, and the least-available targets with an SLO meter. Metric over time — a line chart of a numeric metric (latency, days-to-expiry, replication lag…) for a chosen target; hover a point for the value. A metric-bearing finding’s detail drawer shows the same line inline. Every chart is hand-drawn inline SVG — no chart library, no CDN — and follows the light/dark theme. The Dashboard refreshes after each Run. Changes since the last run After a second run, Changes (N) opens a drawer with only what moved — new, resolved, worsened or improved findings — so you see the delta at a glance during an incident (in-session, no history file needed). Trend over time Changes is in-session only. Trend is persistent: every run is appended to a small history file next to the config (..history.jsonl), and the button opens a sparkline of the worst status per run — green/yellow/red/purple bars, oldest to newest — so you can see a fleet degrade (or recover) across restarts. Hover a bar for the timestamp and the OK/WARN/BAD/ERROR breakdown. History browser Trend shows worst-status bars; History browses the runs themselves. The button opens a drawer listing every persisted run — newest first, each with its worst-status badge, timestamp and OK/WARN/BAD/ERROR counts. Open a run to see its findings (status and numeric value — the compact history file doesn’t store messages), and Compare with previous shows exactly what changed versus the run before it (new / resolved / worsened / improved) — the same delta as Changes, but between historical runs. Workspace — many fleets at a glance A single machine often watches more than one fleet: production, staging, an edge region, a lab box — each with its own checkfleet.yml. The workspace is the left-hand panel that keeps them all in one place. Open it with the grid button in the title bar (top-right), or press Esc to close it. Every config you open or run is remembered here automatically — no separate “add” step needed — and you can also pin one explicitly with + Add config (it opens the same file picker as Browse…). The list holds up to 20 fleets, most-recently-used first, and survives restarts (stored in local browser storage, never on disk as a file). Each row shows the config’s basename, and — once evaluated — its worst status as a badge plus the OK·WARN·BAD·ERROR counts. Run all evaluates every config in the workspace in one shot (each runs independently, with its own stack), fills in the badges, and rolls the results up into the single worst-across-all badge in the panel header — so you can tell at a glance whether anything, anywhere, needs attention. Click any row to switch the main view to that fleet (it becomes the active config and its stacks reload); the active fleet is highlighted. The workspace never stores credentials or config contents — only the file paths — and evaluating a fleet here is exactly the same run as pressing Run, so --exit-on-bad semantics and thresholds are identical. Saved views You keep coming back to the same handful of lenses: prod errors only, certs about to expire, everything grouped by module. A saved view captures that whole toolbar state — the stack, the filter text, the min-severity, the Group toggle and which top-level view is open — under a name, and the Views bar under the toolbar switches between them in one click. Set the toolbar the way you want it, then + Save view and name it (reusing a name overwrites it). Each chip applies its view on click; the chip lights up when the toolbar already matches it, so you can see at a glance which lens you’re in. The ✕ on a chip deletes it. Saved views are also commands in the palette (⌘K → View: …, plus Save current view). Import / Export move the whole set as JSON: Export copies it to the clipboard, Import reads a pasted export (matching names are overwritten) — handy for sharing a team’s standard lenses or seeding a new machine. A view is pure UI state (knobs, not data): it holds no credentials and no config contents, and lives in local browser storage, so it never touches your YAML. Background monitoring Tick Auto and the app keeps watching the fleet on the chosen interval. The loop runs in Go, not a browser timer, so each pass is a real run of the same modules — findings, trend and history all keep filling in while you’re on another view. A small ● monitoring chip in the status bar shows it’s live, colored by the latest worst status. The point of a monitor is to tell you when something changes, not to nag: a native OS notification fires only when the worst status crosses a boundary — degraded (e.g. WARN → ERROR), improved, or recovered to OK. A fleet that stays BAD for an hour notifies once, not sixty times. The crossing is computed over un-muted findings (mutes are pushed to the monitor), so a problem you’ve snoozed won’t wake you. Starting the monitor on an already-broken fleet tells you straight away; starting it on a healthy one is silent until something breaks. Changing the config, stack or interval re-points the monitor automatically. A colored menu-bar / tray icon is the natural next step here; it needs a system-tray integration that Wails v2 doesn’t provide (it arrives in Wails v3), so it’s deliberately deferred rather than pulled in as a fragile dependency — tracked in the backlog. Group by module Tick Group to fold the findings table into collapsible sections, one per module, each with a rollup badge showing the module’s worst status and how many findings it has. Click a section header to collapse it — handy when a fleet has many targets and you want to scan module-by-module. The choice is remembered between restarts. Edit the config The Config tab (titlebar) opens a full-panel YAML editor on the selected checkfleet.yml: Reload — re-read the file from disk, discarding unsaved edits. Validate — check the unsaved text (YAML parse + domain rules) and list any problems inline, without saving. This runs the same validation as the CLI. Save — write the text back to the file. Once saved, run the fleet from the GUI, or point cron / checkfleet serve --interval at the same file — the config is the single source, and the app is just one way to edit and run it. Add an endpoint You don’t have to hand-write YAML. + Add endpoint opens a quick form for the common checks — http (URL + expected status), certs / tls (host:443), tcp / smtp (host:port), dns (name + record type), redis / nats (host:port), grpc (host:port + optional service) and postgres (DSN + optional password-env var). Pick a type, fill the field(s) and Add: the endpoint is merged into the YAML (existing comments and formatting are preserved), ready to review and Save. Secrets are never entered here — for postgres you give the name of the env var that holds the password, not the password itself. As you type in the editor, a live validity badge next to the path shows ✓ valid or ✕ N problems (hover for the details) — the same checks as the Validate button, run against the unsaved text, so you catch a broken edit immediately. Run it on a schedule Schedule… prints copy-paste commands to run the same config unattended — a cron line and a checkfleet serve command for the current file and interval — so the app and your automation share one source of truth: # run every 5 min: */5 * * * * checkfleet check all --config /etc/checkfleet/checkfleet.yml --exit-on-bad # or run continuously as a Prometheus exporter: checkfleet serve --config /etc/checkfleet/checkfleet.yml --interval 5m --listen :9876 Light theme The theme toggle (top-right) switches light/dark and remembers your choice. Open straight into a fleet Two environment variables let the app open on a config and run immediately — handy for an “open with” launcher or a kiosk view: CHECKFLEET_CONFIG=/etc/checkfleet.yml CHECKFLEET_AUTORUN=1 checkfleet-desktop Without them the app opens on ./checkfleet.yml (if present) and waits for you to press Run. Get it Every release attaches the desktop builds next to the CLI archives: checkfleet-desktop__darwin_universal.zip — macOS .app (Intel + Apple Silicon) checkfleet-desktop__linux_amd64.tar.gz checkfleet-desktop__windows_amd64.zip The desktop binaries are unsigned for now — on macOS, right-click → Open the first time (or clear the quarantine attribute). Build from source Requires the Wails v2 toolchain and its platform prerequisites (macOS: Xcode command-line tools; Linux: libgtk-3 + libwebkit2gtk-4.1). Node is not needed — the frontend is static. cd desktop go mod tidy wails dev # hot-reload dev app wails build -platform darwin/universal # or linux/amd64, windows/amd64 The app lives in desktop/ as a separate Go module, so the Wails toolchain never enters the CLI’s build. Insight panels With a recorded history the app surfaces the same M30 analyses the CLI prints (checkfleet insight), computed by the shared internal/insight package — no statistics run in the GUI, so a number means the same thing in both places. a Fleet health tile with the index over recent runs beside it; correlated failures above the table: click a group to filter the findings down to it (the rows are already below — expanding a copy of them is the wall of text the grouping exists to replace); a flapping badge on targets that oscillate, with the score and direction in the tooltip; What changed — the narrative digest, with a Copy button because the text is written to be forwarded; in a finding’s drawer: recovery (how long it has been down, and how long it usually takes), error budget, baseline deviation and the forecast ETA, each saying why it has nothing to show rather than going blank. Panels appear only once there is enough history to support them. ================================================================================ # CI integration URL: https://allan-nava.github.io/checkfleet/ci/ Summary: Gate a pipeline on infrastructure findings — the checkfleet GitHub Action, a GitLab CI snippet, scheduled runs, and how `--exit-on` maps findings to exit codes. ================================================================================ checkfleet is built to run in a pipeline. Because a check that ran is a success, a normal run exits 0 regardless of findings — you decide when a finding should fail the build. Gate with --exit-on Pick the severity that should break the build: checkfleet check all --config checkfleet.yml --exit-on bad --exit-on Fails on Use it when (unset) never reporting only — the run is a success because it ran warn WARN, BAD, ERROR you want the build red on the first sign of drift bad BAD, ERROR the common choice: real problems, tolerating WARN error ERROR only you only care that the checks could measure — a BAD target is someone else’s alert --exit-on-bad is still accepted as an alias of --exit-on bad. If both are given, the explicit --exit-on wins. ok is rejected: it would fail every run, including all-green ones. A custom exit code --exit-code N (1–125) changes the code a tripped gate returns, which is how you let a wrapper script tell “checkfleet found something” apart from “checkfleet itself broke”: checkfleet check all --config checkfleet.yml --exit-on bad --exit-code 42 The default stays 2. Codes outside 1–125 are rejected: 0 would make the gate a silent no-op, and 126+ collide with the shell’s own “not executable” and “killed by signal” range. Exit codes at a glance Code Meaning 0 the run completed; no gate was set, or nothing reached the threshold 2 (or --exit-code) the gate tripped — findings at or above the threshold 1 systemic failure: unreadable config, unknown module, bad flag That last row is the important distinction. A gate that trips is a result; a 1 means checkfleet could not do its job, and a pipeline that treats the two the same will one day report a healthy fleet because the config file was missing. Flag errors are caught before the run starts, so a typo in --exit-on costs you a usage error rather than a full fleet sweep. GitHub Actions Use the action: name: fleet-checks on: schedule: - cron: "0 * * * *" # hourly workflow_dispatch: jobs: checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Allan-Nava/checkfleet@v0.132.0 That is the whole job. Every input is optional and the defaults are the common case: run all modules from checkfleet.yml, annotate the run, write the job summary, fail on BAD/ERROR. Inputs Input Default   version latest release to install, e.g. 0.131.0 module all certs, http, … config checkfleet.yml path to the config stack — comma-separated profiles, e.g. region-eu,prod output github any sink list, e.g. github,slack out-file — write the rendered output to a file exit-on bad warn|bad|error; empty = report only exit-code 2 code when the gate trips baseline — baseline file (see below) fail-on-new false gate only on new/worse findings min-severity — drop findings below this severity target — glob filter on targets It exposes one output, exit-code, so a workflow can react without re-running anything: - uses: Allan-Nava/checkfleet@v0.132.0 id: fleet continue-on-error: true with: config: infra/checkfleet.yml exit-on: warn - if: steps.fleet.outputs.exit-code != '0' run: echo "findings reached the gate" Pin it to a release tag rather than a branch. A moving v1 alias tag is not published yet, so use the exact version (@v0.132.0) — the action’s own version input is what controls which checkfleet binary it installs, and that defaults to latest independently of the tag you pin the action to. The action runs on Linux and macOS runners only; on Windows it fails with an explicit message rather than something obscure. Without the action The action is a convenience over one command, so nothing is lost by not using it: - uses: actions/setup-go@v5 with: go-version: "1.25" - run: go install github.com/Allan-Nava/checkfleet/cmd/checkfleet@latest - run: checkfleet check all --config checkfleet.yml --output github --exit-on bad That single command does three things: Annotations — every WARN/BAD/ERROR finding is emitted as a workflow command (::warning / ::error), so it shows up inline on the run and on the PR. OK findings are skipped on purpose: GitHub renders at most 10 annotations per level per step, and spending that budget on green targets would push the real problems out of the view. Job summary — the full Markdown report is written to $GITHUB_STEP_SUMMARY (appended, so it coexists with other steps). The gate — --exit-on bad fails the job. Why not just pipe into $GITHUB_STEP_SUMMARY Because this is silently broken: # DON'T: the gate never fires run: checkfleet check all --config checkfleet.yml --output markdown --exit-on bad >> "$GITHUB_STEP_SUMMARY" | tee /dev/stderr In a pipeline the shell reports the last command’s status, so checkfleet’s exit code is replaced by tee’s 0 and the job stays green no matter what was found. It only works with set -o pipefail (which the run: default shell does not enable — you need an explicit shell: bash). --output github writes the summary file itself, so there is no pipe and nothing to get wrong. Fanning out github composes with the other sinks, since --output takes a list: checkfleet check all --config checkfleet.yml --output github,slack --exit-on bad Code scanning (SARIF) Upload the SARIF report and the findings become Code scanning alerts in the Security tab, with history and dismissal, instead of scrolling back through run logs: jobs: checks: runs-on: ubuntu-latest permissions: contents: read security-events: write # required to upload SARIF steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.25" - run: go install github.com/Allan-Nava/checkfleet/cmd/checkfleet@latest - name: Run checks # No gate here: let the upload happen, then gate in a later step if you # want the build red as well. run: checkfleet check all --config checkfleet.yml --output sarif --out-file checkfleet.sarif - uses: github/codeql-action/upload-sarif@v3 with: sarif_file: checkfleet.sarif Keep --config repo-relative (checkfleet.yml, not /etc/checkfleet.yml): results are anchored to that path, and GitHub attaches each alert to the file it names. Alerts are matched across runs by a fingerprint of check + target that deliberately excludes severity, so a certificate sliding from WARN to BAD updates the existing alert rather than opening a second one. Adopting on a fleet that is already broken A plain gate is unusable on a fleet with pre-existing problems: the first run is red, it stays red, and within a week someone deletes the gate. A baseline freezes the known debt so the build only fails on what appeared since. # First run: records the current state and does NOT fail. checkfleet check all --config checkfleet.yml --baseline checkfleet-baseline.json --fail-on-new # Every run after: fails only on findings the baseline never saw, or worse ones. checkfleet check all --config checkfleet.yml --baseline checkfleet-baseline.json --fail-on-new Commit the baseline file next to the config. What counts as a failure: Baseline Now Gated? BAD BAD no — known debt BAD WARN no — it improved WARN BAD yes — a regression on a known-imperfect target is still a regression (never seen) BAD yes — new OK BAD yes — new BAD (gone) no --fail-on-new implies --exit-on bad; set --exit-on explicitly to gate at a different severity. When the debt is genuinely paid down (or deliberately accepted), re-record it: checkfleet check all --config checkfleet.yml --baseline checkfleet-baseline.json --write-baseline Two deliberate constraints: --baseline on its own never loosens an existing gate — narrowing takes --fail-on-new, so adding a baseline to a pipeline can’t quietly disable its protection — and an unreadable or future-version baseline is a systemic error (exit 1), not a silently empty one that would let everything through. Gating on JSON If you’d rather branch in a script, parse the worst field: worst=$(checkfleet check all --config checkfleet.yml --output json | jq -r '.worst') case "$worst" in BAD|ERROR) echo "fleet unhealthy: $worst"; exit 1 ;; *) echo "fleet ok ($worst)" ;; esac GitLab CI No plugin needed — download the binary and run it. The JUnit sink turns the findings into the pipeline’s Tests tab, and the SARIF-equivalent for GitLab is the JSON artifact. checkfleet: stage: test image: alpine:3 variables: CHECKFLEET_VERSION: "0.131.0" before_script: - apk add --no-cache curl - curl -sSfL "https://github.com/Allan-Nava/checkfleet/releases/download/v${CHECKFLEET_VERSION}/checkfleet_${CHECKFLEET_VERSION}_linux_amd64.tar.gz" | tar -xz checkfleet script: - ./checkfleet check all --config checkfleet.yml --output junit --out-file report.xml --exit-on bad artifacts: when: always # keep the report even when the gate fails the job reports: junit: report.xml rules: - if: $CI_PIPELINE_SOURCE == "schedule" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: always matters: without it a tripped gate fails the job and discards the report that explains why. To schedule it, add a pipeline schedule in the project settings; the rules above keep the job to scheduled runs and the default branch. TeamCity A single command-line build step. Install (or download) checkfleet, run the checks, and let --exit-on bad fail the build; emit a TeamCity service message so the failure is readable in the build log. #!/usr/bin/env bash set -euo pipefail go install github.com/Allan-Nava/checkfleet/cmd/checkfleet@latest export PATH="$PATH:$(go env GOPATH)/bin" # Human report into the build log. checkfleet check all --config checkfleet.yml --output markdown # Gate the build; surface a build problem on BAD/ERROR. if ! checkfleet check all --config checkfleet.yml --exit-on bad; then echo "##teamcity[buildProblem description='checkfleet: fleet unhealthy (BAD/ERROR)']" exit 1 fi Schedule it with a TeamCity cron trigger for periodic fleet checks, or wire the serve exporter into your Prometheus and alert there instead. Cron # hourly, mail the report on BAD/ERROR only 0 * * * * checkfleet check all --config /etc/checkfleet.yml --exit-on bad --output markdown || mail -s "checkfleet: fleet unhealthy" ops@example.com ================================================================================ # Development URL: https://allan-nava.github.io/checkfleet/development/ Summary: Contribute to checkfleet — the project layout, the engine.Check contract, and how to add a new module with offline tests against local fixture servers. ================================================================================ go test ./... # unit tests + modules against local in-test servers — no network go vet ./... golangci-lint run # v2 — see below go build -o checkfleet ./cmd/checkfleet All three must be green before a change lands: they are the checks CI runs, and the linter is a hard gate there. Leaving it out of the local checklist once let 20 consecutive red runs through, which is why it is listed first here. It has to be golangci-lint v2 — .golangci.yml uses the v2 schema and a v1 binary silently fails to read it. Install the version CI pins: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 The desktop app is a separate Go module, excluded from ./...: cd desktop && go test ./... See CONTRIBUTING.md for the rules a change has to respect (offline tests, exit-code semantics, no secrets, zero-dependency default) and Compatibility for the surfaces that must not break. Layout Path Responsibility internal/engine/ The contract: Check, Finding (status OK/WARN/BAD/ERROR), Run (checks run concurrently, each with its own timeout; results flattened in check order then worst-first sorted → deterministic), Summarize/Worst, and the typed YAML config with defaults (LoadConfig). internal/output/ Renderers: Text, Markdown, JSON. internal/checks// One package per module, implementing engine.Check. internal/inventory/ Minimal Ansible INI inventory parser. cmd/checkfleet/ The CLI (stdlib flag, subcommands). Adding a module Create internal/checks// implementing engine.Check. Add its typed config to internal/engine/config.go (with defaults in LoadConfig). Wire it into cmd/checkfleet/main.go. Test it against a local fixture server — an httptest server, or a TLS cert generated on the fly with a known expiry. A test that touches the internet or real infrastructure is a bug. Integration suite (opt-in) Unit tests must stay offline (in-test servers only). To exercise the modules against real services there’s a separate, opt-in suite gated behind the integration build tag, so go test ./... never runs it. docker compose -f docker-compose.integration.yml up -d --build --wait go test -tags integration -v ./test/integration/... checkfleet check all --config checkfleet.integration.yml # end-to-end smoke docker compose -f docker-compose.integration.yml down -v docker-compose.integration.yml brings up redis, nats, consul, haproxy, postgres, patroni (+etcd), keycloak, mysql, mongodb and kafka, each published on 127.0.0.1 with a healthcheck so --wait makes readiness deterministic. Support files live in deploy/integration/ (HAProxy config, the in-compose Patroni image). This is the only place the driver adapters get covered. Most modules speak a protocol written by hand and are fully testable offline. Four are not: mysql/driver.go, postgres/pgx.go, mongodb/mongo.go and kafka/kadm.go reach the server through a vendored driver, and a unit test cannot cross that boundary without a real server — faking a wire protocol by hand would be more untested code than it covers. Offline, those files show coverage only on their error branches; Collect, Close and the whole of kadm.go are reached here or nowhere. checkfleet.integration.yml points every module at those local ports. The suite’s contract is deliberately loose: it asserts reachability — at least one non-ERROR finding per module — not exact status (that stays covered by the unit tests). NATS runs standalone, so its meta-cluster finding is BAD by design (a single node is not an HA cluster); it’s exit-neutral. CI runs all of this in .github/workflows/integration.yml, a job kept separate from the test job in ci.yml. Fuzzing the parsers The parsers that read untrusted external input are fuzzed (CF-36): parseM3U8 (HLS manifests), parseMessage (the hand-rolled DNS wire decoder), parseCSV (HAProxy stats), and the /jsz decode + meta-cluster analysis (NATS). Each has a white-box Fuzz* target in its package. go test ./internal/checks/dns -run '^$' -fuzz '^FuzzParseMessage$' -fuzztime=30s The seed corpora run as ordinary tests under go test ./..., so a known crasher can never regress. The Fuzz workflow fuzzes every target on a schedule, on demand, and on PRs that touch a parser; any crasher lands in internal/checks//testdata/fuzz/ and is uploaded as an artifact. Conventions Status ERROR means “the check could not measure” (network, handshake) — not “the target is unhealthy” (that’s BAD). The worst-first, stable finding sort is a de-facto API — don’t break it. The only dependency is gopkg.in/yaml.v3; add others only with strong justification. Todos live in BACKLOG.md with stable CF-n ids — not scattered in code comments. Backlog ↔ GitHub issues BACKLOG.md is the single source of truth, and issues are derived from it — never the other way around. internal/backlog parses the file into CF-n items (tested). cmd/backlog-sync turns each item into a GitHub issue: label backlog, grouped by milestone (the ## sections). Checking an item ([x]) closes its issue; unchecking reopens it. Matching is by the CF-n title prefix, so the sync is idempotent. The Backlog sync workflow runs it on every push to main that touches BACKLOG.md (or the tool). Run it by hand with: go run ./cmd/backlog-sync -dry-run # preview go run ./cmd/backlog-sync # apply (needs an authenticated gh) Don’t open or close backlog issues by hand — edit BACKLOG.md instead. Releasing Every vX.Y.Z tag triggers .github/workflows/release.yml, which runs goreleaser (.goreleaser.yaml): cross-platform archives (linux/darwin/windows × amd64/arm64), checksums.txt, GitHub release notes from the commit log, and a Homebrew cask. Validate the config locally without publishing: goreleaser check # lint the config goreleaser release --snapshot --clean # full build + archives into dist/, no upload Homebrew tap: enabled. On every v* tag goreleaser pushes the cask to Allan-Nava/homebrew-tap (the repo and the HOMEBREW_TAP_GITHUB_TOKEN secret are set up, skip_upload: "false"), so: brew install Allan-Nava/tap/checkfleet The cask ships the release archive’s prebuilt binary (darwin amd64/arm64) and strips the com.apple.quarantine attribute on install (the binary is unsigned). Only tags after the tap was enabled carry the cask — older releases won’t install via brew. CI quality gates The test job runs go test with a coverage profile and prints the total to the job summary (go tool cover -func=cover.out). Coverage is reported for visibility — there is no hard threshold that fails the build. Run it locally with go test -coverprofile=cover.out ./... && go tool cover -func=cover.out | tail -1 (or -html=cover.out for a browsable report). Beyond go vet + go test, CI runs a lint job: govulncheck (gate) — fails the build on vulnerabilities your code actually reaches (run with the latest stable Go, so patched stdlib issues clear automatically). Run it locally with go run golang.org/x/vuln/cmd/govulncheck@latest ./.... golangci-lint (gate) — fails the build on any lint finding. The enabled linters are pinned in .golangci.yml (errcheck, govet, ineffassign, staticcheck, unused). Run it locally with go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 run ./.... Keep the pinned version on a release built with Go >= the go.mod version: an older binary refuses to load the config (“the Go language version … is lower than the targeted Go version”). ================================================================================ # Why checkfleet URL: https://allan-nava.github.io/checkfleet/comparison/ Summary: How checkfleet compares to Prometheus, Blackbox exporter, Nagios/Icinga, Zabbix, Uptime Kuma and hand-written bash — what it replaces, what it complements, and when not to use it. ================================================================================ In one sentence: checkfleet answers domain questions about infrastructure on demand — “is the JetStream meta-leader elected?”, “does every host in this Ansible inventory have a valid certificate for the next 30 days?” — from a single binary, with no server to run and nothing installed on the targets. It is not a replacement for a metrics stack. It is the layer that a metrics stack cannot express, and it hands the results back to that stack when you want them there. The gap it fills Generic monitoring answers is it up? well. It answers is it correct? badly, because correctness is domain knowledge: Generic monitoring sees The domain question checkfleet module Port 4222 is open Is a JetStream meta-leader elected, and are peers current? nats PostgreSQL accepts connections Is a replication slot inactive and retaining WAL? Is wraparound age climbing? postgres Patroni’s HTTP port answers Is there exactly one leader, and are replicas on the leader’s timeline? patroni The manifest URL returns 200 Is the bitrate ladder complete and is the live edge fresh? stream HAProxy’s stats page loads Does any backend have zero available servers? haproxy TLS handshake succeeds Do all 300 hosts in the inventory still have 30 days of validity? certs Kafka brokers respond Are partitions under-replicated and which consumer group is lagging? kafka Encoding those questions in a metrics stack means an exporter per system, a recording rule per signal, and an alert per threshold. checkfleet encodes them once, in Go, and runs them anywhere. Compared to… Prometheus + Alertmanager Complement, not competitor. Prometheus is a time-series database with continuous scraping, retention, and alert routing. checkfleet is a one-shot, stateless interrogation. Use Prometheus for trends, SLOs and paging; use checkfleet for the domain assertions that are awkward as PromQL, and for pre-flight checks before a deploy or a maintenance window. If you want the findings in Prometheus, checkfleet serve --listen :9876 exposes them at /metrics. Scrape that and your existing alerting keeps working — you have added domain knowledge, not a parallel stack. Blackbox exporter Blackbox probes are the closest overlap, and checkfleet’s http, tcp, tls, dns and grpc modules cover similar ground. The differences: checkfleet runs without a Prometheus deployment, reads targets straight from an Ansible inventory, and — beyond those five modules — goes well past probing into cluster state (nats, patroni, consul, kafka, elasticsearch, etcd, vault). Blackbox stays the better choice if everything you need is a probe and you already run Prometheus. Nagios / Icinga The check-plugin model is the ancestor of this idea, and it works. What checkfleet changes: one static binary instead of a plugin directory and a scheduler daemon; YAML instead of per-host object definitions; no central server to keep alive; and reports designed to be pasted into a runbook or a pull request rather than rendered in a web UI. If you already operate an Icinga estate and it fits, there is no reason to move. Zabbix, Datadog, New Relic Agent-based platforms with discovery, storage, dashboards and paging. They do far more than checkfleet and cost accordingly — in licence fees, in agents on every host, or both. checkfleet has no agent, no account, and no telemetry: it runs where you run it and prints what it found. Teams typically keep the platform and add checkfleet for the domain checks and for CI gating. Uptime Kuma / Healthchecks.io Excellent at external uptime and heartbeat monitoring with a friendly UI, and they run as a service you host. checkfleet is a CLI with no persistent process, aimed at internal correctness rather than public availability — and it goes deeper than an HTTP status code. Hand-written bash and check_* scripts This is the honest baseline, and the thing checkfleet is most often replacing. A folder of curl | jq one-liners works until it needs thresholds, retries, timeouts, concurrency, consistent exit codes, JSON output, and a second maintainer. checkfleet is that folder, with tests, one release binary, and a config a colleague can read. When not to use checkfleet Being clear about this is cheaper than a disappointed evaluation: You need history, graphs, or trend analysis. checkfleet stores nothing (the desktop app keeps a local run history, but that is not a TSDB). Use Prometheus, VictoriaMetrics, or your platform. You need paging with escalation and on-call rotations. checkfleet emits findings; Alertmanager, PagerDuty or Opsgenie route them. You need sub-minute continuous evaluation. checkfleet runs when invoked. Cron every five minutes is a normal cadence; per-second scraping is not what it is for. Your estate is fully managed SaaS with no internal services. There is little domain state left for it to interrogate. You need an OSI-approved open-source licence for commercial use. checkfleet is source-available under PolyForm Noncommercial; commercial use needs a separate licence. Where it fits in a working setup Pre-deploy gate in CI — checkfleet check all --exit-on bad before a release job touches production. Weekly certificate sweep — the certs module over the Ansible inventory, --output markdown, pasted into the ops channel or an issue. Incident triage — one command that asks every domain question at once, worst finding first, instead of ten terminal tabs. Maintenance-window pre-flight and post-flight — the same command before and after, with the two reports diffed. Fed back into Prometheus — checkfleet serve so the domain signals live next to everything else you already alert on. Next: Installation · Modules · FAQ ================================================================================ # FAQ URL: https://allan-nava.github.io/checkfleet/faq/ Summary: Frequently asked questions about checkfleet — what it is, what it costs, how it differs from Prometheus, whether it needs an agent, and how it handles credentials. ================================================================================ Short answers to the questions people actually ask before adopting checkfleet. If yours isn’t here, open a discussion or issue. What is checkfleet? checkfleet is a source-available command-line tool that runs domain-aware infrastructure health checks from a single static Go binary. You describe your targets in a checkfleet.yml file and run checkfleet check all; each module knows what “healthy” means for one kind of system — TLS certificates, HTTP endpoints, NATS, Kafka, PostgreSQL, Consul, HAProxy, HLS/DASH streams and more — and reports findings as text, Markdown, JSON, a Slack message, or Prometheus metrics. How is checkfleet different from Prometheus? Prometheus collects and stores time series and alerts on them continuously; checkfleet answers a domain question on demand, in one shot, with no server and no storage. Prometheus can tell you a NATS node is up; checkfleet tells you the JetStream meta-leader is missing, two peers are lagging, and one is a ghost. They are complements, not alternatives — checkfleet serve exposes the findings as Prometheus metrics so alerting and dashboards stay where they already are. Does checkfleet need an agent or a server? No. There is nothing to install on the targets and nothing to keep running. checkfleet is one static binary that executes the checks from wherever you invoke it — your laptop, a cron job, or a CI runner — using the same protocols and read-only endpoints an operator would use by hand. Is checkfleet free? What licence does it use? checkfleet is source-available under the PolyForm Noncommercial License 1.0.0. It is free for personal, research, educational, nonprofit and government use. Commercial use requires a separate licence — see COMMERCIAL.md in the repository. Releases published before v0.50.0 remain under the MIT licence they shipped with. It is not an OSI-approved open-source licence. Can I use checkfleet at work? It is only internal, we do not resell it. If the company is for-profit, that is commercial use and needs a licence — even when checkfleet never leaves your own infrastructure and no customer ever sees it. “Commercial” in the PolyForm Noncommercial License is about the purpose the software serves, not about redistribution, so monitoring the systems that run a for-profit business is covered by it. Evaluating checkfleet before deciding is explicitly free, and so is asking whether your case qualifies. Nonprofits, charities, public research, public safety/health, environmental and government organisations are free regardless of budget. The details, and how to request a licence, are in COMMERCIAL.md. How do I install checkfleet? Run go install github.com/Allan-Nava/checkfleet/cmd/checkfleet@latest, or brew install Allan-Nava/tap/checkfleet on macOS, or download a release archive for Linux, macOS or Windows from GitHub Releases. There is also a Docker image and a composite GitHub Action. Why does checkfleet exit 0 when a check reports BAD? Because a check that ran successfully is a successful run — the exit code reports whether checkfleet worked, not whether your infrastructure is healthy. Non-zero exit codes are reserved for systemic failures such as an unreadable config or an unknown module. To fail a CI job on findings, pass --exit-on warn, --exit-on bad or --exit-on error. What is the difference between the BAD and ERROR statuses? BAD means the target is unhealthy and checkfleet measured it. ERROR means checkfleet could not measure at all — a TCP connection refused, a TLS handshake failure, a timeout. Keeping them apart stops a broken network path from being reported as a broken service. Can checkfleet monitor a whole fleet without listing every host? Yes. Point a module at an Ansible INI inventory with ansible_inventory and every host in it becomes a target, honouring ansible_host overrides. It is the fastest way to get TLS expiry coverage across an estate you already describe in Ansible. How does checkfleet handle credentials and secrets? Credentials are read from environment variables referenced by name in the config (for example token_env), never stored in the YAML file. No check ever logs a credential, and no example config, test or document in the repository contains a real secret. Which systems can checkfleet check? TLS certificates, HTTP endpoints, DNS, NTP, generic TCP services, gRPC health, LDAP, SMTP relays, S3-compatible object storage, RTMP/SRT ingest, HLS/DASH streams, NATS JetStream, Apache Kafka, RabbitMQ, Redis/Valkey, memcached, PostgreSQL, Patroni, MySQL/MariaDB, MongoDB, Cassandra/ScyllaDB, ClickHouse, Elasticsearch/OpenSearch, etcd, Consul, HashiCorp Vault, Keycloak and HAProxy. Can I use checkfleet in CI? Yes, that is a primary use case. Run it as a step with --exit-on bad to fail the pipeline on unhealthy findings, or on a schedule with --output markdown to post a report. The repository ships a composite GitHub Action and a GitLab CI snippet. Does checkfleet write to or modify the systems it checks? No. Every module uses read-only endpoints, read-only SQL, or a protocol handshake. The SMTP module never sends mail, the PostgreSQL and MySQL modules never run DDL or writes, and the cluster modules read status endpoints rather than administrative ones. Is there a GUI? Yes. A desktop application built with Wails wraps the same engine and adds a status dashboard, run history, trends, grouping and muting, for macOS, Linux and Windows. How do I add a check for something checkfleet does not support yet? Implement the engine.Check interface in a new package under internal/checks/, add its typed configuration, wire it into the CLI, and test it against a local fixture server — the test suite never touches the network or real infrastructure. The Development page walks through it. Still deciding? Read why checkfleet exists and how it compares to Prometheus, Blackbox exporter, Nagios and friends, or jump straight to Installation. ================================================================================ # Compatibility URL: https://allan-nava.github.io/checkfleet/compatibility/ Summary: What checkfleet promises not to break — the config schema, the JSON output, exit codes, finding identity, the history file and the Prometheus metric names — and the deprecation policy for changing any of them. ================================================================================ Compatibility checkfleet gets embedded in things that outlive a release: a config file in a repo, a jq expression in a pipeline, a Grafana panel, a cron job that has been green for a year. This page says exactly which of those are safe to depend on. The short version: anything on this page is stable for the whole 1.x line. If it has to change, it changes through the deprecation policy below — never silently, never in a patch. Versioning checkfleet follows semantic versioning on the surfaces listed here. Change Version bump A stable surface changes meaning or disappears major A new module, output, flag or field minor A fix that leaves every stable surface intact patch Adding a field to an output is a minor, not a major: consumers are expected to ignore fields they do not know. Removing or repurposing one is a major. Before 1.0 every release was tagged as a minor or patch regardless, because no stability was promised yet. The guarantees on this page start at 1.0.0. Getting to 1.0 The 1.0 covers the CLI — the binary, its config schema, its outputs and its exit codes. The desktop app is deliberately not in scope and stays beta with its own version. It ships through a release candidate rather than straight from a 0.x tag: v1.0.0-rc.1 first, then a stretch of real use on a real fleet, then v1.0.0. The reason is not ceremony. Everything on this page is a promise that outlives the release that makes it, and a schema nobody has run in anger is exactly the kind of thing you discover you got wrong the week after you promised not to change it. If the rc turns something up, it changes in the next rc — that is what the rc is for. What is stable 1. The config schema Every documented key in Configuration — its name, its type, its default, and what it does. A config that works on 1.0 works on every later 1.x without edits. Two properties are part of the contract, not accidents: Unknown keys do not abort the run and do not change the exit code. A config written for a newer checkfleet runs on an older one, ignoring what it does not know — that tolerance is the point, and it is why KnownFields is deliberately not enabled. But tolerated is not the same as hidden: every command that acts on a config prints a notice on stderr naming the ignored key and the closest valid name, and validate and doctor report them as problems. An ignored key means the module never runs, so the run would otherwise report a healthy fleet having checked nothing. ${VAR} interpolation (${VAR}, ${VAR:-default}, ${file:/path}, $${ for a literal) is applied to config values before parsing. 2. The JSON output --output json emits a document with a top-level schema field. These keys are stable: Key Type Meaning schema number JSON document format version (currently 1) findings array every finding, in the documented order summary object count per status worst string worst status across the findings — this is the field to gate on started string RFC 3339 start time of the run duration_ns number run duration in nanoseconds labels object global labels from the config, omitted when none are set insight object the M30 analyses, present only when check ran with --history; omitted otherwise insight is advisory, not contractual. Its sub-keys (score, digest, clusters, recovery, …) may gain fields, and analyses may be added, inside 1.x — gate on worst, never on an insight. It is documented here so its presence is predictable, not so its shape is frozen. And within a finding: Key Type Meaning check string module name target string what was checked status string OK, WARN, BAD or ERROR message string human-readable detail — not stable text, see below value number optional scalar metric, omitted when the check has none unit string unit of value (ms, s, days, bytes, …), omitted with it runbook string optional procedure URL from the runbooks: config, omitted when no rule matches remediation string optional short “what to do” note from the same rules, omitted with it runbook and remediation are attached only to findings above OK — there is nothing to do about a green result — so a consumer must treat them as absent on any finding, not only on unconfigured ones. message is stable as a field, not as text. Finding messages get clearer between releases; matching on their wording is the thing this page cannot protect. Gate on worst or on status, never on a substring of message. 3. Exit codes Code Meaning 0 the run completed — no gate was set, or nothing reached the threshold 2 (or --exit-code N) the gate tripped 1 systemic failure: unreadable config, unknown module, bad flag The load-bearing rule: findings do not fail the run by themselves. A check that ran and found a problem is a successful check — you decide with --exit-on when a finding should break the build. A 1 means checkfleet could not do its job. See CI & pipelines. Diagnostic commands (validate, doctor, targets, explain, init, completion, version) never gate: they exit 0 unless something systemic went wrong. validate is the exception that exits 1 on a config problem — that is its job. 4. Status semantics OK → WARN → BAD → ERROR, in that severity order, and ERROR means checkfleet could not measure (network, handshake, timeout) — not “the target is unhealthy”. A monitor that treats ERROR as BAD pages someone about a firewall rule as if the database were down. This distinction will not change. 5. Finding order Findings are sorted worst-first, then by check, then by target, and the sort is stable. Anyone parsing the text output depends on it, so it is part of the contract. Identical findings (same check, target and status) are deduplicated. 6. Finding identity A finding is identified by the pair check + target. This is not just cosmetic: it is the deduplication key for report-issues, for alert, for the history file and for --baseline. Renaming a module or changing how a module spells its target makes every open issue reopen and every resolved alert re-fire, so within 1.x a module keeps its name and its target format. 7. The history and baseline files --history file is append-only JSONL, one record per run, with a sv schema version on every line (currently 1). Keys are short because the file grows forever: Record Key Meaning   t run timestamp, Unix seconds   sv schema version of the record   f the run’s entries Entry c check   g target   s status   v optional numeric value   u unit of v Reading is backward and forward compatible by construction: a record written before sv existed reads as version 1, and a record stamped with a newer version is skipped and reported rather than misread — a wrong diff is worse than a loud failure. --baseline file is a JSON document with its own version field, and a baseline whose version this binary does not understand is rejected outright with the command to re-record it. 8. Prometheus metric names Metric and label names are stable — renaming one breaks a dashboard silently, because the query simply returns no data. Metric Labels Meaning checkfleet_finding_status check, target + global labels severity per finding (0=OK, 1=WARN, 2=BAD, 3=ERROR) checkfleet_findings_total status + global labels count per status checkfleet_worst_status global labels worst severity across the fleet checkfleet_run_duration_seconds global labels duration of the last run checkfleet_last_run_timestamp_seconds global labels when the last run finished checkfleet_module_findings module findings produced per module (serve) checkfleet_module_errors module ERROR findings per module (serve) The severity encoding (0/1/2/3) is part of the contract too: alert rules compare against those numbers. 9. Command and flag names Documented subcommands and flags keep working. A flag can gain a new value (as --exit-on-bad became an alias of --exit-on bad) but does not change meaning. What is not stable Depending on any of these is fine — just expect it to move in a minor release: Finding message wording. Improved freely. Never match on it. The Go packages. Everything lives under internal/, which the Go toolchain itself forbids importing. checkfleet is a tool, not a library. Text and Markdown layout. Written for humans; columns and section titles get rearranged. Use json, csv or junit for machines. The desktop app. Deliberately outside the 1.0: it carries its own version, is labelled beta, and its views, bindings and stored preferences can change in any release. It is also not code-signed on macOS yet. Promising stability for an unsigned app whose window is still being redesigned would be a promise made to be broken; the CLI is the source of truth for behaviour. See Desktop app. serve HTML pages (/healthz and /readyz bodies are stable; the human page is not). The exact text of errors and warnings on stderr. Deprecation policy When something stable has to change: The old form keeps working for the rest of the 1.x line. No exceptions, including config keys — a config that stops loading after an upgrade is the failure mode this policy exists to prevent. It warns. Using a deprecated form prints a notice on stderr naming the replacement. Warnings go to stderr, never into the rendered output, so they cannot corrupt a parsed document or a webhook payload. It is documented in the release notes for that version and marked deprecated here, with what replaces it. It is removed only in the next major, never in a minor or a patch. New behaviour that would change existing results arrives opt-in, behind a flag or a config key that defaults to today’s behaviour. Reporting a compatibility break If an upgrade broke something on this page, that is a bug, not a policy decision — open an issue with the two versions and the config or command involved. Include the output of checkfleet version. How this page stays true It is enforced by tests, not by good intentions. The keys, metric names and schema versions listed here are asserted in internal/output/contract_test.go and internal/history/contract_test.go, and those tests also check that everything the renderers emit appears on this page — so a format change cannot land without this document being updated in the same commit. ================================================================================ # Agents URL: https://allan-nava.github.io/checkfleet/agents/ Summary: Install the checkfleet agent skill and use the CLI correctly from an AI assistant — the two semantics that decide whether the output is read right, and why there is no MCP server. ================================================================================ Using checkfleet from an AI assistant checkfleet ships an agent skill: a short document that teaches an assistant what the tool does, which commands exist, and — the part that actually matters — how to read the output without drawing the wrong conclusion. Install The skill lives inside the binary, so it is always the version that matches the checkfleet you are running: checkfleet skill install # → ~/.claude/skills/checkfleet/ checkfleet skill install --dir . # → ./checkfleet/, for a project-local install checkfleet skill print # → stdout, for your own installer Install it globally, not inside a repo. checkfleet is a tool you point at your infrastructure from wherever you happen to be working; a per-repo copy goes stale the moment you upgrade the binary. Re-run checkfleet skill install after an upgrade. It overwrites, and it is idempotent. What the skill contains SKILL.md is deliberately small — under 6 KB, enforced by a test — because it is always in context and context is the scarce resource. It carries the two rules that decide whether an assistant reads results correctly: Exit code 0 does not mean healthy. A check that ran is a success even when it found something broken. Gating requires --exit-on bad. An assistant that infers health from the exit code reports the opposite of the truth. ERROR is not BAD. BAD means the target is unhealthy; ERROR means the check could not measure. Reading “the database is down” from an ERROR is a claim the data does not support — the honest reading is “we could not tell”. It also points at --output json and the worst field instead of grepping the text renderer, whose wording the compatibility contract explicitly does not freeze. Two references load on demand: references/modules.md (every module and what it detects) and references/config-schema.md (keys, types, defaults). How it stays true A skill that confidently cites a flag which no longer exists is worse than no skill at all: the assistant keeps trying it and blames the environment. Three gates keep that from happening. The references are generated from internal/registry and the config structs by go run ./cmd/gen-skill — defaults included, read by applying the real defaults rather than copied out of comments. CI regenerates them and fails if the diff is not empty, so a new module cannot land while the skill still lists the old set. A test compiles the binary and asserts that every command and flag the skill shows as runnable exists in its usage. Why not an MCP server Not now. MCP would be the right shape if checkfleet needed to hold state across calls or stream results — it does not. It is a single binary that takes a config and prints a document, which a shell tool already exposes perfectly well, and every assistant can run a shell command while MCP support varies. A server would add a process to supervise, a transport to debug and a second surface to keep compatible, in exchange for nothing the CLI does not already give. If that changes — long-running fleet state, subscriptions to status transitions — it gets reconsidered on the merits.