veza/veza-backend-api/internal/middleware/request_logger.go
2026-03-06 19:13:16 +01:00

145 lines
4.2 KiB
Go

package middleware
import (
"os"
"strconv"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// RequestLogger middleware pour logger les requêtes HTTP avec contexte structuré
func RequestLogger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// Début de la requête
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
// Traiter la requête
c.Next()
// Calculer la durée
latency := time.Since(start)
// Récupérer le request ID si présent
requestID, exists := c.Get("request_id")
if !exists {
requestID = ""
}
// Récupérer l'user ID si présent (après authentification)
userID, exists := c.Get("user_id")
if !exists {
userID = nil
}
// Préparer les champs structurés
fields := []zap.Field{
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
zap.Duration("latency", latency),
zap.Int("body_size", c.Writer.Size()),
}
// Ajouter request ID si présent
if requestID != "" {
fields = append(fields, zap.String("request_id", requestID.(string)))
}
// Ajouter user ID si présent
if userID != nil {
fields = append(fields, zap.Any("user_id", userID))
}
// Ajouter le trace_id au logger si disponible (T0025)
if traceID := GetTraceID(c); traceID != "" {
fields = append(fields, zap.String("trace_id", traceID))
}
// Ajouter le span_id au logger si disponible (T0025)
if spanID := GetSpanID(c); spanID != "" {
fields = append(fields, zap.String("span_id", spanID))
}
// Ajouter les erreurs s'il y en a
if len(c.Errors) > 0 {
fields = append(fields, zap.Strings("errors", c.Errors.Errors()))
}
// FIX #9: Détecter les requêtes lentes avec seuil configurable
// Seuil par défaut: 1 seconde (configurable via SLOW_REQUEST_THRESHOLD_MS)
slowThresholdMs := getEnvInt("SLOW_REQUEST_THRESHOLD_MS", 1000) // 1000ms = 1s par défaut
slowThreshold := time.Duration(slowThresholdMs) * time.Millisecond
isSlowRequest := latency > slowThreshold
// Logger selon le status code et la latence
if c.Writer.Status() >= 500 {
// Erreurs serveur
logger.Error("Request completed", fields...)
} else if c.Writer.Status() >= 400 {
// Erreurs client
logger.Warn("Request completed with error", fields...)
} else if isSlowRequest {
// FIX #9: Requêtes lentes (succès mais > seuil)
logger.Warn("Slow request detected",
append(fields,
zap.Duration("slow_threshold", slowThreshold),
zap.Bool("is_slow", true),
)...,
)
} else {
// Succès normal
logger.Info("Request completed", fields...)
}
}
}
// WithRequestID returns zap fields for request_id from context (TASK-DEBT-011).
// Use in handlers: logger.Info("msg", middleware.WithRequestID(c)...)
func WithRequestID(c *gin.Context) []zap.Field {
if id, exists := c.Get("request_id"); exists && id != "" {
return []zap.Field{zap.String("request_id", id.(string))}
}
return nil
}
// WithUserID returns zap fields for user_id from context (TASK-DEBT-011).
// Use in handlers: logger.Info("msg", middleware.WithUserID(c)...)
func WithUserID(c *gin.Context) []zap.Field {
if id, exists := c.Get("user_id"); exists && id != nil {
return []zap.Field{zap.Any("user_id", id)}
}
return nil
}
// WithRequestContext returns zap fields for request_id and user_id (TASK-DEBT-011).
// Use in handlers for structured logging: logger.Info("msg", middleware.WithRequestContext(c)...)
func WithRequestContext(c *gin.Context) []zap.Field {
var fields []zap.Field
if id, exists := c.Get("request_id"); exists && id != "" {
fields = append(fields, zap.String("request_id", id.(string)))
}
if id, exists := c.Get("user_id"); exists && id != nil {
fields = append(fields, zap.Any("user_id", id))
}
return fields
}
// getEnvInt récupère une variable d'environnement entière avec une valeur par défaut
// FIX #9: Helper pour lire SLOW_REQUEST_THRESHOLD_MS
func getEnvInt(key string, defaultValue int) int {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
return defaultValue
}