Gin 实战 5:应用日志与请求追踪
接口调通之后,要先让服务具备基本的可观测能力。后面接入数据库、JWT 和接口鉴权时,启动失败、请求异常、接口耗时和 panic 都需要靠日志快速定位。
1. 安装 Zap
bash
go get go.uber.org/zap2. 先输出到控制台
创建 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 做准备。
