ADR-045: gyrum-observ-integrate CLI supersedes the Phase-1 manual checklist
Status: Accepted Date: 2026-04-21
Context
The original observability playbook was a
Phase-1-shaped checklist: nine numbered steps an operator copy-pasted into a
service to wire observ. Each step produced the same edits in every service —
the same imports, the same SetBuildInfo call, the same Dockerfile ldflags
(ADR-032).
Two things have changed since that playbook shipped:
- Scope grew. Phase 2a (HTTP middleware), Phase 2b (metrics.Instrument
- RegisterAppInfo), and Phase 2c (WrapDSN + Probe) each added new wiring
steps. The checklist is now 15+ items, including ordering constraints
(
observ.AccessBEFOREmetrics.Instrument;metrics.RegisterAppInfoAFTERSetBuildInfo; seedistill-gyrum-ai/internal/middleware/stack.go). Every new service gets a fresh chance to mis-order them.
- RegisterAppInfo), and Phase 2c (WrapDSN + Probe) each added new wiring
steps. The checklist is now 15+ items, including ordering constraints
(
- The fleet grew. There are 30+ generated product repos plus the named services (distill, buzzy, guardian, control-plane, hiphip, etc.). A manual rollout across them would take hours and drift — the playbook would have been wrong within a week of the first Phase-2 ship, and silently.
The existing warn-level linters in devtools
(lib/observ-linters.sh, see ADR-003)
already detect drift — they flag log.Print* in observ-importing services,
Dockerfiles without observ ldflags, and main.go files missing
SetBuildInfo. But they don't fix it.
Decision
Ship a devtools CLI — gyrum-observ-integrate — that owns the wiring contract
end to end. The CLI:
- Is the one-command scaffolder for new services.
- Is the one-command upgrader for existing services.
- Has a
--checkmode that reports integration status without making any change, and exits non-zero when a service is not fully integrated. - Is idempotent: every stage checks the "already done" state and skips.
- Is conservative: when a change cannot be made safely (ambiguous router,
non-reference
main.goshape, hand-composed middleware chain), it leaves aTODO observ-integrate:comment and flags the stage rather than guess.
The manual checklist survives as Appendix A of the rewritten playbook — it is the fallback when the CLI hits an ambiguous case and the exact specification that the CLI's apply stages follow. The appendix and the CLI are kept in sync by hand; drift between them is a review-blocker.
A companion dark-factory/scripts/observ-integration-audit.sh walks every
gyrum-labs repo and runs gyrum-observ-integrate --check across it,
collating the matrix into docs/observability-fleet-status.md. That matrix
is the coverage dashboard the platform team reads.
Consequences
- One source of truth for the integration contract. The CLI's apply stages are the machine-readable spec; the playbook's appendix is the human-readable spec; the linters remain the PR-time smoke test. All three trace back to the reference integration in distill.
- Fleet rollout becomes a script, not a project. For each service:
gyrum-observ-integrate <slug> --drill-key <key>→ review diff → PR. 30+ repos complete in a day, not a quarter. --checkis the audit loop. The audit script wraps it, the platform team gets a weekly matrix, and the same--checkruns inside any CI workflow that wants to gate on full integration.- TODO comments become the manual-intervention queue. The ambiguous
cases the CLI refuses to automate (custom routers, gqlgen wiring,
non-
cmd/*/main.golayouts) surface as structured TODOs in the output, not silent gaps. They are the known long tail of the contract. - The playbook gets shorter. Step 1 is the CLI. Steps 2–5 are verification + PR + deploy + post-deploy. The 15-step manual checklist collapses into an appendix operators consult only when the CLI tells them to.
- The CLI owns a growing contract. Every future observ addition (tracing, SLO annotations, profile export) adds a stage. The cost of that growth is carried once, in the CLI, instead of once per service.
Alternatives considered
- Keep the manual checklist; rely on linters to catch drift. What we had. The linters catch the symptoms but require an operator to type the fix into every service. At fleet scale, fixes lag new services being generated, so coverage asymptotes at "most but not all."
- A
gyrum-setupflag (--with-observ). Bolts the wiring onto the bootstrap path. Works for new services but offers nothing to existing ones, which is where the coverage gap lives. - A Go code-mod tool (gofmt-style AST rewrite). Higher fidelity for
complex
main.goshapes but a much larger maintenance surface, and the conservative-TODO path we want already keeps the CLI safe. Reserved for Phase 3 if the TODO tail is too long to live with. - CI-only enforcement — force every PR to fully integrate. Brutal and wrong. A service that never logs to stderr or never serves HTTP does not need the full middleware chain. The CLI stays opt-in; the audit surfaces who has opted in and who hasn't.
- A generator template under
templates/instead of an integrator. Templates are excellent for new services but unusable for existing ones (the cost to retrofit is the whole point of the problem). We keep the stack templates in~/.gyrum/devtools/stacks/for the new-service path and the CLI for the upgrader path; they share the reference integration.
Supersedes: the Phase-1 version of docs/observability-playbook.md
(superseded in the same PR).
Superseded by: