288 lines
7.1 KiB
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,
|
|
}
|
|
}
|