Compare commits
1 Commits
fix/config
...
feat/adr-0
| Author | SHA1 | Date | |
|---|---|---|---|
| 2eb69f2709 |
@@ -299,11 +299,10 @@ jobs:
|
|||||||
# Check for version bump on main branch
|
# Check for version bump on main branch
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||||
echo "🔖 Checking for version bump..."
|
echo "🔖 Checking for version bump..."
|
||||||
# Read commit message from git, NOT from the workflow event payload.
|
# Always read from git log: ${{ github.event.head_commit.message }} expression
|
||||||
# The event-payload expression is interpolated literally into the
|
# is interpolated literally into the shell script, so any backtick, unbalanced
|
||||||
# rendered script (even inside comments — see PR #38 + #46), so any
|
# quote, or special char in a commit body breaks the next line of the script
|
||||||
# backtick / unbalanced quote / multi-line body breaks bash parsing.
|
# (observed on PR #32-#35: 'syntax error: unexpected newline'). git log is safe.
|
||||||
# git log is interpolation-free and safe.
|
|
||||||
COMMIT_MSG=$(git log -1 --pretty=%B)
|
COMMIT_MSG=$(git log -1 --pretty=%B)
|
||||||
./scripts/ci-version-bump.sh "$COMMIT_MSG" --no-push
|
./scripts/ci-version-bump.sh "$COMMIT_MSG" --no-push
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Config Hot Reloading Strategy
|
# Config Hot Reloading Strategy
|
||||||
|
|
||||||
**Status:** Phase 1+2+3 Implemented (2026-05-05). Hot-reloadable fields: `logging.level`, `auth.jwt.ttl`, `telemetry.sampler.type`, `telemetry.sampler.ratio`. Plumbing: `Config.WatchAndApply` in `pkg/config/config.go`, `ReconfigureTracerProvider` in `pkg/telemetry/telemetry.go`, sampler reconfigure callback wired in `pkg/server/server.go Run`. Phase 2 also fixed a pre-existing bug where the hardcoded 24h TTL ignored `auth.jwt.ttl` from config. Remaining field `api.v2_enabled` is **deferred**: hot-reloading routing requires either an always-register-with-middleware-gate refactor of the chi router or an atomic router swap — different complexity class, separate ADR if reopened.
|
**Status:** Phase 1+2 Implemented (2026-05-05 — `logging.level` and `auth.jwt.ttl` hot-reloadable via `Config.WatchAndApply` in `pkg/config/config.go`, wired in `pkg/server/server.go Run`. Phase 2 also fixed a pre-existing bug where the hardcoded 24h TTL ignored `auth.jwt.ttl` from config entirely.) Phase 3 sub-phase 3.1 Implemented (2026-05-05 — `ReconfigureTracerProvider` in `pkg/telemetry/telemetry.go` added). Phase 3 sub-phase 3.2 In Flight (2026-05-05 — `telemetry.sampler.type` + `telemetry.sampler.ratio` hot-reload via `SetSamplerReconfigureCallback` in `pkg/config/config.go`. Remaining field: `api.v2_enabled`.)
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
**Authors:** Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-05
|
**Date:** 2026-04-05
|
||||||
**Last Updated:** 2026-05-05
|
**Last Updated:** 2026-05-05
|
||||||
|
|||||||
@@ -730,19 +730,11 @@ func (c *Config) WatchAndApply(ctx context.Context) {
|
|||||||
// Stop the watcher on context cancel — we set a flag that the
|
// Stop the watcher on context cancel — we set a flag that the
|
||||||
// OnConfigChange handler checks, avoiding the race with viper's
|
// OnConfigChange handler checks, avoiding the race with viper's
|
||||||
// internal state that would occur if we called OnConfigChange again.
|
// internal state that would occur if we called OnConfigChange again.
|
||||||
//
|
|
||||||
// We deliberately do NOT log inside this goroutine: this goroutine
|
|
||||||
// outlives ctx (parent's defer cancel only fires when the test's
|
|
||||||
// outer scope exits, not when t.Cleanup runs), so a log call here
|
|
||||||
// races with the next test's LoadConfig → SetupLogging →
|
|
||||||
// zerolog.SetGlobalLevel under -race (observed 2026-05-05, Q-038).
|
|
||||||
// The flag-set is the load-bearing operation; the missing log line
|
|
||||||
// is a small ops cost (operators learn the watcher stops on shutdown
|
|
||||||
// via the parent shutdown logs, not a dedicated message).
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
c.reloadMu.Lock()
|
c.reloadMu.Lock()
|
||||||
c.watcherStopped = true
|
c.watcherStopped = true
|
||||||
c.reloadMu.Unlock()
|
c.reloadMu.Unlock()
|
||||||
|
log.Info().Msg("Config hot-reload watcher stopped")
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestMain quiets the global zerolog level for the duration of the test
|
|
||||||
// suite. Rationale (Q-038, 2026-05-05): viper's internal watcher goroutine
|
|
||||||
// (started by viper.WatchConfig in WatchAndApply) has no public Stop and
|
|
||||||
// can outlive a test's context. Any log call from a leaked goroutine
|
|
||||||
// races with the next test's LoadConfig → SetupLogging →
|
|
||||||
// zerolog.SetGlobalLevel under `go test -race`. Disabling the logger here
|
|
||||||
// is the root-cause fix: the racing memory location is zerolog's gLevel
|
|
||||||
// global, and if no log call ever evaluates against it we sidestep the
|
|
||||||
// race entirely without changing production behavior.
|
|
||||||
//
|
|
||||||
// In production, log calls happen against an unchanging global level
|
|
||||||
// (SetupLogging runs once at startup), so the race condition does not
|
|
||||||
// occur there.
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
|
||||||
@@ -679,11 +679,10 @@ func (s *Server) Router() http.Handler {
|
|||||||
func (s *Server) Run() error {
|
func (s *Server) Run() error {
|
||||||
// Initialize OpenTelemetry if enabled
|
// Initialize OpenTelemetry if enabled
|
||||||
var err error
|
var err error
|
||||||
var telemetrySetup *telemetry.Setup
|
|
||||||
if s.withOTEL {
|
if s.withOTEL {
|
||||||
log.Trace().Msg("Initializing OpenTelemetry tracing")
|
log.Trace().Msg("Initializing OpenTelemetry tracing")
|
||||||
|
|
||||||
telemetrySetup = &telemetry.Setup{
|
telemetrySetup := &telemetry.Setup{
|
||||||
ServiceName: s.config.GetServiceName(),
|
ServiceName: s.config.GetServiceName(),
|
||||||
OTLPEndpoint: s.config.GetOTLPEndpoint(),
|
OTLPEndpoint: s.config.GetOTLPEndpoint(),
|
||||||
Insecure: s.config.GetTelemetryInsecure(),
|
Insecure: s.config.GetTelemetryInsecure(),
|
||||||
@@ -695,7 +694,6 @@ func (s *Server) Run() error {
|
|||||||
if s.tracerProvider, err = telemetrySetup.InitializeTracing(context.Background()); err != nil {
|
if s.tracerProvider, err = telemetrySetup.InitializeTracing(context.Background()); err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to initialize OpenTelemetry, continuing without tracing")
|
log.Error().Err(err).Msg("Failed to initialize OpenTelemetry, continuing without tracing")
|
||||||
s.withOTEL = false
|
s.withOTEL = false
|
||||||
telemetrySetup = nil
|
|
||||||
} else {
|
} else {
|
||||||
log.Trace().Msg("OpenTelemetry tracing initialized successfully")
|
log.Trace().Msg("OpenTelemetry tracing initialized successfully")
|
||||||
}
|
}
|
||||||
@@ -716,27 +714,7 @@ func (s *Server) Run() error {
|
|||||||
s.userService.StartJWTSecretCleanupLoop(rootCtx, s.config.GetJWTSecretCleanupInterval())
|
s.userService.StartJWTSecretCleanupLoop(rootCtx, s.config.GetJWTSecretCleanupInterval())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire the sampler hot-reload callback (ADR-0023 Phase 3, sub-phase 3.3).
|
// Start config hot-reload watcher (ADR-0023 Phase 1: logging.level only).
|
||||||
// telemetrySetup is non-nil only when telemetry was successfully initialized
|
|
||||||
// at startup — hot-reloading telemetry-on is out of scope (see ADR-0023).
|
|
||||||
// The callback updates the SamplerType/Ratio on the captured Setup, then
|
|
||||||
// rebuilds the global tracer provider via ReconfigureTracerProvider.
|
|
||||||
if telemetrySetup != nil {
|
|
||||||
s.config.SetSamplerReconfigureCallback(func(ctx context.Context, samplerType string, samplerRatio float64) error {
|
|
||||||
telemetrySetup.SamplerType = samplerType
|
|
||||||
telemetrySetup.SamplerRatio = samplerRatio
|
|
||||||
newTP, rerr := telemetrySetup.ReconfigureTracerProvider(ctx, s.tracerProvider)
|
|
||||||
if rerr != nil {
|
|
||||||
return rerr
|
|
||||||
}
|
|
||||||
if newTP != nil {
|
|
||||||
s.tracerProvider = newTP
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start config hot-reload watcher (ADR-0023 Phase 1+2+3).
|
|
||||||
// Stops automatically on rootCtx cancellation.
|
// Stops automatically on rootCtx cancellation.
|
||||||
s.config.WatchAndApply(rootCtx)
|
s.config.WatchAndApply(rootCtx)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user