Skip to content
BitExactdocs
Documentation
On this page

Ops health, alerts, and capacity#

The recorder and proxy produce evidence; operations needs to know when that production is degrading. bitexact health (and the Settings ▸ Health panel) aggregate the per-run integrity signals into one status a monitor can gate on.

Status and alerts#

bitexact health scans the store and reports a status — healthy, degraded, or critical — with typed alerts:

AlertSeverityMeaning
verify-failcriticala run no longer verifies — tamper, or an unreadable/torn manifest
anchor-rollbackcriticalan external anchor attests more steps than the store now holds (the store was rewound below an anchored point)
seal-failurewarninga quarantined torn tail from an interrupted write
recording-failurewarningthe recorder dropped a response (an in-band failure it recorded)
orphan-journalwarningan unmerged stream journal — resolve with bitexact repair
unsealed-runwarninga run with no seal that nothing has written for 24 hours: its end is not attested. If it is finished, bitexact seal RUN
capture-key-revokedwarning; critical when the active capture key is revokedthe capture policy names revoked capture keys; the alert's revoked maps each key id to the date it was revoked. Fields committed under one are guessable by whoever holds it; when it is the active key, rotate to a new hash_only_key

The report also carries every alert the coverage scorecard raises over the whole deployment (source-quiet, source-dark, source-silent, source-never-seen, expectation-removed, collector-backlog, collector-unrecorded; see Sources and silence), so a declared source that stops writing is raised however busy the rest of the store is. The inspector's Settings ▸ Health and Overview raise the same alerts, beside auth-off-prod and no-recent-evidence, and name a host only to a role that may open Settings.

The command exits 2 on a critical status, so it drops straight into a monitor:

bitexact health            # human-readable
bitexact health --json     # the raw report for a scraper; exit 2 on critical

BitExact surfaces the health and the non-zero exit; your monitoring (cron, a liveness probe, a scrape of /api/health) turns that into an alert on verify-fail / seal-fail / anchor-rollback. Alerting is deliberately your channel — BitExact does not send mail or hit a pager itself.

What expires on a calendar#

Three things in a deployment expire, and each is a predictable outage if nobody is told: host and recorder certificates (bitexact pki issue, 365 days by default), the revocation list (bitexact pki crl, 365 days - past it the recorder refuses every host) and the license (past it and its grace, recording stops; traffic does not). bitexact health reports each one:

bitexact health --cert /pki/recorder.crt --cert /pki/host-1.crt \
                --crl /pki/ca.crl --warn-days 30
AlertSeverityMeaning
cert-expiring / crl-expiringwarningwithin --warn-days (default 30)
cert-expired / crl-expiredcriticalpast it - an expired CRL refuses every host
cert-unreadable / crl-unreadablecriticalthe file cannot be read as one
license-expiringwarningthe license ends within --warn-days
license-in-gracewarningexpired, still recording until its grace ends
license-expiredcriticalrecording has stopped; agents run unrecorded
standingwarning when amber; critical when redwhere the deployment stands (Licensing): its 90-day renewal is due within 14 days (amber) or has passed (red, still recording), the contract is in its grace (amber) or past it, or the deployment may not record at all - no valid or bound license, or files that are not the release's (red). Its reasons say which, and what to run

The same standing is in bitexact health --json as standing (state, reasons, records), in the viewer's deployment settings, and as the standing field of every run's run_meta, so a sealed run carries the standing it was recorded under.

