logger
基于 zap 的 Go 日志库。分类日志、链路追踪、按天/按大小切割、自动清理与压缩、Gin/GORM 集成,一行初始化。
特性
- 一行初始化:
logger.Init("./logs") 即可用
- 分类日志:info / warning / error / debug / sql / access 各自独立文件
- 链路追踪:
trace_id 自动注入,支持跨服务传递
- 按天切割:每天自动归档前一天的日志
- 按大小切割:单文件超过阈值自动分卷
- 自动压缩:归档文件 gzip,节省磁盘
- 自动清理:按天数 + 按数量双重限制
- 控制台彩色:开发时好看,生产只写文件
- Gin 集成:一行中间件搞定 access 日志 + trace_id 注入
- GORM 集成:SQL 日志自动分流到 sql / warning / error
安装
go get gitee.com/code-pilot/go-box/logger
快速开始
package main
import (
"gitee.com/code-pilot/go-box/logger"
"go.uber.org/zap"
)
func main() {
// 一行初始化,只需一个日志目录
if err := logger.Init("./logs"); err != nil {
panic(err)
}
defer logger.Close()
defer logger.Sync()
logger.Info("服务启动", zap.String("addr", ":8080"))
logger.Warning("配置缺失,使用默认值")
logger.Error("数据库连接失败")
logger.Debug("调试信息")
}
分类日志
日志按用途分为 6 个独立分类,各自写入独立文件:
| 分类 |
方法 |
默认级别 |
说明 |
| info |
logger.Info() |
Info |
普通业务日志 |
| warning |
logger.Warning() |
Warn |
警告日志 |
| error |
logger.Error() |
Error |
错误日志(带堆栈) |
| debug |
logger.Debug() |
Debug |
调试日志 |
| sql |
logger.SQL() |
Debug |
SQL 执行日志 |
| access |
logger.Access() |
Info |
HTTP 访问日志 |
每条日志都会带 category 字段,方便区分来源。
logger.Info("用户登录", zap.Int("uid", 1001))
logger.Warning("缓存未命中", zap.String("key", "user:1001"))
logger.Error("数据库连接失败", zap.Error(err))
logger.Debug("请求参数", zap.Any("params", params))
logger.SQL("SELECT * FROM users WHERE id = ?", zap.Int("id", 1))
logger.Access("GET /api/user 200", zap.Duration("cost", 12*time.Millisecond))
也支持 printf 风格:
logger.Infof("用户 %d 登录成功", 1001)
logger.Errorf("请求失败: %v", err)
文件命名与切割
正在写的文件不带日期,切割归档时才加日期,方便 tail -F。
./logs/
├── info.log # 正在写(无日期,tail -F 友好)
├── warning.log
├── error.log
├── debug.log
├── sql.log
├── access.log
├── info-2026-09-29.log.gz # 归档(带日期 + 压缩)
├── info-2026-09-30.1.log.gz # 超过大小后分卷
└── access-2026-09-29.log.gz
切割规则:
- 按天切割:每天 0 点后第一次写入,自动归档前一天的日志
- 按大小切割:单文件超过
MaxSizeMB(默认 100MB)自动分卷
- 自动压缩:归档文件自动 gzip
- 自动清理:超过
MaxAgeDays 天(默认 30 天)自动删除
- 限制数量:每个分类最多保留
MaxBackups 个归档
用 tail -F(大写 F)实时跟踪,切割时不会断流:
tail -F ./logs/info.log
tail -F ./logs/access.log
链路追踪
trace_id 用于串联一次请求在多个服务、多个函数里的所有日志。
生成与使用
ctx, traceID := logger.EnsureTraceID(context.Background())
// 有就复用,没有就新建
logger.Ctx(ctx).Info("处理请求开始")
logger.Ctx(ctx).SQL("SELECT * FROM users WHERE id = ?", zap.Int("id", 1))
logger.Ctx(ctx).Error("处理失败")
三条日志会共享同一个 trace_id。
traceID 有什么用
ctx 只能在进程内传递,出了进程边界(HTTP、gRPC、MQ)就断了。traceID 字符串是跨进程传递的唯一凭证:
调下游 HTTP 服务
ctx, traceID := logger.EnsureTraceID(ctx)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("X-Trace-Id", traceID)
client.Do(req)
下游复用同一个 trace_id:
traceID := r.Header.Get("X-Trace-Id")
ctx = logger.WithTraceID(r.Context(), traceID)
发 Kafka 消息
producer.Send(&kafka.Message{
Headers: []kafka.Header{
{Key: "X-Trace-Id", Value: []byte(traceID)},
},
})
返回给前端
c.JSON(200, gin.H{
"trace_id": traceID, // 用户报错时截图,直接拿去搜日志
"data": data,
})
API
id := logger.NewTraceID() // 新建
ctx = logger.WithTraceID(ctx, id) // 写入
id := logger.GetTraceID(ctx) // 读取
ctx, id := logger.EnsureTraceID(ctx) // 有则复用,无则新建
在 Service / Repo 层传递 trace_id
这是最常见的疑问:后续的 service、repo 等方法,trace_id 怎么传下去?
核心结论:
不是"把 trace_id 传下去",而是"把 ctx 传下去"。trace_id 在 ctx 里,ctx 传到哪,trace_id 就到哪。
❌ 错误做法:把 traceID 当参数传
func (s *UserService) GetUser(traceID string, id int) (*User, error) {
// 每层都要加 traceID 参数,恶心
}
func (r *UserRepo) FindByID(traceID string, id int) (*User, error) {
// repo 层也被污染
}
问题:每个函数签名多一个参数;和业务参数混在一起;以后加用户ID、租户ID 还得再加。
✅ 正确做法:把 ctx 当第一个参数传
这是 Go 官方推荐 的做法:
// ============ Handler ============
func (h *UserHandler) Get(c *gin.Context) {
ctx := c.Request.Context() // GinAccess 中间件已注入 trace_id
logger.Ctx(ctx).Info("收到请求", zap.String("path", c.Request.URL.Path))
user, err := h.svc.GetUser(ctx, 1)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
}
// ============ Service ============
type UserService struct {
repo *UserRepo
}
func (s *UserService) GetUser(ctx context.Context, id int) (*User, error) {
logger.Ctx(ctx).Info("Service: 查询用户", zap.Int("id", id))
if id <= 0 {
err := errors.New("invalid id")
logger.Ctx(ctx).Error("参数非法", zap.Error(err))
return nil, err
}
user, err := s.repo.FindByID(ctx, id) // ctx 继续往下传
if err != nil {
logger.Ctx(ctx).Error("Repo 查询失败", zap.Error(err))
return nil, err
}
return user, nil
}
// ============ Repo ============
type UserRepo struct {
db *gorm.DB
}
func (r *UserRepo) FindByID(ctx context.Context, id int) (*User, error) {
logger.Ctx(ctx).SQL("SELECT * FROM users WHERE id = ?", zap.Int("id", id))
var user User
// GORM 的 WithContext 会把 ctx 传到 GORM 的 logger
err := r.db.WithContext(ctx).First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}
三条日志共享同一个 trace_id:
{"msg":"收到请求","category":"info","trace_id":"e58d..."}
{"msg":"Service: 查询用户","category":"info","trace_id":"e58d..."}
{"msg":"SELECT * FROM users","category":"sql","trace_id":"e58d..."}
传递链路图
HTTP 请求
↓
GinAccess 中间件
├─ 从 X-Trace-Id 读或新建 trace_id
├─ ctx = WithTraceID(ctx, id)
└─ c.Request = c.Request.WithContext(ctx)
↓
Handler: ctx := c.Request.Context()
├─ logger.Ctx(ctx).Info(...) ✅ trace_id
↓
Service: GetUser(ctx, id)
├─ logger.Ctx(ctx).Info(...) ✅ trace_id
↓
Repo: FindByID(ctx, id)
├─ logger.Ctx(ctx).SQL(...) ✅ trace_id
└─ db.WithContext(ctx).First(...) ✅ SQL 日志也有 trace_id
↓
下游 HTTP / Kafka
├─ req.Header.Set("X-Trace-Id", id) ✅ 跨进程传递
关键点
ctx 必须一路传下去:handler → service → repo → db,每一层都接收并传递 ctx
ctx 是第一个参数:Go 官方约定,不要放后面
ctx 不要存到 struct 里:它的生命周期是请求级的,存 struct 会出问题
- goroutine 要显式传 ctx:新协程用
context.Background() 会丢 trace_id
常见问题
Q1:不用 Gin 的纯 Go 服务呢?
在 main 入口注入:
func main() {
logger.Init("./logs")
ctx, traceID := logger.EnsureTraceID(context.Background())
_ = traceID
if err := run(ctx); err != nil {
logger.Ctx(ctx).Error("服务异常退出", zap.Error(err))
}
}
func run(ctx context.Context) error {
return doBusiness(ctx)
}
Q2:goroutine 里 trace_id 会断吗?
会。ctx 不是自动传播的,起协程时必须显式传:
func (s *UserService) GetUser(ctx context.Context, id int) (*User, error) {
// ❌ 错误:新协程用 Background,trace_id 丢了
go func() {
logger.Ctx(context.Background()).Info("异步任务") // 没有 trace_id
}()
// ✅ 正确:把 ctx 传进去
go func(ctx context.Context) {
logger.Ctx(ctx).Info("异步任务") // 有 trace_id
}(ctx)
return nil, nil
}
Q3:GORM 的 WithContext 有什么用?
db.WithContext(ctx) 会把 ctx 传给 GORM 的 logger,这样 SQL 日志里就能带上 trace_id。如果不调 WithContext,SQL 日志就没有 trace_id。
Q4:ctx 会一直传下去吗?会不会污染?
ctx 的设计就是请求级别的生命周期。一个 HTTP 请求进来,创建 ctx;请求结束,ctx 被 GC。不会跨请求污染。但不要把 ctx 存到 struct 里长期持有。
配置
默认配置开箱即用,也可自定义:
cfg := &logger.Config{
Dir: "./logs", // 日志目录
Level: "debug", // 全局最低级别:debug/info/warn/error
Encoding: "console", // console / json
Console: true, // 是否输出到控制台
Caller: true, // 是否记录调用者
MaxSizeMB: 100, // 单文件最大 MB
MaxAgeDays: 30, // 保留天数
MaxBackups: 50, // 每分类最多归档数
Compress: true, // 压缩归档
ErrorStack: true, // error 级别带堆栈
}
if err := logger.InitWithConfig(cfg); err != nil {
panic(err)
}
控制台输出策略:只有 info / warning / error 三个分类会同时输出到控制台,debug / sql / access 只写文件,避免控制台刷屏。
Gin 集成
import (
"gitee.com/code-pilot/go-box/logger"
"github.com/gin-gonic/gin"
)
r := gin.New()
r.Use(logger.GinRecovery()) // panic 恢复 + 记录到 error 日志
r.Use(logger.GinAccess()) // 注入 trace_id + 记录 access 日志
r.GET("/hello", func(c *gin.Context) {
// 自动带上 trace_id
logger.FromGin(c).Info("处理 hello")
logger.FromGin(c).SQL("SELECT ...")
c.JSON(200, gin.H{"trace_id": c.GetHeader("X-Trace-Id")})
})
GinAccess 自动完成:
- 从
X-Trace-Id 请求头读取 trace_id,没有就新建
- 写入
c.Request.Context(),后续 logger.Ctx(ctx) 自动带上
- 通过
X-Trace-Id 响应头返回给前端
- 请求结束记录 access 日志:方法、路径、query、状态码、耗时、IP、UA
- 按状态码分流:5xx → error,4xx → warning,其它 → info
GORM 集成
import "gitee.com/code-pilot/go-box/logger"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.NewGormLogger(),
})
SQL 日志分流:
| 情况 |
写入分类 |
级别 |
| 正常 SQL |
sql |
Debug |
| 慢查询(超过阈值) |
warning |
Warn |
| 执行错误 |
error |
Error |
API 一览
初始化与生命周期
logger.Init(dir string) error
logger.InitWithConfig(cfg *Config) error
logger.Close() error
logger.Sync() error
logger.BaseDir() string
分类日志
logger.Info(msg, fields...)
logger.Warning(msg, fields...)
logger.Error(msg, fields...)
logger.Debug(msg, fields...)
logger.SQL(msg, fields...)
logger.Access(msg, fields...)
logger.Infof(format, args...)
logger.Warningf(format, args...)
logger.Errorf(format, args...)
logger.Debugf(format, args...)
带 context(自动注入 trace_id)
logger.Ctx(ctx).Info(msg, fields...)
logger.Ctx(ctx).Warning(msg, fields...)
logger.Ctx(ctx).Error(msg, fields...)
logger.Ctx(ctx).Debug(msg, fields...)
logger.Ctx(ctx).SQL(msg, fields...)
logger.Ctx(ctx).Access(msg, fields...)
logger.Ctx(ctx).Infof(format, args...)
logger.Ctx(ctx).Errorf(format, args...)
链路追踪
logger.NewTraceID() string
logger.WithTraceID(ctx, id) context.Context
logger.GetTraceID(ctx) string
logger.EnsureTraceID(ctx) (context.Context, string)
Gin
logger.GinAccess() gin.HandlerFunc
logger.GinRecovery() gin.HandlerFunc
logger.FromGin(c *gin.Context) *CtxLogger
GORM
logger.NewGormLogger() *GormLogger
输出示例
控制台(console 格式)
2026-09-30 16:52:54.145 INFO main.go:20 服务启动 {"category":"info","addr":":8080"}
2026-09-30 16:52:54.145 WARN main.go:21 配置缺失 {"category":"warning"}
2026-09-30 16:52:54.145 ERROR main.go:22 数据库连接失败 {"category":"error","stacktrace":"..."}
文件(json 格式)
{"level":"INFO","time":"2026-09-30 16:52:54.145","caller":"main.go:20","msg":"服务启动","category":"info","addr":":8080"}
{"level":"DEBUG","time":"2026-09-30 16:52:54.145","caller":"main.go:28","msg":"SELECT * FROM users","category":"sql","id":1,"trace_id":"e58d80fedfd3d13833402c9a1e8ddc71"}
{"level":"INFO","time":"2026-09-30 16:52:54.146","caller":"middleware.go:20","msg":"GET /api/user","category":"access","status":200,"cost":"12ms","trace_id":"e58d80fedfd3d13833402c9a1e8ddc71"}
依赖
go get go.uber.org/zap
go get github.com/gin-gonic/gin # 可选,用 Gin 集成时
go get gorm.io/gorm # 可选,用 GORM 集成时
设计原则
- 入口极简:一行
logger.Init("./logs") 即可用
- 零配置可用:默认配置覆盖 90% 场景
- 分类清晰:不同用途的日志物理隔离
- 不 panic:初始化失败返回 error,懒初始化兜底
- 易扩展:包装底层实现,未来可替换
License
MIT