前言
在项目开发和运维过程中,日志记录是不可或缺的一环,它帮助我们追踪请求、排查问题和监控系统状态。
Gin 框架本身提供了两个非常实用的默认中间件:gin.Logger() 和 gin.Recovery()。理解它们的功能是构建更强大日志系统的基础。本文会先介绍这两个中间件,并演示如何将 Gin 与功能强大的日志库 Zap 集成,以实现高性能、结构化的日志输出。
一、Gin 的默认日志中间件
1. gin.Logger()
gin.Logger() 是 Gin 提供的一个日志中间件,用于记录每个 HTTP 请求的基本信息。当你使用 gin.Default() 创建一个路由实例时,这个中间件会自动被加载。
它默认将日志输出到控制台(os.Stdout),记录的内容通常包括:
- 请求方法(如 GET、POST)
- 请求路径(URL)
- HTTP 状态码
- 响应耗时
- 客户端 IP 地址
示例输出:
[GIN] 2025/09/09 - 15:00:00 | 200 | 123.456µs | 127.0.0.1 | GET /api/users
这个中间件对于开发和简单的调试非常有用,但其输出是纯文本格式,不利于后续的日志分析、搜索和监控。此外,它缺乏对日志级别、结构化字段等高级功能的支持。
2. gin.Recovery()
gin.Recovery() 是一个恢复中间件,用于捕获在处理请求过程中发生的 panic 异常,防止整个服务因单个请求的崩溃而终止。
当发生 panic 时,gin.Recovery() 会:
- 捕获 panic。
- 向客户端返回一个 500 Internal Server Error 响应。
- 将 panic 的堆栈信息输出到日志(默认也是 os.Stdout)。
示例输出:
[GIN] 2025/09/09 - 15:05:00 | 500 | 1.234ms | 127.0.0.1 | GET /api/crash
panic: runtime error: invalid memory address or nil pointer dereference
与 gin.Logger() 类似,gin.Recovery() 的日志输出也是简单的文本格式,且默认输出到标准输出。
二、为什么需要集成 Zap?
虽然 Gin 的默认中间件提供了基本的日志功能,但在生产环境中,我们通常需要更强大、更灵活的日志解决方案。这就是 Zap 发挥作用的地方。
Zap 是 Uber 开源的一个高性能、结构化的 Go 日志库。它的主要优势包括:
- 高性能:Zap 经过精心设计,性能远超标准库 log 和许多其他日志库,特别适合高并发场景。
- 结构化日志:Zap 默认输出 JSON 格式的日志,包含明确的字段(如 level, msg, timestamp, fields 等),便于机器解析、搜索和集成到 ELK、Loki 等日志分析系统。
- 丰富的日志级别:支持 Debug, Info, Warn, Error, DPanic, Panic, Fatal 等级别,方便进行日志分级管理。
- 灵活的配置:可以轻松配置日志输出目标(文件、网络、标准输出等)、格式(JSON、文本)、编码器等。
如何将 Gin 与 Zap 集成
接下来,我们将一步步实现 Gin 与 Zap 的集成,替换默认的 Logger 和 Recovery 中间件
1.安装
go get -u go.uber.org/zap
2. 增加Viper log日志配置
config/config.go
Log struct {
Level string `mapstructure:"level"` // 日志等级
Format string `mapstructure:"format"` // 日志格式
Filename string `mapstructure:"filename"` // 基准日志文件名
MaxSize int `mapstructure:"maxsize"` // 单个日志文件最大内容,单位:MB
MaxAge int `mapstructure:"max_age"` // 日志文件保存时间,单位:天
MaxBackups int `mapstructure:"max_backups"` // 最多保存几个日志文件
Compress bool `mapstructure:"compress"` // 是否压缩旧日志文件
Stdout bool `mapstructure:"stdout"` // 是否输出到标准输出
} `mapstructure:"log"`
config.[dev|prod].yaml
log:
level: "debug"
format: "text" # 或 "json"
filename: "./logs/dev/app.log"
maxsize: 10 # 单个日志文件最大10MB
max_age: 7 # 日志保存7天
max_backups: 5 # 最多保存5个日志文件
compress: false # 不压缩旧日志
stdout: true # 输出到标准输出
3.初始化
在initialize目录下创建logger.go文件,实现zap日志的初始化功能:
package initialize
import (
"fmt"
"gin/global"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net"
"net/http"
"net/http/httputil"
"os"
"path"
"runtime/debug"
"strings"
"time"
)
// InitLogger 初始化zap日志
func InitLogger() {
// 创建编码器
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time", // 时间键
LevelKey: "level", // 日志级别键
NameKey: "logger", // 日志名称键
CallerKey: "caller", // 调用者键
MessageKey: "msg", // 消息键
StacktraceKey: "stacktrace", // 栈跟踪键
LineEnding: zapcore.DefaultLineEnding, // 行结束符
EncodeLevel: zapcore.CapitalColorLevelEncoder, //使用带颜色的日志级别编码器
EncodeTime: zapcore.ISO8601TimeEncoder, // 时间编码器
EncodeDuration: zapcore.StringDurationEncoder, // 持续时间编码器
EncodeCaller: zapcore.ShortCallerEncoder, // 调用者编码器
}
// 设置日志级别
var level zapcore.Level
switch global.Config.Log.Level {
case "debug":
level = zapcore.DebugLevel
case "info":
level = zapcore.InfoLevel
case "warn":
level = zapcore.WarnLevel
case "error":
level = zapcore.ErrorLevel
default:
level = zapcore.InfoLevel
}
// 创建核心
var writers []zapcore.WriteSyncer
// 如果配置了标准输出
if global.Config.Log.Stdout {
writers = append(writers, zapcore.AddSync(os.Stdout))
}
// 如果配置了文件输出
if global.Config.Log.Filename != "" {
fileWriter := getLogWriter(
global.Config.Log.Filename,
global.Config.Log.MaxSize,
global.Config.Log.MaxBackups,
global.Config.Log.MaxAge,
global.Config.Log.Compress,
)
writers = append(writers, fileWriter)
}
// 如果没有配置任何输出,默认输出到标准输出
if len(writers) == 0 {
writers = append(writers, zapcore.AddSync(os.Stdout))
}
core := zapcore.NewCore(
getEncoder(global.Config.Log.Format, encoderConfig),
zapcore.NewMultiWriteSyncer(writers...),
level,
)
// 创建Logger
logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
// 设置全局Logger
global.Logger = logger
zap.ReplaceGlobals(logger)
}
// getEncoder 根据格式选择编码器
func getEncoder(format string, encoderConfig zapcore.EncoderConfig) zapcore.Encoder {
if format == "json" {
return zapcore.NewJSONEncoder(encoderConfig)
}
return zapcore.NewConsoleEncoder(encoderConfig)
}
// getLogWriter 创建日志文件写入器
func getLogWriter(filename string, maxSize, maxBackup, maxAge int, compress bool) zapcore.WriteSyncer {
// 创建日志目录
logDir := path.Dir(filename)
if logDir != "." {
// 确保日志目录存在
if err := os.MkdirAll(logDir, os.ModePerm); err != nil {
fmt.Printf("创建日志目录失败: %v\n", err)
}
}
// 打开文件
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Printf("打开日志文件失败: %v\n", err)
// 如果打开文件失败,返回标准错误输出
return zapcore.AddSync(os.Stderr)
}
return zapcore.AddSync(file)
}
4.自定义日志中间件
// initialize/logger.go
func ZapLogger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// 处理请求
c.Next()
// 计算请求耗时
latency := time.Since(start)
// 获取请求信息
clientIP := c.ClientIP()
method := c.Request.Method
statusCode := c.Writer.Status()
reqPath := c.Request.URL.Path
userAgent := c.Request.Header.Get("User-Agent")
// 根据状态码决定日志级别
var level zapcore.Level
switch {
case statusCode >= 500:
level = zap.ErrorLevel
case statusCode >= 400:
level = zap.WarnLevel
default:
level = zap.InfoLevel
}
// 构建日志字段
fields := []zap.Field{
zap.Int("status", statusCode),
zap.String("method", method),
zap.String("path", reqPath),
zap.String("ip", clientIP),
zap.String("user-agent", userAgent),
zap.Duration("latency", latency),
}
// 添加自定义字段(例如,从上下文中获取的请求ID)
if requestId, exists := c.Get("X-Request-ID"); exists {
fields = append(fields, zap.String("request_id", requestId.(string)))
}
// 记录日志
logger.Log(level, "HTTP Request", fields...)
}
}
5.自定义恢复中间件
https://github.com/gin-contrib/zap/blob/master/zap.go
func ZapRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
if stack {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
6.在 Gin 应用中使用
main.go
// 在config初始化后
initialize.InitLogger()
initialize/router.go
Router := gin.New()
Router.Use(ZapLogger(global.Logger))
Router.Use(ZapRecovery(global.Logger, true))
// 记录不同级别的日志
global.Logger.Debug("这是一条调试日志", zap.String("key", "value"))
global.Logger.Info("这是一条信息日志", zap.Int("count", 10))
global.Logger.Warn("这是一条警告日志", zap.Error(err))
global.Logger.Error("这是一条错误日志", zap.String("error_type", "validation"))
global.Logger.Fatal("Failed to connect to database", zap.Error(err))
global.Logger.With(zap.String("request_id", "12345")).Info("处理请求")
注:Logger.Error仅记录日志。调用 Error 方法后,程序会继续正常执行后续的代码;
Logger.Fatal 记录日志 + 立即终止程序。调用 Fatal 方法后,Zap 会先将日志写入(并调用 Sync() 确保日志被刷新到磁盘或输出目标),然后立即调用 os.Exit(1)
7.运行与验证
启动应用,观察使用日志的地方是否有日志输出。
三、其他用法
- 日志上下文:利用 Gin 的 Context 传递请求 ID、用户 ID 等信息,并在日志中输出,便于全链路追踪。
异步写入:对于极高性能要求的场景,可以考虑使用 Zap 的异步写入功能。 - 日志脱敏:在记录日志时,注意对敏感信息(如密码、身份证号)进行脱敏处理。
- 集中化管理:将日志发送到 Kafka 或直接对接 Loki、Fluentd 等日志收集系统。
四、总结
通过将 Gin 与 Zap 集成,我们成功地将一个基础的 Web 框架升级为一个具备生产级日志能力的应用。Zap 提供的高性能和结构化日志特性,使得我们的应用在面对高并发流量时依然能够稳定、高效地记录关键信息。这不仅提升了系统的可观测性,也为后续的运维、监控和问题排查奠定了坚实的基础。
日志是系统的“黑匣子”,一个设计良好的日志系统是保障服务稳定运行的基石。希望本文能帮助你更好地在 Gin 项目中使用 Zap,构建更健壮的 Go 应用。