6.8 KiB
6.8 KiB
tags, create time
| tags | 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 方法 + 路径 |
// 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:
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:
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...)
}
}
}
注册方式:
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. 跳过特定路径的日志
并非所有请求都需要记录——静态资源、健康检查等高频请求会产生大量噪音:
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 中间件(将日志中间件挂载到特定分组而非全局):
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. 控制日志颜色和格式
// 关闭彩色输出(适合日志采集系统)
gin.DisableConsoleColor()
// 修改日期格式
gin.DebugPrint(func(format string, args ...interface{}) {
logger.Printf("[DEBUG] "+format, args...)
})
// 完全禁用 Debug 输出
gin.SetMode(gin.ReleaseMode) // 隐藏 DebugPrint
6. 路由日志格式定制
如果需要不同的日志格式(如 JSON),可以自建 middleware:
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)