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")),
    )
}

总结

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值