package handler import ( "net/http" "runtime" "strconv" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // 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 returns application health status. // Reference: Chatwoot uses /health for monitoring in docker-compose.production.yaml func HealthHandler(db *gorm.DB, startTime time.Time, version string) gin.HandlerFunc { return func(c *gin.Context) { checks := make(map[string]string) overall := "healthy" // Database check sqlDB, err := db.DB() if err != nil { checks["database"] = "unhealthy: " + err.Error() overall = "unhealthy" } else if err := sqlDB.Ping(); err != nil { checks["database"] = "unhealthy: " + err.Error() overall = "unhealthy" } else { checks["database"] = "healthy" } // Memory check var m runtime.MemStats runtime.ReadMemStats(&m) memMB := m.Alloc / 1024 / 1024 checks["memory_alloc_mb"] = strconv.FormatUint(memMB, 10) if memMB > 500 { checks["memory_warning"] = "high memory usage" } // Goroutine check goroutines := runtime.NumGoroutine() checks["goroutines"] = strconv.Itoa(goroutines) if goroutines > 1000 { checks["goroutine_warning"] = "high goroutine count" } // Uptime uptime := time.Since(startTime) checks["uptime_seconds"] = strconv.FormatUint(uint64(uptime.Seconds()), 10) statusCode := http.StatusOK if overall == "unhealthy" { statusCode = http.StatusServiceUnavailable } c.JSON(statusCode, HealthResponse{ Status: overall, Timestamp: time.Now().UTC().Format(time.RFC3339), Version: version, Uptime: uptime.String(), Checks: checks, }) } } // ReadyHandler returns whether the app is ready to accept traffic. // Used by K8s readiness probes — returns 503 if not ready. func ReadyHandler(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { sqlDB, err := db.DB() if err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"ready": false, "reason": "db error"}) return } if err := sqlDB.Ping(); err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"ready": false, "reason": "db unreachable"}) return } c.JSON(http.StatusOK, gin.H{"ready": true}) } } // LiveHandler returns whether the app process is alive. // Used by K8s liveness probes — simplest possible check. func LiveHandler() gin.HandlerFunc { return func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"alive": true}) } }