Logging estructurado
El logging estructurado produce una salida de logs legible por máquinas (típicamente JSON) en lugar de texto plano. Esto hace que los logs sean más fáciles de buscar, filtrar y analizar en sistemas de agregación de logs como ELK Stack, Datadog o Grafana Loki.
Por qué logging estructurado
El logger predeterminado de Gin produce texto legible por humanos, que es excelente para desarrollo pero difícil de analizar a escala. El logging estructurado te da:
- Campos consultables — filtrar logs por código de estado, latencia o ID de usuario
- Formato consistente — cada entrada de log tiene la misma forma
- Correlación — rastrear una solicitud a través de servicios usando IDs de solicitud
Usando slog (Go 1.21+)
La biblioteca estándar de Go incluye log/slog para logging estructurado. Así es como usarlo con Gin:
package main
import ( "log/slog" "os" "time"
"github.com/gin-gonic/gin")
func SlogMiddleware(logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() path := c.Request.URL.Path query := c.Request.URL.RawQuery
c.Next()
logger.Info("request", slog.String("method", c.Request.Method), slog.String("path", path), slog.String("query", query), slog.Int("status", c.Writer.Status()), slog.Duration("latency", time.Since(start)), slog.String("client_ip", c.ClientIP()), slog.Int("body_size", c.Writer.Size()), )
if len(c.Errors) > 0 { for _, err := range c.Errors { logger.Error("request error", slog.String("error", err.Error())) } } }}
func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := gin.New() r.Use(SlogMiddleware(logger)) r.Use(gin.Recovery())
r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) })
r.Run(":8080")}Esto produce entradas de log como:
{"time":"2024-01-15T10:30:00Z","level":"INFO","msg":"request","method":"GET","path":"/ping","query":"","status":200,"latency":"125µs","client_ip":"127.0.0.1","body_size":18}ID de solicitud / ID de correlación
Agregar un ID de solicitud único a cada entrada de log ayuda a rastrear solicitudes a través de servicios:
package main
import ( "log/slog" "os" "time"
"github.com/gin-gonic/gin" "github.com/google/uuid")
func RequestIDMiddleware() gin.HandlerFunc { return func(c *gin.Context) { requestID := c.GetHeader("X-Request-ID") if requestID == "" { requestID = uuid.New().String() } c.Set("request_id", requestID) c.Header("X-Request-ID", requestID) c.Next() }}
func SlogMiddleware(logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { start := time.Now()
c.Next()
requestID, _ := c.Get("request_id") logger.Info("request", slog.String("request_id", requestID.(string)), slog.String("method", c.Request.Method), slog.String("path", c.Request.URL.Path), slog.Int("status", c.Writer.Status()), slog.Duration("latency", time.Since(start)), ) }}
func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := gin.New() r.Use(RequestIDMiddleware()) r.Use(SlogMiddleware(logger)) r.Use(gin.Recovery())
r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) })
r.Run(":8080")}Usando zerolog
zerolog es un popular logger JSON sin asignaciones de memoria:
package main
import ( "os" "time"
"github.com/gin-gonic/gin" "github.com/rs/zerolog" "github.com/rs/zerolog/log")
func ZerologMiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now()
c.Next()
log.Info(). Str("method", c.Request.Method). Str("path", c.Request.URL.Path). Int("status", c.Writer.Status()). Dur("latency", time.Since(start)). Str("client_ip", c.ClientIP()). Msg("request") }}
func main() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
// Pretty logging for development if gin.Mode() == gin.DebugMode { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) }
r := gin.New() r.Use(ZerologMiddleware()) r.Use(gin.Recovery())
r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) })
r.Run(":8080")}Mejores prácticas
- Usa formato JSON en producción — formato legible por humanos para desarrollo, JSON para agregación de logs en producción
- Incluye IDs de solicitud — propaga los encabezados
X-Request-IDa través de los límites de servicios para rastreo distribuido - Registra en los niveles apropiados — usa
Infopara solicitudes normales,Warnpara 4xx,Errorpara 5xx - Evita registrar datos sensibles — excluye contraseñas, tokens y PII de la salida de logs
- Configura
GIN_MODE=release— esto deshabilita el logging de depuración predeterminado de Gin en producción