Files
loggerx/readme.md
T
2026-10-05 19:30:40 +08:00

11 KiB
Raw Blame History

loggerx

基于 Go 原生 log 封装的日志库:支持按时间切分文件、同步/异步落盘、channel 分目录、Gin 中间件。

log := loggerx.NewLogger(ctx, loggerx.SetDir("./log"), loggerx.SetToConsole())
defer log.Close() // 退出前务必调用,否则最后一批日志(最多 32KB)不会落盘

log.Info(ctx, "hello")
log.Infof(ctx, "hello %s", "world")

用法

创建与选项

log := loggerx.NewLogger(ctx,
    loggerx.SetDir("./log"),                        // 日志目录,默认 ./log
    loggerx.SetToConsole(),                         // 同时输出到控制台
    loggerx.SetDays(7),                             // 保留天数,默认 7;<=0 表示不删除
    loggerx.SetTimeZone(time.FixedZone("CST", 8*3600)), // 时区,默认 time.Local
    loggerx.SetFileSplit(loggerx.FileSplitTimeE),   // 时间切割方式,默认按天
    loggerx.SetSizeSplit(64<<20),                   // 单文件上限 64MB,超过则滚动归档
    loggerx.SetCompress(true),                      // 归档是否压成 .gz,默认 true
    loggerx.SetCompressLevel(gzip.BestCompression), // 压缩级别 1~9,默认 BestSpeed
    loggerx.SetFlushInterval(200*time.Millisecond), // 定时刷盘,默认关闭
    loggerx.SetEscapeHTML(false),                   // 是否转义 HTML,默认 true
    loggerx.SetGID(false),                          // 是否记录 goroutine id,默认 true
    loggerx.SetFormat(loggerx.FormatJSON),          // 输出格式:FormatJSON(默认) / FormatText
    loggerx.SetPrefix("[order-svc] "),              // 每行日志前缀,便于区分来源
    loggerx.SetMinLevel(loggerx.LevelInfo),         // 最低输出级别,默认 LevelDebug(全输出)
    loggerx.SetErrorHandler(onLogError),            // 日志库自身故障回调(磁盘满/句柄失效等)
    loggerx.SetTraceField("trace_id"),              // trace 字段名,默认 trace_id
    loggerx.SetAllLevelToInfo(),                    // debug/warn/error 是否同时写入 info 日志
    loggerx.SetExpandData("app", "order"),          // 每条日志追加固定字段
    loggerx.SetExtraDriver(f, hooks),               // 额外落盘驱动(实现 io.Writer 即可)
    loggerx.SetPrintFile(false),                    // 不写文件,只走驱动
)

定时刷盘:把崩溃丢失窗口压到最小

默认不刷盘时,日志攒在 32KB 内存缓冲里,进程被 kill -9 最多丢 32KB。 开启定时刷盘后,丢失量收敛为「一个间隔内产生的日志量」:

log := loggerx.NewLogger(ctx,
    loggerx.SetDir("./log"),
    loggerx.SetFlushInterval(200*time.Millisecond), // 每 200ms 刷一次
)

刷盘由后台协程完成,失败只记日志、不会中断写入;Close() 会等它退出。

按大小切割 + 自动压缩归档

log := loggerx.NewLogger(ctx,
    loggerx.SetDir("./log"),
    loggerx.SetSizeSplit(64<<20), // 每个文件最多 64MB
    loggerx.SetCompress(true),    // 滚动后压成 .gz(默认开)
)

滚动过程(不阻塞写入):

  1. 关掉当前句柄(关句柄会先清空缓冲,归档内容因此完整)
  2. 改名成 2026-09-13_info_3.log(先改名,任何时刻文件都存在,崩溃也不丢)
  3. 后台协程压缩成 2026-09-13_info_3.log.gz,成功后删掉未压缩文件

细节保证:

  • 序号全局递增,且跳过已存在的文件,不覆盖历史归档;跨进程重启也从目录里续号
  • 单条日志超长(比上限还大)不会死循环,最坏就是该文件超限
  • 归档失败(如权限问题)时继续写原文件,宁可文件大一点也不丢日志
  • Close() 会等压缩协程收尾,返回后读归档不会读到半截 .gz
  • 过期清理(SetDays)同时作用于 .log 与 .gz