Renew a certificate by issuing a new one under the same CA with a new name

  • bitexact pki issue never overwrites one (bitexact pki issue host-1-2027 --dir pki --spiffe spiffe://acme/agents/host-1 --days <days>, the same identity as before) - and pointing the component at the new files; the recorder reads its certificate at start, so restart it, and a collector presents its new one on its next connection. Renew the CRL with bitexact pki crl --dir pki; the recorder re-reads it as it changes.

Expiry alerts as events#

With event delivery configured (Event delivery), the same certificate and CRL alerts leave as alert.opened and alert.resolved events, for every certificate a BitExact process serves with or presents, wherever it runs:

PublisherPublishes, as service on host
bitexact recorderrecorder: --tls-cert, --tls-client-ca, --tls-crl
bitexact serveproxy: --tls-cert, --tls-client-ca, and the recorder client certificate and CA (BITEXACT_RECORDER_TLS_CERT, BITEXACT_RECORDER_TLS_CA) when BITEXACT_RECORDER is set
bitexact viewviewer: the same files as bitexact serve
the recorder, at each peer's hello and heartbeatrecorder-client on the peer's name: the client certificate a collector, an inspector or the command line presents
the recorder, at each collector heartbeatcollector on the collector's peer name: the CA file it checks the recorder against (--tls-ca), which the collector reports in the heartbeat's trust_anchor (Collector protocol)
every process that pushes a checkpoint to a witnesswitness on the witness's host:port: the certificate an https witness in BITEXACT_LOG_WITNESSES presents

Each publisher writes one expiry fact per file to the store (the events/expiry-facts documents: the service, host, path, not-after or next update, subject and SHA-256 fingerprint, or why the file does not read) at start and every hour, reading the file afresh each time. The alert round evaluates every published fact, whichever process holds the alerts lease, so an alert is never resolved because that process cannot see the file. An alert resolves when its subject is renewed - the next fact published carries a date past --warn-days - or when nothing has published it for 24 hours: its process stopped, its file is no longer named, or the peer no longer connects. Until then a fact keeps raising its alert from the date it last published. Once a retired fact's alert has resolved, the process holding the alerts lease deletes the fact, so the store keeps only facts that can still raise one. Alerts are operational signals, never evidence, and a store that refuses a fact is logged (expiry_publish_failed) without stopping the process; the next hourly refresh writes it again. bitexact health reports the same published facts beside the files its --cert and --crl name, so it can be run on any host that reaches the store.

The recorder's health port#

bitexact recorder --health-port 7480 serves plain HTTP beside the mTLS port, for orchestrators and Prometheus:

PathAnswers
/livez200 while the process answers
/readyz200 while the store takes writes; 503 with the reason when it does not. Its standing is the state alone (ok, amber or red), never the reasons: the port is unauthenticated, and a reason can name a file path. It never computes the coverage scorecard, so a probe every few seconds never pays for reading every expectation and source
/metricsbitexact_recorder_ready, bitexact_license_valid{state=...}, bitexact_license_days_left, and bitexact_expiry_days_left{what=...} for its certificate and CRL; with events configured, the outbox's bitexact_events_pending, bitexact_events_dead, bitexact_events_dropped_total, bitexact_events_delivered_total{destination=...}, bitexact_events_enqueue_failed_total, bitexact_events_oversize_total and bitexact_events_oldest_pending_seconds, every _total a deployment-wide count no prune lowers (Events); and the source and collector gauges below

A lapsed license, a red standing or an expiring certificate is a metric and an alert, never a reason to be unready: taking the recorder out of service only makes every collector spool. The Kubernetes and compose files in deploy/ probe it.

Recording health#

The recording proxy (bitexact serve) answers GET /health/recording, unauthenticated like /healthz and /metricsz beside it, with whether the calls arriving now are being recorded:

{"status": "recording", "store_reachable": true, "disk_free_bytes": 52613349376,
 "breaker": {"state": "closed", "trips_total": 0, "consecutive_failures": 0},
 "license": "ok", "last_write": "ok",
 "counts": {"degraded_total": {}, "unrecorded_calls_total": {},
            "refusals_unwritten_total": 0}}
FieldMeaning
statusrecording, or degraded when any check below fails
store_reachablethe store says it can take a write (as /healthz asks it)
disk_free_bytesfree bytes on the data directory's filesystem; below BITEXACT_DISK_MIN_BYTES it is degraded, naming the shortfall
breakerthe store circuit breaker: closed, open or half_open, how often it has opened, and the store faults in a row (Never in the way)
licenseok, grace, expired, missing or invalid; collector on a BITEXACT_COLLECTOR host, where the recorder enforces it
last_writethe last store write: ok, failed, skipped (the breaker refused it), or null before the first
countsthis process's degraded_total and unrecorded_calls_total by cause, and refusals_unwritten_total, as /metricsz reports them
reasonpresent only while degraded: each failing check, ; -separated

It answers 200 while recording and 503 while degraded. The counts are history and never make it 503 on their own. It is a recording probe, not a traffic one: calls keep passing through while it is 503, so alert on it and keep the orchestrator's liveness probe on /healthz.

Sources and heartbeats#

Each collector tells the recorder it is up as soon as it connects and then every BITEXACT_HEARTBEAT_S seconds (default 60): its spool counts, its dropped ranges, and each run and SDK label it took events for since its last kept beat (the frame is in the protocol). The recorder keeps the latest in the store's capture_sources table, under the peer the collector's certificate names, with last_heartbeat_at taken from the recorder's own clock and the collector's interval as heartbeat_s. The table holds at most 100,000 sources; past that the least recently written is evicted, and every eviction is counted.

A served proxy and a Python SDK process that write into the store themselves (no BITEXACT_COLLECTOR) keep a writer row of their own in the same table, one per capture path they record through (proxy, mcp-proxy, sdk-python or an adapter's: otel, langchain, openai-agents, claude-agent-sdk, adk, auditant, llamaindex, vapi, or the governance-decision API's governance), keyed by path, host name and process id. The MCP proxy keeps its own mcp-proxy row, so its liveness is never read off the model proxy's, and a process recording policy and human decisions keeps a governance row, so decision recording is never read off the SDK's other traffic. A background thread upserts it as soon as the path starts and then every BITEXACT_HEARTBEAT_S seconds: started_at, the last_call_at of the path's latest call, last_heartbeat_at and the interval as heartbeat_s. A value that is not a positive, finite number of seconds falls back to 60. This row is what lets the scorecard read an idle proxy or SDK process as quiet rather than silent, and as dark once three beats are missed. A restarted process starts a new row; the rows of processes that stopped beating age out under the same 100,000-source bound, each eviction counted.

A heartbeat is operations state, never evidence. A beat the store cannot take is refused and sent again with the next one, and the session goes on recording; a liveness row the store will not take is logged (liveness_failed) and counted in liveness_failures_total, which the served proxy reports at /metricsz, and the agent's call goes on.

From the coverage scorecard, /metrics serves three gauges, so a "no data" monitor in your own Datadog or Grafana can sit on them:

GaugeValue
bitexact_source_last_evidence_seconds{expectation=...}an age, not a timestamp: the seconds elapsed, at the scrape, since the expectation's source last produced evidence, so it climbs while the source is silent and drops when evidence arrives; left out when there is none to measure, as for a source never seen
bitexact_source_state{expectation=...,state=...}1 for the state the expectation is in and 0 for each other: live, quiet, dark, silent, never-seen, gapped (and the expectation's own state when it is in another)
bitexact_collector_last_heartbeat_seconds{peer=...}an age, not a timestamp: the seconds elapsed, at the scrape, since the recorder last heard that collector's heartbeat

Alert on an age with a threshold, such as bitexact_source_last_evidence_seconds > 3600; it is not a Unix time to subtract from time(). Only declared expectations have series: one inferred from history never alerts, so it serves none.

Every age in one scrape is measured from one instant, the scorecard's own at. The scorecard is computed once per /metrics scrape and never for /readyz. When it cannot be computed, none of these series is served; the scrape itself never fails on it, and readiness never depends on it.

A current heartbeat says the collector is up, never that capture works. The scorecard raises these whatever the heartbeat says:

AlertSeverityMeaning
collector-backlogwarninga collector's spool holds more than 80% of its limit, or it reports dropped ranges
collector-unrecordedwarninga collector reports events the recorder refused for good
inventory-unverifiedcriticalthe capture expectations ledger does not verify, so no declared source can be judged until it does

Capacity#

The report also carries capacity: total bytes on disk and the largest runs, so GB-scale runs are visible before they are a problem. A large run's bundle exports as streaming JSONL (bitexact export --format jsonl, a header line followed by one entry per line) so neither the recorder nor a verifier ever has to hold a multi-gigabyte bundle whole in memory. Redaction, verification, and the fork/replay lab all operate over the streamed form.

Recorder memory. For each run it is writing, the distributed recorder holds the digest and the id of every event it has taken, under 80 bytes an event (the suite holds each part under 40), so it can tell a resend from an equivocation and a reused id from a new one exactly. That memory is released when the run seals or sits idle past the sweep, and the run then lives only on disk. It grows with the events of the runs open at once, not with the store: a recorder with ten runs of a million events open needs about 800 MB for them.