下载工作台
Gin Web 开发

日志 zap 与访问追踪

试读上半部分 · 解锁后可读全文

第 8 章 · 日志 zap 与访问追踪

本章目标:集成 uber-go/zap 结构化日志;用自定义中间件替换 Gin 默认 Logger;将 request_iduser_idlatencystatus 写入 access log;Recovery panic 记录 stack;配置 dev/prod 不同输出;request_id 贯穿 handler、service、日志;对照 fastapi-web ch08 日志与 toolkit-go access 格式。

学时建议:3~4 小时(含 1.5 小时跟练)

前置:完成 gin-web ch07ch02 RequestID 中间件。


8.1 场景说明:可观测的 access 日志

生产环境需 JSON Lines 日志供 Loki/ELK/阿里云 SLS 检索。每条 HTTP 请求一条 access 记录,且与 X-Request-ID 关联,便于从用户报错追踪到服务端链路。

字段来源示例
request_idch02 中间件550e8400-e29b-...
methodHTTPGET
pathURL/api/v1/products/go-handbook
status响应码200
latency_ms计时12
user_idJWT Auth(可选)1
client_ipc.ClientIP()127.0.0.1
Request
   ▼
RequestID()     ── 生成/透传 X-Request-ID
   ▼
AccessLog(zap)  ── 记录 access JSON
   ▼
Recovery(zap)   ── panic → 500 + error log
   ▼
Handler

对照章节

主题fastapi-web ch08flask-webgin-web ch08
结构化日志structlog / loggingapp.loggerzap
请求 IDmiddleware自定义RequestID
access 格式JSONWerkzeug 文本JSON zap

8.2 逐步操作表

步骤操作验证
1go get go.uber.org/zap依赖就绪
2创建 internal/logger/logger.godev/prod 模式
3创建 middleware/access_log.go每条请求一条 Info
4创建 middleware/recovery_zap.gopanic 有 Error 日志
5router 去掉 gin.Logger()无重复 access
6main defer log.Sync()进程退出 flush
7curl 带 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
ProductionJSON、采样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.RecoveryRecoveryWithZap
客户端500 文本或 JSONenvelope 50000
服务端控制台 stackzap Error + stack 字段

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。