feat(relay): prometheus metrics registry scaffolding (#59)#62
Merged
Conversation
Adopt github.com/prometheus/client_golang (per ADR-0008) and stand up the relay's private metrics registry + handler factory. Scaffolding only — no counters, no gauges, no listener wiring (those land in #57, #58, #60). `NewMetricsRegistry()` returns a fresh `*prometheus.Registry`; per-call construction matches the existing `NewRegistry()` testability shape and lets each test build an independent registry without process-wide collisions. `NewMetricsHandler(reg)` returns `http.Handler` wrapping `promhttp.HandlerFor(reg, …)`. `HandlerOpts.Registry: reg` is load-bearing: it routes promhttp's own self-instrumentation collectors (`promhttp_metric_handler_*`) onto the relay's private registry rather than `prometheus.DefaultRegisterer` — that's how the ADR-0008 § Scope of use "never the default registerer" rule holds even for the library's internal bookkeeping. Three unit tests: - `TestMetricsHandler_EmptyRegistry_ResponseShape` covers the AC: HTTP 200, `Content-Type` starts with `text/plain; version=0.0.4; charset=utf-8`, body parseable by `expfmt`'s text decoder to `io.EOF`. The Content-Type is substring-matched because prometheus/common v0.62+ appends an additive `; escaping=<scheme>` suffix (UTF-8 names support) which the AC literal predates — the substring is still the spec-author's canary for non-additive format changes. - `TestMetricsHandler_RegisterAndScrape_RoundTrip` proves the registry→handler seam works end-to-end with a trivial counter. - `TestMetricsRegistry_NoGlobalRegistrarLeak` structurally enforces the ADR-0008 § Scope of use rule by asserting a zero delta on `DefaultGatherer.Gather()` across registry+handler construction. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
Author
Code Review: #59Decision: PASS FindingsNone blocking. Two well-justified deviations from the spec, both documented inline:
Verification
Security review verification (security-sensitive)
SummaryFaithful, minimal implementation of the scaffolding slice. ADR-0008 is comprehensive and matches the dep tree in The |
…+ feature in INDEX (#59) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adopt
github.com/prometheus/client_golang(per ADR-0008, landed in the spec commit on this branch) and stand up the relay's private metrics registry + handler factory.internal/relay/metrics.go:NewMetricsRegistry() *prometheus.Registry— fresh registry per call (no singleton; mirrorsNewRegistry()'s test-friendly shape).NewMetricsHandler(reg) http.Handler— wrapspromhttp.HandlerFor(reg, …)following the factory pattern fromNewHealthzHandler(relay: /healthz returns version + connected-binary count + connected-phone count (JSON) #10).HandlerOpts.Registry: regroutes promhttp's own self-instrumentation counters onto the private registry, neverDefaultRegisterer(ADR-0008 § Scope of use).go.mod/go.sum— direct depgithub.com/prometheus/client_golang@v1.23.2; transitivesclient_model,commonpromoted to direct because tests import them; remaining transitives (perks,xxhash,goautoneg,procfs,protobuf, …) are indirect.Scaffolding only — no counters, no gauges, no listener wiring. Those land in #57, #58, #60.
Issue
Closes #59.
Testing
Three unit tests in
internal/relay/metrics_test.go:TestMetricsHandler_EmptyRegistry_ResponseShape— the explicit AC: HTTP 200,Content-Typeprefix-matchestext/plain; version=0.0.4; charset=utf-8, body parseable byexpfmt's text decoder.TestMetricsHandler_RegisterAndScrape_RoundTrip— registers a trivial counter against the registry, scrapes, asserts the counter+value appears in the body.TestMetricsRegistry_NoGlobalRegistrarLeak— structural defence for ADR-0008's "neverDefaultRegisterer" rule: asserts zero-delta onDefaultGatherer.Gather()across registry+handler construction.Gates clean:
make vetmake test(-race)make buildArchitecture compliance
HandlerOpts.Registry: regis set deliberately — spec rationale matches the ADR-0008 § Scope of use rule "DefaultRegisterer is never touched", including for promhttp's own bookkeeping. Documented in themetrics.godocstring.EnableOpenMetricsleft default false), matching/healthz's no-negotiation posture.Deviation from spec test as-written, with rationale
Two test items in the spec couldn't be implemented literally:
prometheus/commonv0.62+ appends; escaping=<scheme>(defaultunderscores) to the Content-Type as part of Prometheus 3.0 UTF-8 name support. The suffix is additive and backward-compatible. The spec explicitly designates the test literal as a "canary" for format changes; the canary fired and the change is benign. I converted exact-equality to prefix-match on the AC literaltext/plain; version=0.0.4; charset=utf-8, keeping the literal as a substring (so a non-additive change still surfaces as a failure). Rationale in the test'stextFormatPrefixdoc comment.HandlerOpts.Registry: reg(a load-bearing design choice the spec keeps) means the registry isn't empty afterNewMetricsHandler: promhttp registers its own self-instrumentation collectors on it. I dropped thefamilies == 0assertion; the "parseable body" half of the AC (decoder reachesio.EOFwithout an error) is preserved. Rationale in the test's inline comment.Neither deviation weakens the AC; both make the test internally consistent with the spec's own design choices.
Out of scope
NewMetricsHandleratcmd/pyrycode-relay/main.go— relay: localhost-only /metrics listener with bind-address validation #60.docs/knowledge/codebase/59.mdanddocs/knowledge/INDEX.md— per the spec's § Open questions relay: connection registry — server-id → binary + server-id → [phone] thread-safe maps #3, the documentation phase owns those.🤖 Generated with Claude Code