package middleware import ( "log/slog" "net/http" "time" "github.com/gin-gonic/gin" ) // RequestLoggerConfig holds the configuration for the request logger. type RequestLoggerConfig struct { SkipPaths []string } // DefaultRequestLoggerConfig returns the default request logger configuration. func DefaultRequestLoggerConfig() *RequestLoggerConfig { return &RequestLoggerConfig{ SkipPaths: []string{"/health", "/ready", "/metrics"}, } } // Logger returns a Gin middleware handler for logging requests. func Logger() gin.HandlerFunc { return LoggerWithConfig(DefaultRequestLoggerConfig()) } // LoggerWithConfig returns a Gin middleware handler for structured // request logging via slog. When LOG_FORMAT=json the output is // machine-parseable JSON lines; otherwise human-readable key=value. func LoggerWithConfig(config *RequestLoggerConfig) gin.HandlerFunc { skip := make(map[string]bool, len(config.SkipPaths)) for _, p := range config.SkipPaths { skip[p] = true } return func(c *gin.Context) { path := c.Request.URL.Path if skip[path] { c.Next() return } start := time.Now() c.Next() reqID, _ := c.Get(RequestIDKey) attrs := []any{ "method", c.Request.Method, "path", path, "status", c.Writer.Status(), "latency_ms", time.Since(start).Milliseconds(), "client_ip", c.ClientIP(), } if id, ok := reqID.(string); ok && id != "" { attrs = append(attrs, "request_id", id) } if uid := c.GetString("user_id"); uid != "" { attrs = append(attrs, "user_id", uid) } if errs := c.Errors.ByType(gin.ErrorTypePrivate).String(); errs != "" { attrs = append(attrs, "errors", errs) } slog.Info("request", attrs...) } } // Recovery returns a Gin middleware handler for recovering from panics. func Recovery() gin.HandlerFunc { return RecoveryWithConfig(DefaultRecoveryConfig()) } // RecoveryConfig holds the configuration for the recovery middleware. type RecoveryConfig struct { SkipStackTrace bool StackTraceSize int StackAll bool } // DefaultRecoveryConfig returns the default recovery configuration. func DefaultRecoveryConfig() *RecoveryConfig { return &RecoveryConfig{ SkipStackTrace: false, StackTraceSize: 1024, StackAll: false, } } // RecoveryWithConfig returns a Gin middleware handler for recovering // from panics with custom configuration. func RecoveryWithConfig(config *RecoveryConfig) gin.HandlerFunc { return func(c *gin.Context) { defer func() { if err := recover(); err != nil { reqID, _ := c.Get(RequestIDKey) slog.Error("panic", "error", err, "method", c.Request.Method, "path", c.Request.URL.Path, "request_id", reqID, ) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ "error": "internal server error", }) } }() c.Next() } }