Files
ai-customer-service/internal/http/handlers/health_handler.go
Your Name 142b991334 fix(config+app): production fail-fast + readiness收紧
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
2026-05-04 07:38:10 +08:00

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)
}