🔧 feat: enhance readiness endpoint with detailed connection status
All checks were successful
CI/CD Pipeline / CI Pipeline (pull_request) Successful in 8m6s
CI/CD Pipeline / CI Pipeline (push) Successful in 10m43s

- 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 <vibe@mistral.ai>
This commit is contained in:
2026-04-07 01:13:08 +02:00
parent 760d1cc8b0
commit 107bae528a

View File

@@ -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,
})
}
}
}