package health import ( "context" "encoding/json" "net/http" "time" "code.nochebuena.dev/einherjar/contracts/logging" "code.nochebuena.dev/einherjar/contracts/observability" ) type checkResult struct { name string status ComponentStatus priority observability.Level } type handler struct { logger logging.Logger checks []observability.Checkable timeout time.Duration } // NewHandler returns an http.Handler for the health endpoint with default configuration. // Runs all checks concurrently within a 5-second timeout. // Returns 200 (UP / DEGRADED) or 503 (DOWN). func NewHandler(logger logging.Logger, checks ...observability.Checkable) http.Handler { return NewHandlerWithConfig(logger, Config{}, checks...) } // NewHandlerWithConfig returns an http.Handler configured by cfg. // If cfg.CheckTimeout is zero the default (5 seconds) is applied. func NewHandlerWithConfig(logger logging.Logger, cfg Config, checks ...observability.Checkable) http.Handler { timeout := cfg.CheckTimeout if timeout == 0 { timeout = defaultCheckTimeout } return &handler{logger: logger, checks: checks, timeout: timeout} } func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { reqLogger := h.logger.WithContext(r.Context()) reqLogger.Debug("health: running checks", "count", len(h.checks)) ctx, cancel := context.WithTimeout(r.Context(), h.timeout) defer cancel() resChan := make(chan checkResult, len(h.checks)) for _, chk := range h.checks { go func(c observability.Checkable) { start := time.Now() err := c.HealthCheck(ctx) latency := time.Since(start).String() status := "UP" errMsg := "" if err != nil { errMsg = err.Error() if c.Priority() == observability.LevelDegraded { status = "DEGRADED" } else { status = "DOWN" } } resChan <- checkResult{ name: c.Name(), priority: c.Priority(), status: ComponentStatus{ Status: status, Latency: latency, Error: errMsg, }, } }(chk) } overallStatus := "UP" httpStatus := http.StatusOK components := make(map[string]ComponentStatus, len(h.checks)) for range h.checks { res := <-resChan components[res.name] = res.status switch res.status.Status { case "DOWN": overallStatus = "DOWN" httpStatus = http.StatusServiceUnavailable case "DEGRADED": if overallStatus == "UP" { overallStatus = "DEGRADED" } } } resp := Response{Status: overallStatus, Components: components} w.Header().Set("Content-Type", "application/json") w.WriteHeader(httpStatus) if err := json.NewEncoder(w).Encode(resp); err != nil { reqLogger.Error("health: failed to encode response", err) } }