go实现全局日志管理:结合使用 zap 和 Lumberjack

内容纲要

日志管理是任何软件开发中的关键环节,这玩意平时看着不起眼,出问题的时候可是救命稻草。日志记录着软件运行时的重要行为、发生错误时的异常信息,更有助于故障排除。zapLumberjack是Go生态中两种流行的日志管理工具。zap是Uber开源的日志库,以高性能著称,而Lumberjack处理日志文件轮换和压缩。本文将解析如何通过这两工具的有机配合,打造高可靠、易维护的日志管理体系。

关键说明

  • 🔥 zap:Uber家的高性能日志库,号称比标准库快10倍,结构化日志一把梭
  • 🪓 Lumberjack:专治日志文件膨胀症,自动切分+压缩旧日志的强迫症患者
  • 📦 日志轮转:通过创建新日志文件替代旧文件的维护机制,防止单个文件过大

目录结构

yourproject/
├── config/
│   └── config.go      # 配置文件
├── pkg/
│   └── logger/
│       ├── logger.go  # 日志核心实现
│       └── gin.go     # Gin日志适配
└── main.go

安装依赖

go get -u go.uber.org/zap
go get -u gopkg.in/natefinch/lumberjack.v2

配置文件 (config/config.go)

package config

type LogConfig struct {
    Level      string `yaml:"level"`       // 日志级别: debug, info, warn, error
    FilePath   string `yaml:"file_path"`   // 日志文件路径
    MaxSize    int    `yaml:"max_size"`    // 单个文件最大大小(MB)
    MaxBackups int    `yaml:"max_backups"` // 保留旧文件最大数量
    MaxAge     int    `yaml:"max_age"`     // 保留旧文件最大天数
    Compress   bool   `yaml:"compress"`    // 是否压缩旧文件
}

日志封装实现 (pkg/logger/logger.go)

package logger

import (
    "os"
    "time"

    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "gopkg.in/natefinch/lumberjack.v2"
)

var (
    globalLogger *zap.Logger
)

// 初始化日志系统
func Init(env string, cfg *config.LogConfig) error {
    var core zapcore.Core

    // 设置编码器
    encoderConfig := zap.NewProductionEncoderConfig()
    encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    encoder := zapcore.NewJSONEncoder(encoderConfig)

    // 生产环境配置
    if env == "production" {
        // 日志文件切割配置
        lumberJackLogger := &lumberjack.Logger{
            Filename:   cfg.FilePath,
            MaxSize:    cfg.MaxSize,
            MaxBackups: cfg.MaxBackups,
            MaxAge:     cfg.MaxAge,
            Compress:   cfg.Compress,
            LocalTime:  true,
        }

        // 生产环境使用文件+错误级别过滤
        core = zapcore.NewCore(
            encoder,
            zapcore.AddSync(lumberJackLogger),
            getZapLevel(cfg.Level),
        )
    } else {
        // 开发环境使用控制台+彩色输出
        consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
        core = zapcore.NewCore(
            consoleEncoder,
            zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)),
            zapcore.DebugLevel,
        )
    }

    // 创建Logger
    globalLogger = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))

    // 替换zap全局Logger
    zap.ReplaceGlobals(globalLogger)
    return nil
}

// 获取日志级别
func getZapLevel(level string) zapcore.Level {
    switch level {
    case "debug":
        return zapcore.DebugLevel
    case "info":
        return zapcore.InfoLevel
    case "warn":
        return zapcore.WarnLevel
    case "error":
        return zapcore.ErrorLevel
    default:
        return zapcore.InfoLevel
    }
}

// 获取全局Logger
func L() *zap.Logger {
    return globalLogger
}

// 安全关闭
func Sync() error {
    return globalLogger.Sync()
}

Gin适配器 (pkg/logger/gin.go)

package logger

import (
    "time"

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

// GinLogger 替换Gin默认日志中间件
func GinLogger() 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)
        L().Info("HTTP Request",
            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("latency", latency),
            zap.String("error", c.Errors.ByType(gin.ErrorTypePrivate).String()),
        )
    }
}

使用示例 (main.go)

日志初始化

package main

import (
    "yourproject/config"
    "yourproject/pkg/logger"

    "github.com/gin-gonic/gin"
)

func main() {
    // 初始化配置
    cfg := &config.LogConfig{
        Level:      "info",
        FilePath:   "./logs/app.log",
        MaxSize:    100,   // 100MB
        MaxBackups: 30,    // 保留30天
        MaxAge:     7,     // 保留7个旧文件
        Compress:   true,
    }

    // 初始化日志
    if err := logger.Init("production", cfg); err != nil {
        panic(err)
    }
    defer logger.Sync()

    // 创建Gin实例
    r := gin.New()

    // 使用自定义日志中间件
    r.Use(
        logger.GinLogger(), // 访问日志
        gin.Recovery(),     // 恢复panic
    )

    // 业务路由
    r.GET("/ping", func(c *gin.Context) {
        logger.L().Info("处理ping请求")
        c.String(200, "pong")
    })

    // 启动服务
    if err := r.Run(":8080"); err != nil {
        logger.L().Fatal("服务启动失败", zap.Error(err))
    }
}

