Wheelhouse docs

Alert on drift#

You will end up being told when the router stops matching the desired-state file it should match. Two mechanisms: a Prometheus metric, and an endpoint you poll.

Before you start#

The endpoint#

bash
R=https://<router>:8443
T=wh_...
curl -sk -H "Authorization: Bearer $T" "$R/api/drift"
json
{"managed":true,"file":"/config/wheelhouse/agent.yaml","mode":"stage","drifted":true,
 "ops":2,"checked_at":"2026-08-30T22:27:07Z"}
FieldMeans
managedWhether a file is set at all. false on an agent without --reconcile-file, which is the default.
fileWhich file
modestage, commit, or off
driftedWhether the last pass found a difference
opsHow many operations would close it
checked_atWhen the loop last looked

checked_at is the field to watch as closely as drifted. A loop that has stopped running reports drifted: false for ever, and a stale timestamp is the only thing that tells you.

The metrics#

wheelhouse_desired_managed
wheelhouse_desired_drift
wheelhouse_desired_pending_ops
prometheus rules
- alert: WheelhouseConfigDrift
  expr: wheelhouse_desired_drift == 1
  for: 15m
  annotations:
    summary: "a router has drifted from its desired-state file"

- alert: WheelhouseReconcileStopped
  expr: wheelhouse_desired_managed == 1 and absent_over_time(wheelhouse_desired_drift[10m])
  for: 10m

for: 15m matters in stage mode: drift is normal for as long as it takes somebody to look at the Commit Bar and commit. Alerting instantly makes the alert meaningless.

Scrape the agent has the scrape configuration.

Without Prometheus#

A scheduled job somewhere else, using the endpoint:

/usr/local/bin/wheelhouse-drift-check
#!/bin/sh
set -eu
R=${R:?}; T=${T:?}
curl -sfk -H "Authorization: Bearer $T" "$R/api/drift" |
python3 - <<'PY'
import json, sys, datetime
d = json.load(sys.stdin)
if not d.get("managed"):
    sys.exit(0)                      # not managing a file; nothing to say
stale = False
if d.get("checked_at"):
    seen = datetime.datetime.fromisoformat(d["checked_at"].replace("Z", "+00:00"))
    stale = (datetime.datetime.now(datetime.timezone.utc) - seen).total_seconds() > 600
if d.get("drifted") or stale:
    print(f"drift={d.get('drifted')} ops={d.get('ops')} checked_at={d.get('checked_at')}")
    sys.exit(1)
PY

Run it from cron on the monitoring host and let the non-zero exit be the alert.

What drift is, and what it is not#

Drift here means one thing: the configuration differs from the desired-state file. It does not mean the router is broken, and it does not cover the other kind of drift this product reports — what the kernel is doing differing from what the configuration declares. That second kind is on the pages themselves: Interfaces flags anything the configuration does not declare, Static routes flags a route declared but not installed, and QoS shows the attached policy beside the qdisc the kernel installed.

Neither of those is alertable through this endpoint. There is no metric for them.

A different check, for a migrated router#

If what you want is "does this router still match the OPNsense configuration it was built from", that is a different tool and it exits non-zero too — Check a running router against the configuration it should have.

Check it worked#

Change something on the router that the file declares, wait an interval, and confirm the alert fires. Then put it back and confirm it clears. An alert nobody has seen fire is a belief.

Undoing it#

Remove the rule or the cron entry, and revoke the token if it was issued only for this.

See also#


Checked against agent/desired.go · agent/metrics.go · docs/deploy.md

Updated 2026-09-02 drift monitoring desired-state