--- tags: [后端, Go, Gin, 日志, 监控] create time: 2026-04-28 00:00 --- # 日志系统 ## 概述 Gin 的日志机制建立在两个核心概念之上:默认日志器的可替换性(`DefaultWriter` / `DefaultErrorWriter`)和中间件的灵活组合。生产环境中通常需要接入结构化日志库,自定义日志格式,并支持按路径跳过某些路由的日志记录。 思考题:`gin.Default()` 内部的 Logger 中间件会把请求体打印到日志吗?如果会,这对性能有什么影响? ## 正文 ### 1. Gin 内置 Logger `gin.Default()` 内置了 `Logger()` 中间件——每处理一个请求,在终端输出类似这样的日志: ``` [GIN] 2026/04/27 - 10:30:00 | 200 | 2.345ms | 127.0.0.1 | GET "/api/users" ``` **输出字段含义:** | 字段 | 说明 | |------|------| | `[GIN]` | 前缀标识 | | `2026/04/27 - 10:30:00` | 时间戳 | | `200` | HTTP 状态码 | | `2.345ms` | 请求耗时 | | `127.0.0.1` | 客户端 IP | | `GET "/api/users"` | HTTP 方法 + 路径 | ```go // gin/logger.go — 简化版实现 func Logger() HandlerFunc { return func(c *Context) { start := time.Now() path := c.Request.URL.Path query := c.Request.URL.RawQuery c.Next() latency := time.Since(start) status := c.Writer.Status() log.Printf("[%s] %d %s %s %s", start.Format(time.RFC3339), status, latency, c.ClientIP(), c.Request.Method+" "+path, ) } } ``` > **提问:** Gin 的默认日志直接写到 `os.Stdout`,那如果你想把日志输出到文件而不是终端,应该怎么做? ### 2. 自定义日志输出目标 通过修改 `DefaultWriter` 和 `DefaultErrorWriter`: ```go import ( "os" "gopkg.in/natefinber/lumberjack.v2" // 滚动日志 ) func main() { // 创建滚动日志文件 logFile := &lumberjack.Logger{ Filename: "./logs/gin.log", MaxSize: 100, // MB MaxBackups: 30, // 最多保留 30 个备份 Compress: true, // gzip 压缩 } // 重写默认输出 gin.DefaultWriter = io.MultiWriter(os.Stdout, logFile) gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, logFile) r := gin.Default() // Logger 内部使用 DefaultWriter r.Run(":8080") } ``` ### 3. 接入结构化日志(Zap / Logrus) 用结构化日志库替换 Gin 内置的简单 logger: ```go func structuredLogger(logger *zap.Logger) gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() path := c.Request.URL.Path query := c.Request.URL.RawQuery body := "" // ⚠️ 读取 body 会影响性能,通常只记录大 body 或错误请求 if c.Request.ContentLength > 0 && shouldLogBody(c) { raw, _ := io.ReadAll(io.LimitReader(c.Request.Body, 4096)) c.Request.Body = io.NopCloser(bytes.NewBuffer(raw)) body = string(raw) } c.Next() latency := time.Since(start) fields := zap.Fields( 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.Request.UserAgent()), zap.Int("body_size", c.Request.ContentLength), ) if len(c.Errors) > 0 { fields = append(fields, zap.Strings("errors", c.Errors.ToStrings())) } if c.Writer.Status() >= 500 { logger.Error("request error", fields...) } else { logger.Info("request", fields...) } } } ``` 注册方式: ```go logger, _ := zap.NewProduction() defer logger.Sync() r := gin.New() r.Use(structuredLogger(logger)) r.Use(gin.Recovery()) ``` > **关键细节:** 在日志中间件中读 `c.Request.Body` 会消耗 body,后续 handler 就再也读不到了。所以必须先读完再重新赋值 `c.Request.Body = io.NopCloser(...)`。 ### 4. 跳过特定路径的日志 并非所有请求都需要记录——静态资源、健康检查等高频请求会产生大量噪音: ```go func skipLogging() gin.HandlerFunc { skipPaths := map[string]bool{ "/health": true, "/ready": true, "/metrics": true, "/favicon.ico": true, } return func(c *gin.Context) { if skipPaths[c.Request.URL.Path] { c.Next() return } // 走正常日志流程 c.Next() latency := time.Since(start) log.Printf("%s %s %d %v", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), latency) } } ``` 或者更简单地——让路由不经过 Logger 中间件(将日志中间件挂载到特定分组而非全局): ```go r := gin.New() // 不挂全局,只挂到 API 分组 api := r.Group("/api") api.Use(Logger()) { api.GET("/users", listUsers) // 有日志 api.POST("/users", createUser) // 有日志 } r.GET("/health", healthCheck) // 无日志(不在 api 分组下) r.Static("/static", "./static") // 无日志 ``` ### 5. 控制日志颜色和格式 ```go // 关闭彩色输出(适合日志采集系统) gin.DisableConsoleColor() // 修改日期格式 gin.DebugPrint(func(format string, args ...interface{}) { logger.Printf("[DEBUG] "+format, args...) }) // 完全禁用 Debug 输出 gin.SetMode(gin.ReleaseMode) // 隐藏 DebugPrint ``` ### 6. 路由日志格式定制 如果需要不同的日志格式(如 JSON),可以自建 middleware: ```go func jsonRouterLogger() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() entry := map[string]interface{}{ "timestamp": time.Now().Format(time.RFC3339), "method": c.Request.Method, "path": c.Request.URL.Path, "query": c.Request.URL.RawQuery, "status": c.Writer.Status(), "latency_ms": time.Since(start).Milliseconds(), "client_ip": c.ClientIP(), "bytes_in": c.Request.ContentLength, "bytes_out": c.Writer.Size(), } // 序列化输出 JSON 日志行 b, _ := json.Marshal(entry) os.Stdout.Write(b) os.Stdout.WriteString("\n") } } ``` 思考题:如果你的服务跑在 Kubernetes 中,日志输出到 stdout 后被 sidecar(如 Fluent Bit)收集,你觉得需要手动做 JSON 序列化吗?还是可以用 K8s 生态已有的方案? ## 关联笔记 - [[GIN/3-middleware]] — 中间件注册位置和 Skip 模式 - [[GIN/18-observability]] — 结合 Prometheus metrics 的全面监控方案 - [[部署与运维基础]] — 容器化环境中的日志采集(stdout → fluentd/fluent-bit)