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