Skip to content

Zap — 高性能结构化日志

Zap 是 Uber 开源的 Go 日志库,以极低的内存分配和极高的性能著称,是生产环境的首选。

安装

bash
go get -u go.uber.org/zap

两种 Logger

go
// 1. zap.Logger — 零分配,性能最高,但 API 稍繁琐
logger, _ := zap.NewProduction()
defer logger.Sync()

logger.Info("用户登录",
    zap.String("username", "alice"),
    zap.Int("userID", 42),
    zap.Duration("latency", time.Millisecond*100),
)

// 2. zap.SugaredLogger — 略有分配,但 API 更友好
sugar := logger.Sugar()
sugar.Infow("用户登录",
    "username", "alice",
    "userID", 42,
)
sugar.Infof("用户 %s 登录成功", "alice")

配置

go
// 开发环境(彩色输出,人类可读)
logger, _ := zap.NewDevelopment()

// 生产环境(JSON 格式,高性能)
logger, _ := zap.NewProduction()

// 自定义配置
config := zap.Config{
    Level:       zap.NewAtomicLevelAt(zap.InfoLevel),
    Development: false,
    Encoding:    "json",  // "json" 或 "console"
    EncoderConfig: zapcore.EncoderConfig{
        TimeKey:        "ts",
        LevelKey:       "level",
        NameKey:        "logger",
        CallerKey:      "caller",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.LowercaseLevelEncoder,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    },
    OutputPaths:      []string{"stdout", "/var/log/app.log"},
    ErrorOutputPaths: []string{"stderr"},
}

logger, err := config.Build()

结构化字段

go
// 强类型字段(零分配)
logger.Info("处理请求",
    zap.String("method", "GET"),
    zap.String("path", "/api/users"),
    zap.Int("status", 200),
    zap.Float64("latency_ms", 12.5),
    zap.Bool("cached", true),
    zap.Time("timestamp", time.Now()),
    zap.Duration("elapsed", time.Millisecond*12),
    zap.Error(err),
    zap.Strings("tags", []string{"api", "v1"}),
    zap.Any("user", user),  // 任意类型(有反射开销)
)

// 对象字段(实现 zap.ObjectMarshaler 接口)
type UserLog struct {
    ID    int
    Name  string
    Email string
}

func (u UserLog) MarshalLogObject(enc zapcore.ObjectEncoder) error {
    enc.AddInt("id", u.ID)
    enc.AddString("name", u.Name)
    enc.AddString("email", u.Email)
    return nil
}

logger.Info("用户操作", zap.Object("user", UserLog{1, "Alice", "alice@example.com"}))

With — 创建带固定字段的子 Logger

go
// 为每个请求创建带 requestID 的 logger
func handleRequest(w http.ResponseWriter, r *http.Request) {
    requestID := r.Header.Get("X-Request-ID")
    log := logger.With(
        zap.String("request_id", requestID),
        zap.String("method", r.Method),
        zap.String("path", r.URL.Path),
    )

    log.Info("请求开始")

    user, err := getUser(r.Context())
    if err != nil {
        log.Error("获取用户失败", zap.Error(err))
        return
    }

    log.Info("请求完成", zap.Int("user_id", user.ID))
}

动态日志级别

go
// 运行时动态调整日志级别(无需重启)
atom := zap.NewAtomicLevel()

logger := zap.New(zapcore.NewCore(
    zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
    zapcore.Lock(os.Stdout),
    atom,
))

// HTTP 接口动态调整级别
http.HandleFunc("/log/level", atom.ServeHTTP)
// curl -X PUT http://localhost:8080/log/level -d '{"level":"debug"}'

多输出目标

go
// 同时输出到控制台和文件
fileEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())

logFile, _ := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)

core := zapcore.NewTee(
    zapcore.NewCore(fileEncoder, zapcore.AddSync(logFile), zap.InfoLevel),
    zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zap.DebugLevel),
)

logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))

日志轮转(配合 lumberjack)

bash
go get gopkg.in/natefinish/lumberjack.v2
go
import "gopkg.in/natefinish/lumberjack.v2"

w := zapcore.AddSync(&lumberjack.Logger{
    Filename:   "/var/log/app.log",
    MaxSize:    100,  // MB
    MaxBackups: 7,    // 保留 7 个备份
    MaxAge:     30,   // 保留 30 天
    Compress:   true, // 压缩旧日志
})

core := zapcore.NewCore(
    zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
    w,
    zap.InfoLevel,
)
logger := zap.New(core)

Gin 集成

go
// Gin 使用 Zap 中间件
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        query := c.Request.URL.RawQuery

        c.Next()

        cost := time.Since(start)
        logger.Info("HTTP请求",
            zap.Int("status", c.Writer.Status()),
            zap.String("method", c.Request.Method),
            zap.String("path", path),
            zap.String("query", query),
            zap.String("ip", c.ClientIP()),
            zap.String("user-agent", c.Request.UserAgent()),
            zap.Duration("cost", cost),
            zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
        )
    }
}

func GinRecovery(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                logger.Error("panic recovered",
                    zap.Any("error", err),
                    zap.String("path", c.Request.URL.Path),
                    zap.ByteString("stack", debug.Stack()),
                )
                c.AbortWithStatus(500)
            }
        }()
        c.Next()
    }
}

全局 Logger

go
// 初始化全局 logger
func InitLogger(env string) {
    var logger *zap.Logger
    if env == "production" {
        logger, _ = zap.NewProduction()
    } else {
        logger, _ = zap.NewDevelopment()
    }
    zap.ReplaceGlobals(logger)
}

// 在任何地方使用
zap.L().Info("全局 logger")
zap.S().Infow("Sugar 全局 logger", "key", "value")

Zap vs Zerolog vs slog

  • Zap:功能最全,生态最好,适合大型项目
  • Zerolog:API 更简洁,性能相当,适合中小项目
  • slog(标准库,Go 1.21+):无需第三方依赖,适合库开发

生产项目推荐 Zap,库开发推荐 slog。

本站内容由 褚成志 整理编写,仅供学习参考