vault backup: 2026-04-28 19:33:43
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user