🔧 feat: enhance readiness endpoint with detailed connection status
- 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:
@@ -24,6 +24,7 @@ import (
|
|||||||
userapi "dance-lessons-coach/pkg/user/api"
|
userapi "dance-lessons-coach/pkg/user/api"
|
||||||
"dance-lessons-coach/pkg/validation"
|
"dance-lessons-coach/pkg/validation"
|
||||||
"dance-lessons-coach/pkg/version"
|
"dance-lessons-coach/pkg/version"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
@@ -213,12 +214,12 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
// handleReadiness godoc
|
// handleReadiness godoc
|
||||||
//
|
//
|
||||||
// @Summary Readiness check
|
// @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
|
// @Tags System/Health
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} map[string]bool "Service is ready"
|
// @Success 200 {object} object "Service is ready with connection details"
|
||||||
// @Failure 503 {object} map[string]bool "Service is not ready"
|
// @Failure 503 {object} object "Service is not ready with failure details"
|
||||||
// @Router /ready [get]
|
// @Router /ready [get]
|
||||||
func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Trace().Msg("Readiness check requested")
|
log.Trace().Msg("Readiness check requested")
|
||||||
@@ -227,21 +228,61 @@ func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
|||||||
select {
|
select {
|
||||||
case <-s.readyCtx.Done():
|
case <-s.readyCtx.Done():
|
||||||
log.Trace().Msg("Readiness check: not ready (shutting down)")
|
log.Trace().Msg("Readiness check: not ready (shutting down)")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
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
|
return
|
||||||
default:
|
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 s.userRepo != nil {
|
||||||
if err := s.userRepo.CheckDatabaseHealth(r.Context()); err != nil {
|
if err := s.userRepo.CheckDatabaseHealth(r.Context()); err != nil {
|
||||||
log.Warn().Err(err).Msg("Database health check failed")
|
log.Warn().Err(err).Msg("Database health check failed")
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
connectionStatus["database"] = map[string]interface{}{
|
||||||
w.Write([]byte(`{"ready":false}`))
|
"status": "unhealthy",
|
||||||
return
|
"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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allHealthy {
|
||||||
log.Trace().Msg("Readiness check: ready")
|
log.Trace().Msg("Readiness check: ready")
|
||||||
w.Write([]byte(`{"ready":true}`))
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user