package handler import ( "context" "errors" "net/http" "runtime" "strconv" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type DependencyCheck struct { Name string Check func(context.Context) error } // HealthResponse is the structured health check response. type HealthResponse struct { Status string `json:"status"` Timestamp string `json:"timestamp"` Version string `json:"version,omitempty"` Uptime string `json:"uptime,omitempty"` Checks map[string]string `json:"checks"` } // HealthHandler reports dependency state and optional runtime diagnostics. func HealthHandler(db *gorm.DB, startTime time.Time, version string, dependencies ...DependencyCheck) gin.HandlerFunc { return func(c *gin.Context) { selected := c.Query("check") checks, healthy := runDependencyChecks(c.Request.Context(), db, selected, dependencies) uptime := time.Since(startTime) if selected == "" { var memory runtime.MemStats runtime.ReadMemStats(&memory) checks["memory_alloc_mb"] = strconv.FormatUint(memory.Alloc/1024/1024, 10) checks["goroutines"] = strconv.Itoa(runtime.NumGoroutine()) checks["uptime_seconds"] = strconv.FormatUint(uint64(uptime.Seconds()), 10) } status, statusCode := "healthy", http.StatusOK if !healthy { status, statusCode = "unhealthy", http.StatusServiceUnavailable } c.JSON(statusCode, HealthResponse{ Status: status, Timestamp: time.Now().UTC().Format(time.RFC3339), Version: version, Uptime: uptime.String(), Checks: checks, }) } } // ReadyHandler returns 503 while draining or when a required dependency fails. func ReadyHandler(db *gorm.DB, dependencies ...DependencyCheck) gin.HandlerFunc { return func(c *gin.Context) { checks, ready := runDependencyChecks(c.Request.Context(), db, "", dependencies) statusCode := http.StatusOK if !ready { statusCode = http.StatusServiceUnavailable } c.JSON(statusCode, gin.H{"ready": ready, "checks": checks}) } } // LiveHandler only proves that the process can serve HTTP; dependency failures // belong to readiness so an outage does not trigger a restart loop. func LiveHandler() gin.HandlerFunc { return func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"alive": true}) } } func runDependencyChecks(ctx context.Context, db *gorm.DB, selected string, dependencies []DependencyCheck) (map[string]string, bool) { checks := make(map[string]string, len(dependencies)+1) healthy := true all := append([]DependencyCheck{{Name: "database", Check: databasePing(db)}}, dependencies...) found := selected == "" for _, dependency := range all { if selected != "" && dependency.Name != selected { continue } found = true if dependency.Check == nil { checks[dependency.Name] = "unhealthy: check is not configured" healthy = false continue } if err := dependency.Check(ctx); err != nil { checks[dependency.Name] = "unhealthy: " + err.Error() healthy = false } else { checks[dependency.Name] = "healthy" } } if !found { checks[selected] = "unhealthy: unknown check" healthy = false } return checks, healthy } func databasePing(db *gorm.DB) func(context.Context) error { return func(ctx context.Context) error { if db == nil { return errors.New("database is not configured") } sqlDB, err := db.DB() if err != nil { return err } return sqlDB.PingContext(ctx) } }