文件切割

取值 目录/文件名形态
FileSplitNone info.log
FileSplitTimeA 2026/09/13/15_info.log
FileSplitTimeB 2026/09/13_info.log
FileSplitTimeC 2026/09-13_info.log
FileSplitTimeD 2026-09-13-15_info.log
FileSplitTimeE 2026-09-13_info.log(默认)

channel 分目录

log.Channel("order").Info(ctx, "下单")   // 落到 ./log/order/2026-09-13_info.log
log.Channel("pay").Error(ctx, "支付失败")

同步 / 异步

log.Info(ctx, "默认同步")                  // 写入内存缓冲(32KB 满才落盘)
log.WriteAsync().Info(ctx, "这条异步")     // 交给后台协程消费
loggerx.SetWriteAsync()                    // 全局异步

异步是有界阻塞队列(容量 1000):队列满时写入方会等待,不会丢日志。 Close() 会等队列排空并把缓冲刷盘。

Gin 中间件

log := loggerx.NewLogger(context.Background(), loggerx.SetToConsole())
defer log.Close()

g := gin.New()
g.Use(gin.Recovery())
g.Use(middleware.SetGinTraceId(log)) // 读取/生成 trace id,并写回响应头 X-Trace-Id
g.Use(middleware.SetGinParams(log))  // 记录请求与响应(含 body,截断到 1000 字节)

g.GET("/ping", func(c *gin.Context) {
    // trace id 同时注入 gin.Context 与 request context,两种写法日志里都能带上
    log.Info(c, "via gin ctx")
    log.Info(c.Request.Context(), "via request ctx")
    c.JSON(200, gin.H{"trace_id": middleware.GetTraceId(c.Request.Context(), log.GetTraceField())})
})

trace id 的行为:

  • 优先取请求头 X-Trace-Id(上游透传,跨服务串起同一条链路),没有才生成
  • 生成后写回响应头,客户端/下游能拿到同一个 id
  • 上下文里的 key 用自定义类型(不是裸 string),避免与第三方库撞键
  • 需要自定义头名时用 middleware.SetGinTraceIdByKey("trace_id", "X-Request-Id")

与标准库 log 互通

默认不接管全局 log(只 import 本包不会产生任何副作用)。 需要把老代码里的 log.Printf 也收进日志文件时,显式打开:

log := loggerx.NewLogger(ctx, loggerx.SetDir("./log"), loggerx.SetAsGlobalLog())
defer log.Close() // Close 会把全局 log 还原成接管前的 writer

*Logger 本身实现 io.Writer,也可以直接塞给任何需要 writer 的地方:

log.SetOutput(loggerx.NewLogger(ctx, loggerx.SetDir("./log")))

输出格式与前缀

// JSON(默认):每行一条合法 JSON,带 level 字段,推荐给采集端,也是性能最好的一种
{"level":"info","time":"2026-09-13 14:07:39.731954","file":"/main.go:20","func":"main","gid":"7","content":["hello"]}

// FormatText:单行紧凑文本,适合人直接看
level=info time=2026-09-13 14:07:39.731954 file=/main.go:20 func=main gid=7 content=hello

两者都支持 SetPrefix,前缀加在行首([order-svc] level=info ...)。 text 格式下值里含空格/引号时会用 %q 包起来,保证一行一条且不歧义。

实测(本机 16 核):默认 JSON 约 10.2 µs/条,text 约 12.1 µs/条。 text 并非更快 —— 它走的是逐字段拼接,而 JSON 走的是已高度优化的 encoding/json。 选 text 的理由是「人读着方便」,不是性能。

级别过滤

loggerx.SetMinLevel(loggerx.LevelInfo) // 生产常用:Debug 直接丢弃,连 JSON 都不序列化
loggerx.SetMinLevel(loggerx.LevelOff)  // 全关

全量日志汇总到一个文件(SetAllLevelToInfo)

开启后,除 info 以外的所有级别(debug / warn / error)都会额外往同一个 channel 的 info 日志里再写一份,info.log 就是「一个文件看全量」的汇总文件:

