diff --git a/cmd/api/main.go b/cmd/api/main.go index 7d9132e..584a215 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -73,8 +73,9 @@ func main() { authHandlers := httpInfra.NewAuthHandlers(registerHandler, loginHandler, jwtService) habitHandlers := httpInfra.NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler) statsHandlers := httpInfra.NewStatsHandlers(getHabitStatsHandler) + healthHandlers := httpInfra.NewHealthHandlers(db.Conn()) - router := httpInfra.NewRouter(cfg.CORSOrigins, habitHandlers, authHandlers, statsHandlers, jwtService) + router := httpInfra.NewRouter(cfg.CORSOrigins, habitHandlers, authHandlers, statsHandlers, healthHandlers, jwtService) addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.Port) log.Printf("Server starting on %s", addr) diff --git a/internal/infrastructure/http/health_handlers.go b/internal/infrastructure/http/health_handlers.go new file mode 100644 index 0000000..c40bb08 --- /dev/null +++ b/internal/infrastructure/http/health_handlers.go @@ -0,0 +1,87 @@ +package http + +import ( + "database/sql" + "net/http" + "time" +) + +var startTime = time.Now() + +type HealthHandlers struct { + db *sql.DB +} + +func NewHealthHandlers(db *sql.DB) *HealthHandlers { + return &HealthHandlers{ + db: db, + } +} + +type HealthResponse struct { + Status string `json:"status"` + Database string `json:"database"` + Uptime string `json:"uptime"` +} + +// Health godoc +// @Summary Health check +// @Description Get API health status including database connectivity and uptime +// @Tags system +// @Produce json +// @Success 200 {object} HealthResponse +// @Failure 503 {object} HealthResponse +// @Router /health [get] +func (h *HealthHandlers) Health(w http.ResponseWriter, r *http.Request) { + dbStatus := "ok" + overallStatus := "ok" + statusCode := http.StatusOK + + if err := h.db.Ping(); err != nil { + dbStatus = "error" + overallStatus = "degraded" + statusCode = http.StatusServiceUnavailable + } + + uptime := time.Since(startTime) + uptimeStr := formatDuration(uptime) + + response := HealthResponse{ + Status: overallStatus, + Database: dbStatus, + Uptime: uptimeStr, + } + + respondJSON(w, statusCode, response) +} + +func formatDuration(d time.Duration) string { + d = d.Round(time.Second) + h := d / time.Hour + d -= h * time.Hour + m := d / time.Minute + d -= m * time.Minute + s := d / time.Second + + if h > 0 { + return formatTime(int(h), "h") + formatTime(int(m), "m") + } + if m > 0 { + return formatTime(int(m), "m") + formatTime(int(s), "s") + } + return formatTime(int(s), "s") +} + +func formatTime(value int, unit string) string { + if value == 0 { + return "" + } + return formatInt(value) + unit +} + +func formatInt(n int) string { + if n < 10 { + return "0" + string(rune('0'+n)) + } + return string(rune('0'+n/10)) + string(rune('0'+n%10)) +} diff --git a/internal/infrastructure/http/router.go b/internal/infrastructure/http/router.go index 3f6b5c2..9acee64 100644 --- a/internal/infrastructure/http/router.go +++ b/internal/infrastructure/http/router.go @@ -15,7 +15,7 @@ import ( _ "apocapoc-api/docs" ) -func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *AuthHandlers, statsHandlers *StatsHandlers, jwtService *auth.JWTService) *chi.Mux { +func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *AuthHandlers, statsHandlers *StatsHandlers, healthHandlers *HealthHandlers, jwtService *auth.JWTService) *chi.Mux { r := chi.NewRouter() r.Use(middleware.Logger) @@ -34,10 +34,7 @@ func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *A httpSwagger.URL("/api/v1/docs/doc.json"), )) - r.Get("/api/v1/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"ok"}`)) - }) + r.Get("/api/v1/health", healthHandlers.Health) r.Route("/api/v1/auth", func(r chi.Router) { r.Use(httprate.LimitByIP(10, 1*time.Minute))