在普通函数/方法中使用

package service

import (
    "yourproject/pkg/logger"
)

type UserService struct{}

func (s *UserService) CreateUser(name string) error {
    // 记录调试信息
    logger.L().Debug("开始创建用户", 
        zap.String("username", name),
        zap.String("service", "user"),
    )

    // 业务逻辑...
    if len(name) < 3 {
        // 记录警告
        logger.L().Warn("用户名过短",
            zap.String("username", name),
            zap.Int("length", len(name)),
        )
        return errors.New("用户名太短")
    }

    // 记录成功信息
    logger.L().Info("用户创建成功",
        zap.String("username", name),
    )
    return nil
}

在HTTP控制器中使用

package handlers

import (
    "net/http"

    "github.com/gin-gonic/gin"
    "yourproject/pkg/logger"
)

func (h *UserHandler) GetUser(c *gin.Context) {
    userID := c.Param("id")

    // 记录请求参数
    logger.L().Info("获取用户信息",
        zap.String("userID", userID),
        zap.String("clientIP", c.ClientIP()),
    )

    user, err := h.userService.Get(userID)
    if err != nil {
        // 记录错误
        logger.L().Error("获取用户失败",
            zap.String("userID", userID),
            zap.Error(err), // 自动记录错误堆栈
        )
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, user)
}

在数据库操作中使用

package repository

import (
    "gorm.io/gorm"
    "yourproject/pkg/logger"
)

type UserRepo struct {
    db *gorm.DB
}

func (r *UserRepo) FindByEmail(email string) (*User, error) {
    var user User
    err := r.db.Where("email = ?", email).First(&user).Error

    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            logger.L().Debug("用户不存在",
                zap.String("email", email),
            )
        } else {
            logger.L().Error("查询用户失败",
                zap.String("email", email),
                zap.Error(err),
            )
        }
        return nil, err
    }

    // 敏感信息脱敏记录
    logger.L().Debug("查询到用户",
        zap.String("email", email),
        zap.Int("userID", user.ID),
        zap.String("maskedName", maskString(user.Name)), // 自定义脱敏函数
    )

    return &user, nil
}

在异步任务中使用

package worker

import (
    "time"
    "yourproject/pkg/logger"
)

func ProcessTask(taskID string) {
    // 添加任务上下文字段
    taskLogger := logger.L().With(
        zap.String("taskID", taskID),
        zap.String("worker", "background_processor"),
    )

    taskLogger.Info("开始处理任务")

    start := time.Now()
    defer func() {
        taskLogger.Info("任务处理完成",
            zap.Duration("duration", time.Since(start)),
        )
    }()

    // 业务处理...
    if err := doSomething(); err != nil {
        taskLogger.Error("任务处理失败", zap.Error(err))
        return
    }
}

在中间件中使用

package middleware

import (
    "github.com/gin-gonic/gin"
    "yourproject/pkg/logger"
)

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")

        // 使用With创建带上下文的logger
        reqLogger := logger.L().With(
            zap.String("path", c.Request.URL.Path),
            zap.String("method", c.Request.Method),
        )

        if token == "" {
            reqLogger.Warn("未授权访问尝试")
            c.AbortWithStatusJSON(401, gin.H{"error": "未授权"})
            return
        }

        user, err := validateToken(token)
        if err != nil {
            reqLogger.Error("令牌验证失败",
                zap.String("token", maskToken(token)), // 敏感信息脱敏
                zap.Error(err),
            )
            c.AbortWithStatusJSON(401, gin.H{"error": "无效令牌"})
            return
        }

        reqLogger.Info("用户认证成功",
            zap.Int("userID", user.ID),
        )
        c.Set("user", user)
        c.Next()
    }
}

在测试代码中使用

package service_test

import (
    "testing"

    "yourproject/pkg/logger"
    "go.uber.org/zap/zaptest"
)

func TestUserService(t *testing.T) {
    // 使用测试专用的logger(输出到testing.T)
    testLogger := zaptest.NewLogger(t)
    logger.ReplaceGlobal(testLogger)

    // 测试用例...
    t.Run("创建用户", func(t *testing.T) {
        svc := &UserService{}
        err := svc.CreateUser("test")
        if err != nil {
            t.Fatal(err)
        }
        // 日志会自动输出到测试结果
    })
}

在初始化代码中使用

package main

import (
    "os"
    "os/signal"
    "syscall"

    "yourproject/pkg/logger"
)

