1. config.go: AI_CS_ENV runtime mode with production restriction - New RuntimeConfig.Env field (AI_CS_ENV / AI_CS_RUNTIME_ENV) - production + Postgres.Enabled=false → Load() returns error - production + empty webhook secret → Load() returns error - normalizeRuntimeEnv: dev/dev/ → development, prod/production → production, test → test 2. app.go: probe.SetReady only when store is confirmed ready - Postgres.Enabled: probe.SetReady(true) after DB+migration OK - Memory mode: probe.SetReady(false) — not production-ready 3. health_handler_test.go: add probe live+ready state transition tests 4. config_test.go: add TestLoad_RejectsProdWhenPostgresDisabled, TestLoad_RejectsProdWhenWebhookSecretMissing 5. app_test.go: add TestNew_RejectsMemoryModeWithoutExplicitNonProdEnv, TestNew_AllowsMemoryModeInTestEnv, TestNew_WithPostgresEnabled_* for invalid DSN and migration-failure paths Phase 1 (code gate) objectives met: ✅ prod cannot fall back to memory store ✅ readiness reflects actual store readiness ✅ both changes have test coverage
71 lines
2.0 KiB
Go
71 lines
2.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/bridge/ai-customer-service/internal/platform/health"
|
|
)
|
|
|
|
type HealthHandler struct {
|
|
probe *health.Probe
|
|
checkers []health.Checker
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewHealthHandler(probe *health.Probe, checkers ...health.Checker) *HealthHandler {
|
|
return &HealthHandler{probe: probe, checkers: checkers, now: time.Now}
|
|
}
|
|
|
|
func (h *HealthHandler) Live(w http.ResponseWriter, _ *http.Request) {
|
|
status := http.StatusOK
|
|
payload := map[string]any{"status": "UP"}
|
|
if h.probe != nil && !h.probe.IsLive() {
|
|
status = http.StatusServiceUnavailable
|
|
payload["status"] = "DOWN"
|
|
}
|
|
writeJSON(w, status, payload)
|
|
}
|
|
|
|
func (h *HealthHandler) Ready(w http.ResponseWriter, r *http.Request) {
|
|
ok, checks := h.evaluate(r.Context())
|
|
if h.probe != nil && !h.probe.IsReady() {
|
|
ok = false
|
|
checks = append([]health.CheckResult{{Name: "startup", Status: "DOWN", Error: "service not ready to receive traffic"}}, checks...)
|
|
}
|
|
if h.probe != nil {
|
|
h.probe.SetReady(ok)
|
|
}
|
|
if !ok {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"status": "DOWN", "checks": checks})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "UP", "checks": checks})
|
|
}
|
|
|
|
func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) {
|
|
ok, checks := h.evaluate(r.Context())
|
|
status := "UP"
|
|
if !ok {
|
|
status = "DEGRADED"
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": status, "checks": checks, "time": h.now().UTC().Format(time.RFC3339)})
|
|
}
|
|
|
|
func (h *HealthHandler) evaluate(ctx context.Context) (bool, []health.CheckResult) {
|
|
if h.probe != nil && !h.probe.IsLive() {
|
|
return false, []health.CheckResult{{Name: "liveness", Status: "DOWN", Error: "server stopping"}}
|
|
}
|
|
checkCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
|
defer cancel()
|
|
return health.Evaluate(checkCtx, h.checkers)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(payload)
|
|
}
|