veza/veza-backend-api/internal/handlers/health.go

288 lines
7.1 KiB
Go

package handlers
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
"veza-backend-api/internal/database"
"veza-backend-api/internal/eventbus"
)
// HealthResponse représente la réponse du health check
type HealthResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Checks map[string]HealthCheck `json:"checks"`
}
// HealthCheck représente le résultat d'un check individuel
type HealthCheck struct {
Status string `json:"status"`
Message string `json:"message,omitempty"`
Duration float64 `json:"duration_ms,omitempty"`
Threshold float64 `json:"threshold_ms,omitempty"`
}
// HealthHandler gère les health checks
type HealthHandler struct {
db *gorm.DB
logger *zap.Logger
redis *redis.Client // Typé avec le vrai type Redis
rabbitMQEventBus *eventbus.RabbitMQEventBus // Instance de l'EventBus RabbitMQ
}
// NewHealthHandler crée un nouveau handler de health
func NewHealthHandler(db *gorm.DB, logger *zap.Logger, redisClient interface{}, rabbitMQEventBus interface{}) *HealthHandler {
h := &HealthHandler{
db: db,
logger: logger,
}
// Type assertion for Redis
if r, ok := redisClient.(*redis.Client); ok {
h.redis = r
}
// Type assertion for RabbitMQ
if eb, ok := rabbitMQEventBus.(*eventbus.RabbitMQEventBus); ok {
h.rabbitMQEventBus = eb
}
return h
}
// NewHealthHandlerSimple crée un nouveau handler de health simple (sans logger/redis)
// Pour compatibilité avec la spécification T0012
func NewHealthHandlerSimple(db *gorm.DB) *HealthHandler {
return &HealthHandler{
db: db,
}
}
// Check vérifie l'état de la base de données et retourne un status simple
// Cette méthode implémente la spécification T0012
// Route /health - Stateless, sans dépendances externes
func (h *HealthHandler) Check(c *gin.Context) {
// Route /health simplifiée - toujours retourner {status: "ok"}
// Stateless, sans vérification de dépendances
RespondSuccess(c, http.StatusOK, gin.H{
"status": "ok",
})
}
// Health check endpoint (/health)
func (h *HealthHandler) Health(c *gin.Context) {
response := HealthResponse{
Status: "ok",
Timestamp: time.Now().UTC().Format(time.RFC3339),
Checks: make(map[string]HealthCheck),
}
// Check database
dbCheck := h.checkDatabase()
response.Checks["database"] = dbCheck
// Check Redis
redisCheck := h.checkRedis()
response.Checks["redis"] = redisCheck
// Check RabbitMQ
rabbitMQCheck := h.checkRabbitMQ()
response.Checks["rabbitmq"] = rabbitMQCheck
// Déterminer le statut global
for _, check := range response.Checks {
if check.Status == "error" {
response.Status = "degraded"
break
}
if check.Status == "slow" {
if response.Status != "degraded" {
response.Status = "degraded"
}
}
}
statusCode := http.StatusOK
if response.Status == "degraded" {
statusCode = http.StatusServiceUnavailable
}
RespondSuccess(c, statusCode, response)
}
// Readiness check endpoint (/ready)
func (h *HealthHandler) Readiness(c *gin.Context) {
response := HealthResponse{
Status: "ready",
Timestamp: time.Now().UTC().Format(time.RFC3339),
Checks: make(map[string]HealthCheck),
}
// Vérifier que la DB est accessible
dbCheck := h.checkDatabase()
response.Checks["database"] = dbCheck
// Vérifier que Redis est accessible
redisCheck := h.checkRedis()
response.Checks["redis"] = redisCheck
// Vérifier que RabbitMQ est accessible (si activé)
rabbitMQCheck := h.checkRabbitMQ()
response.Checks["rabbitmq"] = rabbitMQCheck
// Si un check est en erreur, on n'est pas ready
for _, check := range response.Checks {
if check.Status == "error" {
response.Status = "not_ready"
c.JSON(http.StatusServiceUnavailable, response)
return
}
}
RespondSuccess(c, http.StatusOK, response)
}
// Liveness check endpoint (/live)
func (h *HealthHandler) Liveness(c *gin.Context) {
RespondSuccess(c, http.StatusOK, gin.H{
"status": "alive",
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}
// SimpleHealthCheck est une fonction simple pour le health check endpoint public
func SimpleHealthCheck(c *gin.Context) {
RespondSuccess(c, http.StatusOK, gin.H{
"status": "healthy",
"service": "veza-backend-api",
})
}
// checkDatabase vérifie la connexion à la base de données avec pool stats
func (h *HealthHandler) checkDatabase() HealthCheck {
start := time.Now()
// Utiliser IsConnectionHealthy avec timeout de 5 secondes
err := database.IsConnectionHealthy(h.db, 5*time.Second)
duration := time.Since(start)
if err != nil {
return HealthCheck{
Status: "error",
Message: err.Error(),
Duration: float64(duration.Nanoseconds()) / 1e6,
}
}
threshold := 100.0 // 100ms threshold
status := "ok"
if duration.Milliseconds() > int64(threshold) {
status = "slow"
}
// Récupérer les statistiques du pool
poolStats, statsErr := database.GetPoolStats(h.db)
var message string
if statsErr == nil {
message = "pool_connections"
// On pourrait ajouter plus d'informations sur le pool ici
_ = poolStats // Utiliser dans le futur pour plus de détails
}
return HealthCheck{
Status: status,
Message: message,
Duration: float64(duration.Nanoseconds()) / 1e6, // Convert to ms
Threshold: threshold,
}
}
// checkRedis vérifie la connexion à Redis
func (h *HealthHandler) checkRedis() HealthCheck {
start := time.Now()
threshold := 50.0 // 50ms threshold
if h.redis == nil {
return HealthCheck{
Status: "error",
Message: "Redis connection not configured",
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := h.redis.Ping(ctx).Result()
duration := time.Since(start)
if err != nil {
return HealthCheck{
Status: "error",
Message: err.Error(),
Duration: float64(duration.Nanoseconds()) / 1e6,
}
}
status := "ok"
if duration.Milliseconds() > int64(threshold) {
status = "slow"
}
return HealthCheck{
Status: status,
Duration: float64(duration.Nanoseconds()) / 1e6,
Threshold: threshold,
}
}
// checkRabbitMQ vérifie la connexion à RabbitMQ (Event Bus)
func (h *HealthHandler) checkRabbitMQ() HealthCheck {
start := time.Now()
threshold := 100.0 // 100ms threshold
// Vérifier si l'EventBus est configuré
if h.rabbitMQEventBus == nil {
return HealthCheck{
Status: "error",
Message: "RabbitMQ EventBus not configured",
}
}
// Vérifier si l'EventBus est activé via le champ booléen
if !h.rabbitMQEventBus.IsEnabled {
return HealthCheck{
Status: "disabled",
Message: "RabbitMQ EventBus is disabled by configuration",
}
}
// Tenter un Health Check réel
if err := h.rabbitMQEventBus.Health(); err != nil {
duration := time.Since(start)
return HealthCheck{
Status: "error",
Message: err.Error(),
Duration: float64(duration.Nanoseconds()) / 1e6,
}
}
duration := time.Since(start)
status := "ok"
if duration.Milliseconds() > int64(threshold) {
status = "slow"
}
return HealthCheck{
Status: status,
Duration: float64(duration.Nanoseconds()) / 1e6,
Threshold: threshold,
}
}