func main() {
    // 初始化日志
    if err := logger.Init("production", loadLogConfig()); err != nil {
        panic(err)
    }
    defer func() {
        // 确保日志缓冲区刷新
        if err := logger.Sync(); err != nil {
            // 不能使用logger了,直接打印到stderr
            _, _ = fmt.Fprintf(os.Stderr, "刷新日志失败: %v\n", err)
        }
    }()

    // 优雅退出处理
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        sig := <-sigChan
        logger.L().Info("接收到退出信号",
            zap.String("signal", sig.String()),
        )
        // 执行清理...
        os.Exit(0)
    }()

    // 启动应用...
}

在第三方库适配中使用

package thirdparty

import (
    "database/sql"

    "yourproject/pkg/logger"
)

// 包装sql.DB的查询方法
type LoggingSQL struct {
    *sql.DB
}

func (db *LoggingSQL) Query(query string, args ...interface{}) (*sql.Rows, error) {
    logger.L().Debug("执行SQL查询",
        zap.String("query", query),
        zap.Any("args", args),
    )

    start := time.Now()
    rows, err := db.DB.Query(query, args...)

    logger.L().Debug("SQL查询完成",
        zap.String("query", query),
        zap.Duration("duration", time.Since(start)),
        zap.Error(err),
    )

    return rows, err
}

生产环境日志示例

{
  "level": "info",
  "ts": "2023-08-01T12:00:00.000Z",
  "caller": "logger/gin.go:20",
  "msg": "HTTP Request",
  "status": 200,
  "method": "GET",
  "path": "/ping",
  "query": "",
  "ip": "127.0.0.1",
  "user-agent": "curl/7.68.0",
  "latency": 123456,
  "error": ""
}

方案特点

环境自适应

  • 生产环境:JSON格式 + 文件存储 + 日志切割

  • 开发环境:彩色控制台输出

高性能

  • 使用zap高性能日志库
  • 异步写入(通过lumberjack的缓冲机制)

安全可靠

  • 自动日志切割(按大小和时间)

  • 自动压缩旧日志

  • 文件权限控制(默认使用系统权限)

完整链路

  • 记录HTTP请求全量信息

  • 支持自定义业务日志

  • 错误堆栈追踪

可扩展性

  • 支持日志分级过滤

  • 方便集成日志监控系统

  • 可扩展多日志输出(如同时输出到文件和控制台)

扩展方案

日志分级存储

// 不同级别日志到不同文件
infoWriter := getLumberjack("info.log")
errorWriter := getLumberjack("error.log")

core := zapcore.NewTee(
    zapcore.NewCore(encoder, infoWriter, zap.InfoLevel),
    zapcore.NewCore(encoder, errorWriter, zap.ErrorLevel),
)

日志报警

// 错误日志触发报警
logger.L().Error("数据库连接失败", 
    zap.Error(err),
    zap.Any("notify", map[string]string{
        "type": "critical",
        "target": "ops-team",
    }),
)

日志追踪

// 添加请求ID
r.Use(func(c *gin.Context) {
    c.Set("requestID", uuid.New().String())
})

// 在日志中记录请求ID
logger.L().Info("处理请求", 
    zap.String("requestID", c.GetString("requestID")),
)

日志清理策略

# 使用logrotate管理日志(示例配置)
/path/to/your/logs/*.log {
    daily
    rotate 30
    compress
    missingok
    notifempty
    sharedscripts
    postrotate
        kill -USR1 `cat /var/run/your-app.pid`
    endscript
}

最佳实践

日志分级使用

  • Debug: 调试信息,生产环境通常不记录

  • Info: 关键业务流程节点

  • Warn: 异常但可恢复的情况

  • Error: 需要干预的错误

结构化字段

// 不好的做法
logger.L().Info(fmt.Sprintf("用户 %d 登录失败,原因: %v", userID, err))

// 推荐做法
logger.L().Info("用户登录失败",
    zap.Int("userID", userID),
    zap.Error(err),
)

敏感信息处理

// 自动脱敏工具函数
func maskString(s string) string {
    if len(s) <= 3 {
        return "***"
    }
    return s[:3] + "***"
}

logger.L().Info("处理支付",
    zap.String("cardNumber", maskString("1234567890123456")),
)

性能关键路径

// 使用条件判断避免不必要的日志开销
if logger.L().Core().Enabled(zap.DebugLevel) {
    logger.L().Debug("详细数据",
        zap.Any("bigData", loadHeavyData()),
    )
}

上下文关联

// 创建带请求ID的子logger
func WithRequestID(c *gin.Context) *zap.Logger {
    return logger.L().With(
        zap.String("requestID", c.GetString("requestID")),
    )
}

总结

通过这种统一的日志使用方式,可以保持整个项目的日志风格一致,方便收集和分析日志,并可以快速定位问题,实现完善的审计追踪,轻松调整日志级别而不需要修改业务代码。