feat(config): hot-reload Phase 1 — logging.level (ADR-0023)

Implements the first phase of ADR-0023 selective hot-reloading. Adds
viper.WatchConfig wiring + an OnConfigChange handler that re-unmarshals
the Config struct on file changes and applies the hot-reloadable subset.

Phase 1 reloadable field: logging.level — re-applied via SetupLogging
on every change. The remaining 3 fields listed in ADR-0023
(api.v2_enabled, telemetry sampler type/ratio, auth.jwt.ttl) follow the
same pattern and will land in subsequent phase PRs without further
infrastructure work.

Changes:
- pkg/config/config.go : Config struct gets unexported viper + reloadMu
  fields; new WatchAndApply(ctx) method starts the watcher and stops it
  on context cancel. Defensive: no-op when no config file is in use.
- pkg/server/server.go Run() : calls WatchAndApply(rootCtx) so the
  watcher stops on graceful shutdown.
- pkg/config/config_hot_reload_test.go (new) : 3 unit tests covering
  end-to-end reload, no-config-file no-op, nil-viper no-op. Race
  detector clean.
- adr/0023-config-hot-reloading.md : Status → Phase 1 Implemented;
  remaining fields explicitly Proposed for follow-up phases.

Verifier verdict: APPROVE. Race detector passes. Full BDD suite still
green. The @flaky scenario in features/config/config_hot_reloading.feature
remains @flaky for now — activating it requires reliable cross-process
file-watching behaviour which is sensitive to filesystem semantics on
the CI runner; the unit test exercises the same code path deterministically.
This commit is contained in:
2026-05-05 08:44:44 +02:00
parent 03ea2a7b89
commit 2d5b5834ec
5 changed files with 155 additions and 2 deletions

View File

@@ -708,6 +708,10 @@ func (s *Server) Run() error {
s.userService.StartJWTSecretCleanupLoop(rootCtx, s.config.GetJWTSecretCleanupInterval())
}
// Start config hot-reload watcher (ADR-0023 Phase 1: logging.level only).
// Stops automatically on rootCtx cancellation.
s.config.WatchAndApply(rootCtx)
// Create HTTP server
log.Trace().Str("address", s.config.GetServerAddress()).Msg("Server running")