p-059 — Implement the Prometheus exporter behind serve-metrics¶
Status: completed. The exporter, the tailer and the serve-metrics
wiring all shipped; the 13 gauges match the metric vocabulary table in
docs/observability.md one for one.
Two things this plan did not cover, noted here so they aren't mistaken
for unfinished plan work: the bundled Grafana dashboard queries only 9 of
the 13 metrics (bffi_stage_outcomes_total,
bffi_stage_throughput_per_minute, bffi_stage_skipped and
bffi_stage_phase_planned are exported but unqueried), and the panel set
in docs/observability.md lists panels the dashboard JSON doesn't ship.
The exporter side is the plan's scope and it is done; closing the
dashboard gap needs its own plan.
Problem¶
bffi-pipeline serve-metrics was a scaffold that raised
NotImplementedError. The stage side of observability was live — every
stage appends to runs/<run>/stage-events.jsonl — but nothing translated
those events into Prometheus metrics, so the Grafana dashboard rendered
no data and make observability-up started a process that died
immediately.
docs/observability.md already specifies the metric vocabulary, the
label cardinality, and the exporter lifecycle. This plan implements that
specification rather than inventing a new one.
Design¶
New module src/bffi_pipeline/observability/exporter.py, three pieces
that are separately testable:
| Piece | Responsibility |
|---|---|
MetricStore |
Pure translation: one sidecar row in → gauge updates on a private CollectorRegistry. No I/O. |
SidecarTailer |
Per-file (st_dev, st_ino) + byte-offset tracking; yields complete JSON rows only. Survives truncation and rotation. |
Exporter |
Glob rescan, poll loop, PID/argv files, atexit cleanup. |
Every metric is a Gauge, including the _total-suffixed ones. The
sidecar carries absolute cumulative counter values, so replaying it
into a Counter (which can only inc) would double-count on exporter
restart. A Gauge holding the last observed absolute value is the honest
primitive. The _total names are kept because the dashboard and
docs/observability.md already specify them.
Derived metrics (bffi_stage_throughput_per_minute,
bffi_stage_eta_seconds) come from a 5-event sliding window of
progress rows per (run_uuid, stage, phase).
Phase label defaults to the _ sentinel for stages without internal
phases, matching the existing convention.
Trade-offs on the record¶
- Gauge-not-Counter as above. Deviation from the
Countercolumn indocs/observability.md; the doc is updated to match. - No socket binding in tests.
MetricStore/SidecarTailer/Exporter.tickare driven directly againsttmp_pathfixtures. The HTTP bind is a two-line call intoprometheus_client; exercising it would mean listening on a port from the unit suite, which the "tests never hit the network" rule rules out. - Rehydration is just a cold tail. On launch the exporter attaches every glob match at offset 0, so restarting rebuilds full state from the sidecars. No separate rehydrate path.
Out of scope¶
- Pushing metrics anywhere. The exporter serves
/metricson localhost for the operator's own Prometheus to scrape; no outbound telemetry. - A
statusCLI. The sidecar plustail -Fcovers that need today.