- The main collector, one, wherever your collectors run. It receives every push, derives the counters below from the log records and hands everything to your backends.
- A vault collector beside each OpenBao, on the same host or in the same namespace. Your vaults take no connections from outside their side (the signer’s side accepts none at all), so this one reads its vault locally and pushes what it reads to the main collector. It accepts nothing itself.
1. Point the components at the main collector
On the engine and on the signer:otlp.endpoint and otlp.caSecret, the VM installer from
OTLP_ENDPOINT and OTLP_CA.
Start the main collector before them. Logs are pushed once and never retried: whatever the
engine logs while no collector answers — its keys, its policy version, policy engine ready
— stays in its own output only (and otlp.logs.dropped{reason="export"} counts it).
2. OpenBao
Health — works on every vault, no token.GET /v1/sys/health?standbyok=true answers
200 when the node is initialized and unsealed (active, or a standby of a three-node
cluster), 503 sealed, 501 not initialized; a node that is down does not answer.
Metrics. OpenBao serves /v1/sys/metrics?format=prometheus only when its server
configuration has a telemetry stanza, and only to a token allowed to read sys/metrics:
A sealed vault serves no metrics — it cannot check the token. Take the seal state from the
health endpoint, never from
vault_core_unsealed. The Helm chart and the VM installer do not
set the telemetry stanza yet; until they do, the health endpoint is what OpenBao gives you.vault_route_update_multichainsign_sum / _count (time
per sign, derive and verdict signature inside the vault — the closest thing to signing
latency the stack has), vault_raft_peers, vault_autopilot_healthy,
vault_autopilot_failure_tolerance, vault_core_active.
3. The main collector
It needs thecontrib distribution of the collector (the transform and
deltatocumulative processors, the count connector). Replace the hosts, the certificate
and the log backend with yours.
_total:
citadel.engine.decisions is citadel_engine_decisions_total, citadel.engine.lease.active
is citadel_engine_lease_active. service.name and service.instance.id become job
(with service.namespace in front, if you set one) and instance.
4. A vault collector beside each OpenBao
The same configuration twice, once beside each vault, with two variables:
Use exactly
engine and seed: the dashboard and the alerts below tell the two vaults apart
by the label vault. Put the metrics token (policy citadel-metrics) of that vault in
/etc/otelcol/openbao-metrics.token.
httpcheck target and one scrape target per
node, the per-pod addresses on Kubernetes
(<release>-openbao-0.<release>-openbao-internal.<namespace>.svc.cluster.local:8200, …).
Each node serves its own metrics and its own health; the dashboard and the alerts count a
vault as up while any of its nodes is unsealed.
Where it runs:
- Kubernetes. A small Deployment in the role’s namespace. With the chart’s network policy on (the default) that namespace denies all traffic but the chart’s own paths. Add a NetworkPolicy of yours — policies add up, the chart’s stays as it is — that lets the collector pod reach the OpenBao pods on 8200, DNS and the main collector, and lets the OpenBao pods accept it on 8200.
- A VM. On the host itself (the collector’s own package, a systemd service). The
installer’s firewall lets this host’s processes reach their node on
NODE_ADDR:8200and your collector onOTLP_CIDRS; a collector in a container on a docker bridge network is dropped, run it with--network host. The node’s certificate carriesNODE_ADDRand127.0.0.1.
5. OpenBao’s log (optional)
The dashboard’s OpenBao log panel shows OpenBao’s server log once something sends it to the main collector withservice.name openbao and citadel.vault set to the vault. OpenBao
writes it to its own output, so where it is depends on where the vault runs. Read only the
OpenBao container — never every container (see the warning above).
Kubernetes — add a receiver to the node agent that already reads /var/log/pods (a
DaemonSet), not to the vault collector, which runs once:
journalctl
must be on the host; an evaluation host with three nodes runs citadel-openbao-0.service,
-1 and -2 instead):
6. What to watch
Policy engine
Signer
Approved by the engine versus answered by the signer: every
sign approval should be
followed by a signer answer (signed, failed or refused). An approval with no answer was lost
between them — typically a signer that was off the bus when the verdict was published.
Compare citadel_engine_decisions_total{kind="sign",result="approved"} with
citadel_signer_requests_total{op="sign"} over 15 minutes.
OpenBao, both vaults
Liveness and the telemetry itself
There is nothing to scrape on the engine or the signer, so noup either: a component is
alive while its pushes keep arriving. With the exporter sending push times,
time() - timestamp(citadel_engine_policy_version) is the age of the engine’s last push
(and citadel_signer_nats_connected for the signer). Alert when it passes three push
intervals — 180 s at the default 60 s. otlp_logs_dropped_total above 0 means log lines
reached only the component’s own output.
7. Counting right
Two habits of Prometheus meet two habits of these components:- A counter series is born at its first event. The components create a series the first
time it counts, so Prometheus never sees it at 0 and
increase()drops that first event — the first signer refusal ever would never fire an alert. Count events in a windowWas(a series that did not existWago counts in full; one that is gone by now — a replaced pod — falls back toincrease()). - A replaced pod’s last value lingers for minutes, beside the new pod’s. Take state
gauges only from fresh pushes,
M unless (time() - timestamp(M) > 180), and alert on connectivity per instance rather than onmax()over instances, which would let the old pod’s last1hide the new pod’s0.
8. Alerts
Prometheus rules
Prometheus rules
9. The dashboard
The “Citadel merchant stack” Grafana dashboard is one JSON file,citadel-merchant-stack.dashboard.json, published with each release beside the other
artifacts and their SHA256SUMS. Download both from <download-url>/<version>/, check the
hash and import the file in Grafana (Dashboards → New → Import):
- a Prometheus data source with uid
prometheusand a Loki one with uidloki(give yours these uids, or replace them in the file before the import); - the label
vaultset toengineandseedby the two vault collectors; - for its OpenBao log panel, OpenBao’s log (section 5); without it that panel stays empty and the rest is unaffected.
Host and container resources (CPU, memory, disk) are not on it: take them from what your
platform already collects for its nodes and pods.
What is not measured
- Latency of a decision or a signature — the components push counters and gauges only. The time per key operation inside OpenBao is the closest measure.
- Traces — none are emitted.
- The engine’s NATS events — it logs no disconnects or reconnects; you get its
nats_connectedgauge, sampled at every push. - Holds for approval — approval rules hold a payout on the platform, before your engine
sees it. Follow them with the
operation.awaiting_approvalwebhook. - Why the signer refused, as a metric attribute — only in its log line (hence
citadel_signer_refusals_total, derived).