Skip to content

Gin 实战 5:应用日志与请求追踪

接口调通之后,要先让服务具备基本的可观测能力。后面接入数据库、JWT 和接口鉴权时,启动失败、请求异常、接口耗时和 panic 都需要靠日志快速定位。

1. 安装 Zap

bash
go get go.uber.org/zap

2. 先输出到控制台

创建 internal/common/logger/logger.go

go
package logger

import "go.uber.org/zap"

// Log 保存全局 Logger 实例。
// 简化阶段先用全局变量,后续也可以通过依赖注入传递。
var Log *zap.Logger

func Init() error {
    // NewDevelopment 适合本地开发:输出可读性好,带调用位置。
    // 生产环境通常会切换为 NewProduction 或自定义 JSON 配置。
    l, err := zap.NewDevelopment()
    if err != nil {
        return err
    }

    // 初始化成功后,其他包就可以通过 logger.Log 输出日志。
    Log = l
    return nil
}

func Sync() {
    if Log != nil {
        // Sync 会刷新缓冲区,避免程序退出时日志丢失。
        _ = Log.Sync()
    }
}

日志级别、文件路径、输出格式可以继续放进 Viper 配置。这里先保证本地开发能清楚看到日志。

3. 同时写入日志文件

开发阶段只看控制台就够了;服务部署到服务器后,通常还需要写入文件,方便回看历史问题。

可以把 Init() 扩展成控制台 + 文件双输出:

go
package logger

import (
    "os"

    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

var Log *zap.Logger

func Init() error {
    // 1. 创建 logs 目录,不存在就自动创建。
    if err := os.MkdirAll("logs", 0755); err != nil {
        return err
    }

    // 2. 打开日志文件,追加写入。
    file, err := os.OpenFile("logs/app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        return err
    }

    encoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())

    // 3. 控制台输出:方便本地开发实时观察。
    consoleCore := zapcore.NewCore(
        encoder,
        zapcore.AddSync(os.Stdout),
        zapcore.InfoLevel,
    )

    // 4. 文件输出:方便部署后回看历史日志。
    fileCore := zapcore.NewCore(
        encoder,
        zapcore.AddSync(file),
        zapcore.InfoLevel,
    )

    // 5. NewTee 会把同一条日志同时写到多个输出目标。
    Log = zap.New(zapcore.NewTee(consoleCore, fileCore), zap.AddCaller())
    return nil
}

这样请求日志会同时出现在控制台和 logs/app.log

4. 请求日志中间件

创建 internal/common/logger/middleware.go

go
package logger

import (
    "time"

    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

func Middleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 请求进入时记录开始时间,用于计算接口耗时。
        start := time.Now()

        // c.Next() 会继续执行后续中间件和真正的 Handler。
        // Handler 执行完成后,代码会回到这里继续记录日志。
        c.Next()

        Log.Info("http request",
            // 记录请求方法、路径、状态码、耗时和客户端 IP。
            // 这些字段足够支撑大多数接口排查。
            zap.String("method", c.Request.Method),
            zap.String("path", c.Request.URL.Path),
            zap.Int("status", c.Writer.Status()),
            zap.Duration("cost", time.Since(start)),
            zap.String("ip", c.ClientIP()),
        )
    }
}

5. main.go 接入日志

go
// 日志尽量在启动流程靠前初始化。
// 后续数据库、路由注册失败时,都可以直接写日志。
if err := logger.Init(); err != nil {
    log.Fatalf("日志初始化失败: %v", err)
}
defer logger.Sync()

r := gin.New()

// 请求日志中间件记录每次 HTTP 请求。
r.Use(logger.Middleware())

// Recovery 捕获 panic,避免单个请求导致整个进程退出。
r.Use(gin.Recovery())

现在每次请求都会输出结构化日志,启动和请求链路的问题也有了排查入口。下一章接入 MySQL 和 GORM,为用户 CRUD 做准备。

基于 MIT 许可发布 · 如需部署协助或二开定制,请查看 支持与合作