The policy engine and the signer push their metrics and logs over OTLP; nothing on them is scraped (Telemetry lists the variables and every metric they emit). OpenBao answers a health endpoint and, once enabled, serves its own Prometheus metrics. This page puts the three together with OpenTelemetry Collectors and any backend — the examples use Prometheus for metrics, Loki for logs and Grafana on top — and adds what the components do not count themselves: refusals by reason, NATS and lease events, all derived from log records in the collector. Two kinds of collector:
  • 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.
Everything travels outbound to the main collector, over the same path the engine and the signer already push on.

1. Point the components at the main collector

On the engine and on the signer:
The Helm chart sets them from 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).
Do not ship the engine’s container output to a log store. The engine prints its one-time pairing code to its own stdout at every start — on purpose never to the collector, so that whoever reads your logs cannot spend it. A node-level log agent that tails every container (the usual Kubernetes setup) would carry it into your log store anyway. Exclude the engine container from that agent, or drop the line that starts with pairing code:. Everything the engine and the signer log is already in the OTLP push.

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.
Worth watching from its telemetry: 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 the contrib distribution of the collector (the transform and deltatocumulative processors, the count connector). Replace the hosts, the certificate and the log backend with yours.
In Prometheus the dots become underscores and counters get _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.
A three-node vault: list every node — one 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:8200 and your collector on OTLP_CIDRS; a collector in a container on a docker bridge network is dropped, run it with --network host. The node’s certificate carries NODE_ADDR and 127.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 with service.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:
A VM — the vault collector on the host reads the node’s unit from the journal (journalctl must be on the host; an evaluation host with three nodes runs citadel-openbao-0.service, -1 and -2 instead):
Either way the receiver joins a logs pipeline that exports to the main collector:

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 no up 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 window W as
    (a series that did not exist W ago counts in full; one that is gone by now — a replaced pod — falls back to increase()).
  • 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 on max() over instances, which would let the old pod’s last 1 hide the new pod’s 0.

8. Alerts

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):
It reads exactly what the configurations above produce, and expects:
  • a Prometheus data source with uid prometheus and a Loki one with uid loki (give yours these uids, or replace them in the file before the import);
  • the label vault set to engine and seed by 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_connected gauge, 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_approval webhook.
  • Why the signer refused, as a metric attribute — only in its log line (hence citadel_signer_refusals_total, derived).