veza/veza-backend-api/internal/handlers/health.go
okinrev b7955a680c P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.

Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.

Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).

Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.

Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 11:14:38 +01:00

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
c.JSON(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
}
c.JSON(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
}
}
c.JSON(http.StatusOK, response)
}
// Liveness check endpoint (/live)
func (h *HealthHandler) Liveness(c *gin.Context) {
c.JSON(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) {
c.JSON(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,
}
}