Files
cs-note/hzh/GIN/14-logging.md
T

243 lines
6.8 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
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)