From 107bae528a4b363241e5424a4d6977842d82f720 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Tue, 7 Apr 2026 01:13:08 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20feat:=20enhance=20readiness=20en?= =?UTF-8?q?dpoint=20with=20detailed=20connection=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgraded readiness endpoint to provide per-connection health status - Added structured JSON response with connection details - Database health status now includes explicit healthy/unhealthy/not_configured states - Better observability with detailed failure reasons - Maintains backward compatibility with existing readiness checks Response Examples: ✅ Healthy: {ready: true, connections: {database: {status: healthy}}} ❌ Unhealthy: {ready: false, reason: database_unhealthy, connections: {database: {status: unhealthy, error: ...}}} ❌ Shutting Down: {ready: false, reason: server_shutting_down, connections: {database: not_checked}}} Benefits: - Detailed health information for debugging - Per-connection status (database, future: cache, etc.) - Better Kubernetes/container orchestration integration - Clear failure reasons for troubleshooting - Extensible for additional services Testing: - ✅ Readiness endpoint returns detailed connection status - ✅ Database health properly reflected - ✅ All 25 BDD scenarios passing - ✅ All unit tests passing Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- pkg/server/server.go | 61 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/pkg/server/server.go b/pkg/server/server.go index 4e2163e..4686274 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -24,6 +24,7 @@ import ( userapi "dance-lessons-coach/pkg/user/api" "dance-lessons-coach/pkg/validation" "dance-lessons-coach/pkg/version" + "encoding/json" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -213,12 +214,12 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { // handleReadiness godoc // // @Summary Readiness check -// @Description Check if the service is ready to accept traffic including database connectivity +// @Description Check if the service is ready to accept traffic including detailed connection status // @Tags System/Health // @Accept json // @Produce json -// @Success 200 {object} map[string]bool "Service is ready" -// @Failure 503 {object} map[string]bool "Service is not ready" +// @Success 200 {object} object "Service is ready with connection details" +// @Failure 503 {object} object "Service is not ready with failure details" // @Router /ready [get] func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) { log.Trace().Msg("Readiness check requested") @@ -227,21 +228,61 @@ func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) { select { case <-s.readyCtx.Done(): log.Trace().Msg("Readiness check: not ready (shutting down)") + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte(`{"ready":false}`)) + json.NewEncoder(w).Encode(map[string]interface{}{ + "ready": false, + "reason": "server_shutting_down", + "connections": map[string]interface{}{ + "database": "not_checked", + }, + }) return default: - // Server is not shutting down, check database if available + // Server is not shutting down, check all connections + connectionStatus := make(map[string]interface{}) + allHealthy := true + var failureReason string + + // Check database if available if s.userRepo != nil { if err := s.userRepo.CheckDatabaseHealth(r.Context()); err != nil { log.Warn().Err(err).Msg("Database health check failed") - w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte(`{"ready":false}`)) - return + connectionStatus["database"] = map[string]interface{}{ + "status": "unhealthy", + "error": err.Error(), + } + allHealthy = false + failureReason = "database_unhealthy" + } else { + connectionStatus["database"] = map[string]interface{}{ + "status": "healthy", + } + } + } else { + connectionStatus["database"] = map[string]interface{}{ + "status": "not_configured", } } - log.Trace().Msg("Readiness check: ready") - w.Write([]byte(`{"ready":true}`)) + + if allHealthy { + log.Trace().Msg("Readiness check: ready") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "ready": true, + "connections": connectionStatus, + }) + } else { + log.Warn().Str("reason", failureReason).Msg("Readiness check: not ready") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + json.NewEncoder(w).Encode(map[string]interface{}{ + "ready": false, + "reason": failureReason, + "connections": connectionStatus, + }) + } } }