第 8 章 · 日志 zap 与访问追踪
本章目标:集成 uber-go/zap 结构化日志;用自定义中间件替换 Gin 默认 Logger;将 request_id、user_id、latency、status 写入 access log;Recovery panic 记录 stack;配置 dev/prod 不同输出;request_id 贯穿 handler、service、日志;对照 fastapi-web ch08 日志与 toolkit-go access 格式。
学时建议:3~4 小时(含 1.5 小时跟练)
前置:完成 gin-web ch07、ch02 RequestID 中间件。
8.1 场景说明:可观测的 access 日志
生产环境需 JSON Lines 日志供 Loki/ELK/阿里云 SLS 检索。每条 HTTP 请求一条 access 记录,且与 X-Request-ID 关联,便于从用户报错追踪到服务端链路。
| 字段 | 来源 | 示例 |
|---|---|---|
request_id | ch02 中间件 | 550e8400-e29b-... |
method | HTTP | GET |
path | URL | /api/v1/products/go-handbook |
status | 响应码 | 200 |
latency_ms | 计时 | 12 |
user_id | JWT Auth(可选) | 1 |
client_ip | c.ClientIP() | 127.0.0.1 |
Request
▼
RequestID() ── 生成/透传 X-Request-ID
▼
AccessLog(zap) ── 记录 access JSON
▼
Recovery(zap) ── panic → 500 + error log
▼
Handler
对照章节:
| 主题 | fastapi-web ch08 | flask-web | gin-web ch08 |
|---|---|---|---|
| 结构化日志 | structlog / logging | app.logger | zap |
| 请求 ID | middleware | 自定义 | RequestID |
| access 格式 | JSON | Werkzeug 文本 | JSON zap |
8.2 逐步操作表
| 步骤 | 操作 | 验证 |
|---|---|---|
| 1 | go get go.uber.org/zap | 依赖就绪 |
| 2 | 创建 internal/logger/logger.go | dev/prod 模式 |
| 3 | 创建 middleware/access_log.go | 每条请求一条 Info |
| 4 | 创建 middleware/recovery_zap.go | panic 有 Error 日志 |
| 5 | router 去掉 gin.Logger() | 无重复 access |
| 6 | main defer log.Sync() | 进程退出 flush |
| 7 | curl 带 X-Request-ID: trace-1 | 日志含 trace-1 |
| 8 | 登录后 POST 商品 | 日志含 user_id |
8.3 目录结构
internal/
├── logger/
│ └── logger.go
└── middleware/
├── request_id.go # ch02
├── access_log.go
└── recovery_zap.go
8.4 初始化 zap
go get go.uber.org/zap@v1.27.0
// internal/logger/logger.go
package logger
import (
"strings"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func New(env, level string) (*zap.Logger, error) {
var cfg zap.Config
if env == "prod" {
cfg = zap.NewProductionConfig()
cfg.Encoding = "json"
} else {
cfg = zap.NewDevelopmentConfig()
cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
switch strings.ToLower(level) {
case "debug":
cfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
case "warn":
cfg.Level = zap.NewAtomicLevelAt(zap.WarnLevel)
case "error":
cfg.Level = zap.NewAtomicLevelAt(zap.ErrorLevel)
default:
cfg.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
}
return cfg.Build()
}
| 模式 | 特点 | 环境 |
|---|---|---|
| Development | 彩色、可读 | dev |
| Production | JSON、采样 | prod |
ch03 LOG_LEVEL 控制 zap level;prod 下 GORM logger 设为 Warn(ch04)。
8.5 Access Log 中间件
// internal/middleware/access_log.go
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func AccessLog(log *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
rid, _ := c.Get("request_id")
uid, _ := c.Get("user_id")
fields := []zap.Field{
zap.Any("request_id", rid),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.Int("status", status),
zap.Int64("latency_ms", latency.Milliseconds()),
zap.String("client_ip", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
}
if uid != nil {
fields = append(fields, zap.Any("user_id", uid))
}
if status >= 500 {
log.Error("access", fields...)
} else if latency > 500*time.Millisecond {
log.Warn("slow request", fields...)
} else {
log.Info("access", fields...)
}
}
}
access log 格式示例(prod JSON):
{"level":"info","ts":1710000000.123,"msg":"access","request_id":"trace-1","method":"GET","path":"/api/v1/products/go-handbook","status":200,"latency_ms":5,"user_id":null}
与 toolkit-go 解析的 JSON Lines 概念相通。
8.6 Recovery 记录 stack
// internal/middleware/recovery_zap.go
package middleware
import (
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"example.com/api-go-demo/internal/response"
)
func RecoveryWithZap(log *zap.Logger) gin.HandlerFunc {
return gin.CustomRecoveryWithWriter(nil, func(c *gin.Context, recovered any) {
rid, _ := c.Get("request_id")
log.Error("panic recovered",
zap.Any("request_id", rid),
zap.Any("error", recovered),
zap.String("path", c.Request.URL.Path),
zap.String("stack", string(debug.Stack())),
)
response.Fail(c, http.StatusInternalServerError, response.CodeInternal, "服务器内部错误")
})
}
| 对比 | gin.Recovery | RecoveryWithZap |
|---|---|---|
| 客户端 | 500 文本或 JSON | envelope 50000 |
| 服务端 | 控制台 stack | zap Error + stack 字段 |