日志管理是任何软件开发中的关键环节,这玩意平时看着不起眼,出问题的时候可是救命稻草。日志记录着软件运行时的重要行为、发生错误时的异常信息,更有助于故障排除。zap
和Lumberjack
是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")),
)
}
总结
通过这种统一的日志使用方式,可以保持整个项目的日志风格一致,方便收集和分析日志,并可以快速定位问题,实现完善的审计追踪,轻松调整日志级别而不需要修改业务代码。