log := loggerx.NewLogger(ctx, loggerx.SetDir("./log"), loggerx.SetAllLevelToInfo())

log.Info(ctx, "只进 info.log")
log.Warn(ctx, "进 warn.log,同时进 info.log")
log.Error(ctx, "进 error.log,同时进 info.log")

行为细节:

  • 原级别文件照旧写:warn.log / error.log 内容不变,info.log 里是副本
  • 副本保留原始 level 字段,因此在 info.log 里仍然能按级别区分和过滤(不会统一改写成 info)
  • info 本身不重复写,info.log 里不会出现两行相同的 info
  • 副本只落文件,不会再走一遍额外驱动(SetToConsole / SetExtraDriver),控制台不会出现重复行
  • 级别过滤优先:被 SetMinLevel 丢弃的日志不会因为它出现在 info.log 里
  • SetPrintFile(false) 时该选项无意义(没有文件可写)
  • 代价:info.log 体积约等于所有级别日志之和;异步模式下每条非 info 日志会多占一个 队列槽位(队列容量 1000,非 info 级别的有效缓冲因此减半)
  • 按时间/大小切割、过期清理照常生效

把日志库自身的故障暴露出来

磁盘满、句柄失效、归档失败、刷盘超时这类问题默认只体现在返回值里,而调用方通常忽略返回值。 注册回调后可以接到告警或一个「不会失败」的输出:

loggerx.NewLogger(ctx,
    loggerx.SetDir("./log"),
    loggerx.SetErrorHandler(func(err error) {
        fmt.Fprintln(os.Stderr, "loggerx:", err)
    }),
)

回调在写日志的调用栈上同步执行,务必保持轻量,且不要在里面再调用同一实例的日志方法。

落盘行为与保证

  • 缓冲:每条日志先写进 32KB 内存缓冲,写满才 write 一次系统调用。
  • 完整性:以下四种方式都会把缓冲落盘 —— 缓冲写满、Close()、MustSync()、SetFlushInterval 定时器。
  • 崩溃语义:进程被 kill -9、panic 未恢复、断电时,最多丢失最后 32KB 未落盘的日志; 开启 SetFlushInterval 后,丢失量收敛到「一个间隔内产生的日志量」。
  • 关闭语义:Close() 之后该实例不再接受写入(返回 loggerx: 日志已关闭); 会等异步队列排空、等压缩归档收尾;重复调用安全;不泄漏文件句柄。
  • 写失败:写文件失败会重试一次(重开句柄);磁盘满等持续失败时该条日志会丢, 错误通过 io.Writer 语义返回给调用方,建议对 Write/MustSync/Close 的返回值做检查。
  • channel 隔离:Channel("x") 的日志落在 <dir>/x/ 子目录,同步与异步模式都成立。

性能

本机(Windows / 16 核)实测,Go 1.26:

场景 单条耗时 分配
SetGID(false) + 只走驱动 3.4 µs 14
默认(含 gid,写文件) 10.8 µs 15
并行写入(16 goroutine) 19.6 µs 17

单条 10.8 µs 中约 4.3 µs 花在采集 goroutine id 上。对延迟敏感、日志量大的场景建议 SetGID(false),可省掉约 40% 开销。开启 SetSizeSplit 后每条日志会多一次 Stat(约 0.6 µs)用于判断是否该滚动;不配置大小切割则没有这笔开销。

开发计划

  1. 自动清除过期的日志文件(.log 与 .gz 都清)
  2. 支持日志文件压缩(滚动归档自动 gzip)
  3. 支持日志文件切割
  4. 支持日志文件归档到对象存储
  5. 支持多种文件分割类型
    1. 按照时间分割
    2. 按照文件大小分割(SetSizeSplit)
    3. 按照日志行数分割
  6. 支持日志级别过滤(SetMinLevel)
  7. 异步落盘(按实例隔离,不再是全局队列)
  8. 支持是否转义 HTML
  9. 支持定时刷盘(SetFlushInterval)
  10. 支持 text / json 两种输出格式(SetFormat)与行前缀(SetPrefix)
  11. 支持采样(高频日志降采样)