User Story
As a relay operator, I want /metrics exposed on a separate localhost-only HTTP listener that refuses non-loopback bind addresses at startup, so I can scrape Prometheus metrics via an SSH tunnel or sidecar without putting operational state on the internet-exposed public listener.
Context
The relay is internet-exposed. Prometheus metrics leak operational state (active-connection counts indicate whether anyone is using the relay, upgrade-attempt rates reveal traffic patterns). Putting /metrics on the same listener as /healthz (#10) and /v1/{server,client} would publish that state to anyone who can reach the public listener.
Listener exposure decision (locked, not for re-litigation by the architect): /metrics is exposed on a separate localhost-only listener (default 127.0.0.1:9090). Operators reach it via SSH tunnel or sidecar. The flag default and the bind-address validation are part of this ticket's contract.
This ticket consumes the registry + handler factory introduced in the scaffolding slice (#59) — wires the second http.Server and the flag.
Acceptance Criteria
Technical Notes
Size Estimate
S
Split from #56. Depends on the scaffolding slice (#59).
User Story
As a relay operator, I want
/metricsexposed on a separate localhost-only HTTP listener that refuses non-loopback bind addresses at startup, so I can scrape Prometheus metrics via an SSH tunnel or sidecar without putting operational state on the internet-exposed public listener.Context
The relay is internet-exposed. Prometheus metrics leak operational state (active-connection counts indicate whether anyone is using the relay, upgrade-attempt rates reveal traffic patterns). Putting
/metricson the same listener as/healthz(#10) and/v1/{server,client}would publish that state to anyone who can reach the public listener.Listener exposure decision (locked, not for re-litigation by the architect):
/metricsis exposed on a separate localhost-only listener (default127.0.0.1:9090). Operators reach it via SSH tunnel or sidecar. The flag default and the bind-address validation are part of this ticket's contract.This ticket consumes the registry + handler factory introduced in the scaffolding slice (#59) — wires the second
http.Serverand the flag.Acceptance Criteria
cmd/pyrycode-relay/main.goaccepts--metrics-listen(default127.0.0.1:9090). An empty value disables the metrics listener entirely (operator opt-out — no listener bound, no goroutine started, no error).http.Serverserves/metricson the configured address with explicit timeouts (gosec G114 compliance) matching the existing public listener's pattern inmain.go, and its graceful-shutdown path mirrors that same pattern.--metrics-listenis parsed withnet.ParseIPand rejected unlessIsLoopback()returns true. Hostnames are rejected (avoids DNS-time TOCTOU — IP literals only). A non-loopback or unparseable host causes startup to fail loudly with a clear error message (loud-failure-over-silent-correction pattern,docs/PROJECT-MEMORY.mdline 38).GET /metricsagainst the configured listener (use127.0.0.1:0orhttptest) returns HTTP 200 withContent-Typematching Prometheus text format; (b) non-loopback bind addresses (e.g.0.0.0.0:9090,192.168.x.y:9090) cause the documented startup failure; (c) empty--metrics-listenresults in no listener bound and does not error.make vet,make test -race, andmake buildclean;docs/knowledge/codebase/<n>.mdsummary entry created;docs/knowledge/INDEX.mdupdated.Technical Notes
NewMetricsHandler(reg)factory live ininternal/relay/metrics.gofrom the scaffolding slice (relay: adopt prometheus/client_golang and introduce metrics registry scaffolding #59) — consume them, do not re-create./healthzpublic listener. Do not multiplex/metricsonto the public mux.Size Estimate
S
Split from #56. Depends on the scaffolding slice (#59).