Closes the disagreement left open by #2453: requestlog.go logged raw
r.RemoteAddr while the Active-sessions surface resolved through the operator's
configured proxy depth. Behind a proxy — the normal deployment for anything
public — every access-log line carried the same useless proxy address, and the
two surfaces contradicted each other about who connected. Logs and UI
disagreeing is worse than either being wrong alone, because it costs you trust
in both.
`remote` now holds auth.ClientIP(r, hops). The attribute KEY is deliberately
unchanged so existing log greps keep working; only its accuracy improved.
Wiring note. The access log covers /healthz and the SPA, so it's registered
before the pool-bearing branch that used to build the settings service. Rather
than close over a variable reassigned later — which works, but leaves a
mutable-after-registration seam and an awkward question about races — I hoisted
netsettings.New above the router entirely. It already handles a nil pool by
returning a default-valued service, so no branch is needed and the accessor
stays a plain method value.
Applied the lesson from the last three CI failures BEFORE pushing this time: a
bare-identifier grep for `requestLog(` found three call sites in
requestlog_test.go that a qualified pattern could never have matched, since
the function is package-private and its tests are in-package. Also swept
netsettings.New and ClientIP the same way.
Tests: the behaviour change gets its own table — nil accessor and depth 0 log
the socket peer, depth 1 through a PUBLIC-addressed proxy logs the client
(the exact case the old heuristic got wrong forever), depth 2 reaches through
a CDN. Added `remote` to the required-keys assertion so the attribute can't
quietly disappear.
Two debuggability gaps surfaced by the /api/admin route shadowing
investigation:
1. handleTestLidarrConnection swallowed client.Ping's error — only the
bucket code (lidarr_unreachable) reached the response, the underlying
*net.DNSError / *net.OpError / TLS error never reached the logs.
Operators couldn't tell a typo'd hostname from a wrong port from a
refused TLS handshake. Now logged at Warn (expected-when-misconfigured)
with the base_url for diagnostic context. The api_key is never logged.
2. The chi router had no access-log middleware — every 4xx/5xx was silent,
making it impossible to tell whether a request even reached the server.
chi's middleware.Logger writes to the standard log package not slog,
so a small slog wrapper handles it instead. /healthz is skipped (the
compose healthcheck hits it every 5s; would be ~17k log lines/day of
noise). Severity is keyed off response status: 5xx -> Error,
4xx -> Warn, else Info — so 4xx/5xx surface even when the operator's
logger level is set above Info.
Tests cover both the severity routing and the /healthz skip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>