





在基于 Gin 框架进行项目开发时,其自带的日志功能往往比较简单,缺少按业务拆分、文件轮转以及同时输出到终端和文件等企业级特性。当需要排查线上问题时,在海量的单一日志文件中定位信息效率低下。
Go 生态中的 GoFrame 框架的日志模块以其结构化、高可配置性和支持多目录拆分而备受好评。本文将借鉴 GoFrame 的日志设计思想,为 Gin 项目打造一套集日志拆分、双输出和自动轮转于一体的实用日志方案,相关代码可直接复用。
方案核心功能
本方案旨在实现以下核心功能,以提升日志管理效率和问题排查速度:
- 日志拆分:将日志按业务类型划分为访问日志、错误日志、应用运行日志和 SQL 日志,保持目录结构清晰。
- 双输出:支持同时将日志输出到终端(便于开发调试实时查看)和文件(用于持久化存储与线上查阅)。
- 自动轮转:支持按日期自动生成日志文件,并可配置单个文件大小、最大备份数量及文件保留天数。
- 结构化格式:默认采用 JSON 格式记录日志,包含时间戳、日志级别、代码调用位置及错误堆栈等关键信息,便于后续使用 ELK 等工具解析。
- 高可配置性:通过 YAML 配置文件统一管理所有日志相关的参数,如输出目录、级别、输出方式等,灵活性强。
前置依赖
实现该方案需要引入以下三个核心 Go 模块:
go get go.uber.org/zap # 高性能日志库核心
go get gopkg.in/yaml.v3 # 用于解析YAML格式的配置文件
go get gopkg.in/natefinch/lumberjack.v2 # 实现日志文件的轮转
配置文件设计 (config.yaml)
参考 GoFrame 的配置风格,我们将所有日志配置集中管理。建议配置文件路径为 server/manifest/config/config.yaml。
server:
address: ":8808"
# 服务器相关日志(访问日志与错误日志)
logPath: "resource/log/server" # 日志存储根目录
logStdout: true # 是否启用终端标准输出
errorStack: true # 错误日志是否记录堆栈信息
errorLogPattern: "error-{Ymd}.log" # 错误日志文件名模式(按日期轮转)
accessLogPattern: "access-{Ymd}.log" # 访问日志文件名模式
logger:
# 应用程序运行日志配置
path: "resource/log/run" # 运行日志目录
file: "{Y-m-d}.log" # 日志文件名格式
level: "all" # 日志记录级别(all等同于debug)
stdout: true # 是否启用终端输出
database:
# SQL执行日志配置
logger:
path: "resource/log/sql" # SQL日志目录
file: "{Y-m-d}.log" # 日志文件名格式
level: "all" # 日志记录级别
stdout: true # 是否启用终端输出
核心代码实现
1. 定义配置结构与全局日志器
首先定义与 YAML 配置文件对应的结构体,并声明全局的日志器变量,方便在项目各处调用。
package main
import (
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"gopkg.in/yaml.v3"
"os"
"fmt"
)
// LogConfig 映射YAML配置文件结构
type LogConfig struct {
Server struct {
LogPath string `yaml:"logPath"`
LogStdout bool `yaml:"logStdout"`
ErrorStack bool `yaml:"errorStack"`
ErrorLogPattern string `yaml:"errorLogPattern"`
AccessLogPattern string `yaml:"accessLogPattern"`
} `yaml:"server"`
Logger struct {
Path string `yaml:"path"`
File string `yaml:"file"`
Level string `yaml:"level"`
Stdout bool `yaml:"stdout"`
} `yaml:"logger"`
Database struct {
Logger struct {
Path string `yaml:"path"`
File string `yaml:"file"`
Level string `yaml:"level"`
Stdout bool `yaml:"stdout"`
} `yaml:"logger"`
} `yaml:"database"`
}
// 全局日志器实例
var (
AccessLogger *zap.Logger // 访问日志
ErrorLogger *zap.Logger // 错误日志
RunLogger *zap.Logger // 运行日志
SqlLogger *zap.Logger // SQL日志
)
2. 初始化日志器(实现拆分、双输出与轮转)
这是方案的核心,包含配置加载、日志器构建和初始化逻辑。
// 加载日志配置文件
func loadLogConfig() (LogConfig, error) {
var cfg LogConfig
file, err := os.Open("server/manifest/config/config.yaml")
if err != nil {
return cfg, fmt.Errorf("配置文件打开失败: %w", err)
}
defer file.Close()
return cfg, yaml.NewDecoder(file).Decode(&cfg)
}
// 转换配置中的日期模板({Ymd}→20060102,{Y-m-d}→2006-01-02)
func convertDateTemplate(template string) string {
template = strings.ReplaceAll(template, "{Ymd}", "20060102")
template = strings.ReplaceAll(template, "{Y-m-d}", "2006-01-02")
return template
}
// 构建日志器的通用函数
func buildLogger(logPath, filePattern, levelStr string, stdout bool) *zap.Logger {
// 确保日志目录存在
_ = os.MkdirAll(logPath, 0755)
// 配置 lumberjack 实现日志轮转
writeSyncer := &lumberjack.Logger{
Filename: fmt.Sprintf("%s/%s", logPath, time.Now().Format(convertDateTemplate(filePattern))),
MaxSize: 100, // 单个日志文件最大100MB
MaxBackups: 30, // 最多保留30个备份文件
MaxAge: 7, // 文件最长保存7天
Compress: true, // 压缩旧的日志文件以节省空间
}
// 配置JSON编码器
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "time"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
// 核心:实现双输出(文件 + 可选终端)
var cores []zapcore.Core
fileCore := zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), zapcore.AddSync(writeSyncer), getLogLevel(levelStr))
cores = append(cores, fileCore)
if stdout {
consoleCore := zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), zapcore.AddSync(os.Stdout), getLogLevel(levelStr))
cores = append(cores, consoleCore)
}
// 创建日志器,添加调用者信息和错误堆栈
return zap.New(zapcore.NewTee(cores...), zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
}
// 将字符串日志级别转换为zapcore.Level
func getLogLevel(levelStr string) zapcore.Level {
levelMap := map[string]zapcore.Level{
"all": zapcore.DebugLevel,
"debug": zapcore.DebugLevel,
"info": zapcore.InfoLevel,
"error": zapcore.ErrorLevel,
}
if level, ok := levelMap[levelStr]; ok {
return level
}
return zapcore.DebugLevel // 默认级别
}
// 初始化所有全局日志器
func InitLogger() error {
cfg, err := loadLogConfig()
if err != nil {
return err
}
// 按业务初始化四类独立的日志器
AccessLogger = buildLogger(cfg.Server.LogPath, cfg.Server.AccessLogPattern, "info", cfg.Server.LogStdout)
ErrorLogger = buildLogger(cfg.Server.LogPath, cfg.Server.ErrorLogPattern, "error", cfg.Server.LogStdout)
RunLogger = buildLogger(cfg.Logger.Path, cfg.Logger.File, cfg.Logger.Level, cfg.Logger.Stdout)
SqlLogger = buildLogger(cfg.Database.Logger.Path, cfg.Database.Logger.File, cfg.Database.Logger.Level, cfg.Database.Logger.Stdout)
return nil
}
3. 适配Gin框架日志输出
需要将 Gin 默认的日志输出,重定向到我们自定义的 zap 日志器中。
// 自定义Writer,将Gin的访问日志重定向到AccessLogger
type GinAccessWriter struct{}
func (w *GinAccessWriter) Write(p []byte) (n int, err error) {
AccessLogger.Info(strings.TrimSpace(string(p)))
return len(p), nil
}
// 自定义Writer,将Gin的错误日志重定向到ErrorLogger
type GinErrorWriter struct{}
func (w *GinErrorWriter) Write(p []byte) (n int, err error) {
ErrorLogger.Error(strings.TrimSpace(string(p)))
return len(p), nil
}
// 主函数示例
func main() {
// 第一步:初始化日志系统
if err := InitLogger(); err != nil {
panic(fmt.Sprintf("日志初始化失败: %v", err))
}
// 程序退出前,确保缓冲区日志被刷新
defer func() {
_ = AccessLogger.Sync()
_ = ErrorLogger.Sync()
_ = RunLogger.Sync()
_ = SqlLogger.Sync()
}()
// 将Gin的默认输出绑定到自定义日志器
gin.DefaultWriter = &GinAccessWriter{}
gin.DefaultErrorWriter = &GinErrorWriter{}
// 启动Gin服务
r := gin.Default()
r.GET("/", func(c *gin.Context) {
// 使用运行日志记录业务信息
RunLogger.Info("收到请求", zap.String("path", c.Request.URL.Path))
c.JSON(200, gin.H{"msg": "success"})
})
// 模拟记录SQL日志
SqlLogger.Debug("执行SQL", zap.String("sql", "SELECT * FROM users WHERE id=1"))
_ = r.Run(":8808")
}
方案效果与优势
生成的日志目录结构
所有日志按类型分目录存储,结构清晰。
resource/
└── log/
├── server/ # 服务器日志(访问/错误)
│ ├── access-20251203.log
│ └── error-20251203.log
├── run/ # 应用运行日志
│ └── 2025-12-03.log
└── sql/ # SQL执行日志
└── 2025-12-03.log
日志输出格式示例
日志为 JSON 结构化格式,便于机器解析与人工阅读。
// 运行日志示例
{"level":"INFO","time":"2025-12-03T16:30:00.123+08:00","caller":"main.go:156","msg":"收到请求","path":"/"}
// SQL日志示例
{"level":"DEBUG","time":"2025-12-03T16:30:00.124+08:00","caller":"main.go:160","msg":"执行SQL","sql":"SELECT * FROM users WHERE id=1"}
方案优势总结
- 结构清晰:通过业务拆分日志,从根本上避免了所有日志混杂在单个文件中的问题,显著提升问题排查效率。
- 配置灵活:所有行为均通过 YAML 文件控制,无需修改代码即可调整日志策略,适合不同环境(开发、测试、生产)。
- 性能与稳定兼顾:基于高性能的
zap 库和稳定的 lumberjack 轮转库,能满足高并发场景下的日志记录需求。
- 易于扩展:日志器初始化逻辑抽象良好,若要新增一种日志类型(如 Redis 操作日志),只需增加配置和一行初始化代码即可。
注意事项
- 日志目录会在首次写入时自动创建,无需手动创建。
- 在生产环境中,建议将
stdout 配置项设为 false 以关闭终端输出,避免占用不必要的 I/O 资源。
- 可根据服务器磁盘容量,合理调整
MaxSize、MaxBackups 和 MaxAge 参数。
- 如果需要更易读的控制台文本格式,可将
zapcore.NewJSONEncoder 替换为 zapcore.NewConsoleEncoder。