日志中间件

日志是排查问题的重要依据,Gin 内置了日志中间件,也支持自定义日志格式。

内置 Logger 中间件

gin.Default() 默认包含 Logger 中间件:

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    
    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello")
    })
    
    r.Run(":8080")
}

输出格式:

[GIN] 2024/01/15 - 10:30:00 | 200 |    1.234ms | 127.0.0.1 | GET      "/hello"

自定义日志格式

使用 gin.LoggerWithFormatter 自定义格式:

func main() {
    r := gin.New()
    
    r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        return fmt.Sprintf("[%s] %s %s %d %s %s\n",
            param.TimeStamp.Format("2006-01-02 15:04:05"),
            param.ClientIP,
            param.Method,
            param.StatusCode,
            param.Path,
            param.Latency,
        )
    }))
    
    r.Use(gin.Recovery())
    
    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello")
    })
    
    r.Run(":8080")
}

写入文件

将日志写入文件:

func main() {
    r := gin.New()
    
    file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    
    gin.DefaultWriter = io.MultiWriter(file, os.Stdout)
    
    r.Use(gin.Logger())
    r.Use(gin.Recovery())
    
    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello")
    })
    
    r.Run(":8080")
}

自定义日志中间件

更灵活的日志中间件:

func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        query := c.Request.URL.RawQuery
        
        c.Next()
        
        end := time.Now()
        latency := end.Sub(start)
        
        logEntry := fmt.Sprintf(
            "[%s] %s %s %s %d %v %s",
            end.Format("2006-01-02 15:04:05"),
            c.ClientIP(),
            c.Request.Method,
            path,
            c.Writer.Status(),
            latency,
            query,
        )
        
        if len(c.Errors) > 0 {
            logEntry += " errors: " + c.Errors.String()
        }
        
        if c.Writer.Status() >= 400 {
            log.Printf("[ERROR] %s", logEntry)
        } else {
            log.Printf("[INFO] %s", logEntry)
        }
    }
}

r.Use(Logger())

结构化日志

使用 JSON 格式的结构化日志:

type LogEntry struct {
    Time      string `json:"time"`
    Method    string `json:"method"`
    Path      string `json:"path"`
    Status    int    `json:"status"`
    Latency   string `json:"latency"`
    ClientIP  string `json:"client_ip"`
    UserAgent string `json:"user_agent"`
}

func JSONLogger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        
        c.Next()
        
        entry := LogEntry{
            Time:      time.Now().Format(time.RFC3339),
            Method:    c.Request.Method,
            Path:      c.Request.URL.Path,
            Status:    c.Writer.Status(),
            Latency:   time.Since(start).String(),
            ClientIP:  c.ClientIP(),
            UserAgent: c.GetHeader("User-Agent"),
        }
        
        data, _ := json.Marshal(entry)
        log.Println(string(data))
    }
}

r.Use(JSONLogger())

使用第三方日志库

集成 zap 日志库:

import "go.uber.org/zap"

func ZapLogger(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        query := c.Request.URL.RawQuery
        
        c.Next()
        
        latency := time.Since(start)
        
        logger.Info("request",
            zap.String("method", c.Request.Method),
            zap.String("path", path),
            zap.String("query", query),
            zap.Int("status", c.Writer.Status()),
            zap.Duration("latency", latency),
            zap.String("client_ip", c.ClientIP()),
            zap.String("user_agent", c.GetHeader("User-Agent")),
        )
    }
}

func main() {
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    
    r := gin.New()
    r.Use(ZapLogger(logger))
    r.Use(gin.Recovery())
    
    r.Run(":8080")
}

跳过特定路径

某些路径不需要记录日志:

func SkipLogger(skipPaths []string) gin.HandlerFunc {
    return func(c *gin.Context) {
        path := c.Request.URL.Path
        
        for _, skip := range skipPaths {
            if path == skip {
                c.Next()
                return
            }
        }
        
        start := time.Now()
        c.Next()
        
        log.Printf("[%s] %s %s - %d - %v",
            start.Format("2006-01-02 15:04:05"),
            c.Request.Method,
            path,
            c.Writer.Status(),
            time.Since(start),
        )
    }
}

r.Use(SkipLogger([]string{"/health", "/metrics"}))

小结

日志中间件是每个项目必备的。Gin 内置的 Logger 基本够用,但生产环境通常需要自定义格式或集成第三方日志库。记得设置合适的日志级别,避免日志过多影响性能。