package app import ( "errors" "log/slog" "time" "github.com/gin-gonic/gin" "galo/internal/auth" "galo/internal/httpx" "galo/internal/logging" ) const ctxRequestID = "logging.request_id" // requestIDMiddleware assigns a request identifier, echoes it in the response // header and stores it in the request context for downstream logging. func requestIDMiddleware() gin.HandlerFunc { return func(c *gin.Context) { id := c.GetHeader("X-Request-ID") if !logging.ValidRequestID(id) { id = logging.NewRequestID() } c.Set(ctxRequestID, id) c.Header("X-Request-ID", id) c.Request = c.Request.WithContext(logging.WithRequestID(c.Request.Context(), id)) c.Next() } } // accessLogMiddleware writes one debug-level line per completed request and // logs collected errors. The query string, Authorization header and bodies are // never logged. func accessLogMiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() if route := c.FullPath(); shouldLogAccess(route) { logRequest(c, start, route) } logErrors(c) } } func shouldLogAccess(route string) bool { return route != "" && route != "/api/health" } func logRequest(c *gin.Context, start time.Time, route string) { slog.Default().LogAttrs(c.Request.Context(), slog.LevelDebug, "http request", slog.String("method", c.Request.Method), slog.String("route", route), slog.Int("status", c.Writer.Status()), slog.Duration("latency", time.Since(start)), slog.Int("bytes", c.Writer.Size()), slog.String("client_ip", c.ClientIP()), slog.String("user_id", auth.UserID(c)), slog.String("request_id", logging.RequestID(c.Request.Context())), ) } func logErrors(c *gin.Context) { for _, ginErr := range c.Errors { level := slog.LevelError var appErr *httpx.AppError if errors.As(ginErr.Err, &appErr) && appErr.Status < 500 { level = slog.LevelDebug } slog.Default().LogAttrs(c.Request.Context(), level, "request error", slog.String("error", ginErr.Err.Error()), slog.String("route", c.FullPath()), slog.String("request_id", logging.RequestID(c.Request.Context())), ) } }