logger

package
v0.0.0-...-46e42da Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 19 Imported by: 0

README

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)   ✅ 跨进程传递
关键点
  1. ctx 必须一路传下去:handler → service → repo → db,每一层都接收并传递 ctx
  2. ctx 是第一个参数:Go 官方约定,不要放后面
  3. ctx 不要存到 struct 里:它的生命周期是请求级的,存 struct 会出问题
  4. 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 自动完成:

  1. 从 X-Trace-Id 请求头读取 trace_id,没有就新建
  2. 写入 c.Request.Context(),后续 logger.Ctx(ctx) 自动带上
  3. 通过 X-Trace-Id 响应头返回给前端
  4. 请求结束记录 access 日志:方法、路径、query、状态码、耗时、IP、UA
  5. 按状态码分流: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

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Access

func Access(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 access 日志

func BaseDir

func BaseDir() string

--- @ty.aicoding@ --- 返回当前日志目录的绝对路径

func Close

func Close() error

--- @ty.aicoding@ --- 关闭所有 writer 并重置全局状态

func Debug

func Debug(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 debug 日志

func Debugf

func Debugf(format string, args ...any)

--- @ty.aicoding@ --- 格式化输出 debug 日志

func EnsureTraceID

func EnsureTraceID(ctx context.Context) (context.Context, string)

--- @ty.aicoding@ --- EnsureTraceID 确保 context 里有 trace_id,没有就新建

func Error

func Error(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 error 日志

func Errorf

func Errorf(format string, args ...any)

--- @ty.aicoding@ --- 格式化输出 error 日志

func GetTraceID

func GetTraceID(ctx context.Context) string

--- @ty.aicoding@ --- GetTraceID 从 context 读取 trace_id

func GinAccess

func GinAccess() gin.HandlerFunc

--- @ty.aicoding@ --- GinAccess 返回 Gin 的访问日志中间件 自动:

  1. 从 Header(X-Trace-Id) 或 新建 trace_id
  2. 写入 context,后续 logger.Ctx(ctx) 能自动带上
  3. 请求结束记录 access 日志(路径、方法、状态、耗时、IP、UA)

func GinRecovery

func GinRecovery() gin.HandlerFunc

--- @ty.aicoding@ --- GinRecovery 捕获 panic 并记录到 error 日志

func Info

func Info(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 info 日志

func Infof

func Infof(format string, args ...any)

--- @ty.aicoding@ --- 格式化输出 info 日志

func Init

func Init(dir string) error

--- @ty.aicoding@ --- Init 初始化日志库(默认配置),只需传目录

func InitError

func InitError() error

--- @ty.aicoding@ --- InitError 返回最近一次初始化(用户显式调用或惰性触发)的错误

func InitWithConfig

func InitWithConfig(c *Config) (err error)

--- @ty.aicoding@ --- InitWithConfig 自定义配置初始化

func NewTraceID

func NewTraceID() string

--- @ty.aicoding@ --- NewTraceID 生成 trace_id

func SQL

func SQL(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 SQL 日志

func Sync

func Sync() error

--- @ty.aicoding@ --- 把所有分类的缓冲区刷盘

func Warning

func Warning(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 warning 日志

func Warningf

func Warningf(format string, args ...any)

--- @ty.aicoding@ --- 格式化输出 warning 日志

func WithTraceID

func WithTraceID(ctx context.Context, traceID string) context.Context

--- @ty.aicoding@ --- WithTraceID 把 trace_id 写入 context

Types

type Category

type Category string

--- @ty.aicoding@ --- Category 日志分类

const (
	CatInfo    Category = "info"
	CatWarning Category = "warning"
	CatError   Category = "error"
	CatDebug   Category = "debug"
	CatSQL     Category = "sql"
	CatAccess  Category = "access"
)

type Config

type Config struct {
	// Dir 日志目录
	Dir string

	// Level 全局最低级别:debug/info/warn/error
	Level string

	// Encoding 文件编码:console / json
	Encoding string

	// Console 是否同时输出到控制台
	Console bool

	// Caller 是否记录调用者
	Caller bool

	// MaxSizeMB 单个日志文件最大 MB,超过则切割
	MaxSizeMB int

	// MaxAgeDays 日志保留天数,超过自动删除
	MaxAgeDays int

	// MaxBackups 每个分类最多保留的旧文件数(0 表示不限制)
	MaxBackups int

	// Compress 是否压缩旧日志(gzip)
	Compress bool

	// ErrorStack 是否给 error 级别加堆栈
	ErrorStack bool
}

--- @ty.aicoding@ --- Config 日志配置

func DefaultConfig

func DefaultConfig(dir string) *Config

--- @ty.aicoding@ --- DefaultConfig 默认配置

type CtxLogger

type CtxLogger struct {
	// contains filtered or unexported fields
}

--- @ty.aicoding@ --- 绑定 ctx 的 logger,输出时自动带上 trace_id

func Ctx

func Ctx(ctx context.Context) *CtxLogger

--- @ty.aicoding@ --- 绑定 ctx,之后输出的日志自动带上 trace_id

func (*CtxLogger) Access

func (c *CtxLogger) Access(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 access 日志,自动携带 trace_id

func (*CtxLogger) Debug

func (c *CtxLogger) Debug(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 debug 日志,自动携带 trace_id

func (*CtxLogger) Debugf

func (c *CtxLogger) Debugf(format string, args ...any)

--- @ty.aicoding@ --- 格式化输出 debug 日志,自动携带 trace_id

func (*CtxLogger) Error

func (c *CtxLogger) Error(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 error 日志,自动携带 trace_id

func (*CtxLogger) Errorf

func (c *CtxLogger) Errorf(format string, args ...any)

--- @ty.aicoding@ --- 格式化输出 error 日志,自动携带 trace_id

func (*CtxLogger) Info

func (c *CtxLogger) Info(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 info 日志,自动携带 trace_id

func (*CtxLogger) Infof

func (c *CtxLogger) Infof(format string, args ...any)

--- @ty.aicoding@ --- 格式化输出 info 日志,自动携带 trace_id

func (*CtxLogger) SQL

func (c *CtxLogger) SQL(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 SQL 日志,自动携带 trace_id

func (*CtxLogger) Warning

func (c *CtxLogger) Warning(msg string, fields ...zap.Field)

--- @ty.aicoding@ --- 输出 warning 日志,自动携带 trace_id

func (*CtxLogger) Warningf

func (c *CtxLogger) Warningf(format string, args ...any)

--- @ty.aicoding@ --- 格式化输出 warning 日志,自动携带 trace_id

type GormLogger

type GormLogger struct {
	SlowThreshold time.Duration
	LogLevel      gormlogger.LogLevel
}

GormLogger 把 GORM 的 SQL 日志接入到 logger 的 sql 分类

func NewGormLogger

func NewGormLogger() *GormLogger

func (*GormLogger) Error

func (g *GormLogger) Error(ctx context.Context, msg string, data ...interface{})

func (*GormLogger) Info

func (g *GormLogger) Info(ctx context.Context, msg string, data ...interface{})

func (*GormLogger) LogMode

func (*GormLogger) Trace

func (g *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error)

func (*GormLogger) Warn

func (g *GormLogger) Warn(ctx context.Context, msg string, data ...interface{})

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

--- @ty.aicoding@ --- Logger 单分类 logger

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL