Files
cs-note/hhs/GIN/14-logging.md
T
2026-05-24 11:42:38 +08:00

243 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)