Files
loggerx/readme.md
T
2026-09-14 00:14:30 +08:00

233 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# loggerx
基于 Go 原生 `log` 封装的日志库:支持按时间切分文件、同步/异步落盘、channel 分目录、Gin 中间件。
```go
log := loggerx.NewLogger(ctx, loggerx.SetDir("./log"), loggerx.SetToConsole())
defer log.Close() // 退出前务必调用,否则最后一批日志(最多 32KB)不会落盘
log.Info(ctx, "hello")
log.Infof(ctx, "hello %s", "world")
```
## 用法
### 创建与选项
```go
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.SetErrorToInfo(), // error 是否同时写入 info 日志
loggerx.SetExpandData("app", "order"), // 每条日志追加固定字段
loggerx.SetExtraDriver(f, hooks), // 额外落盘驱动(实现 io.Writer 即可)
loggerx.SetPrintFile(false), // 不写文件,只走驱动
)
```
### 定时刷盘:把崩溃丢失窗口压到最小
默认不刷盘时,日志攒在 32KB 内存缓冲里,进程被 `kill -9` 最多丢 32KB。
开启定时刷盘后,丢失量收敛为「一个间隔内产生的日志量」:
```go
log := loggerx.NewLogger(ctx,
loggerx.SetDir("./log"),
loggerx.SetFlushInterval(200*time.Millisecond), // 每 200ms 刷一次
)
```
刷盘由后台协程完成,失败只记日志、不会中断写入;`Close()` 会等它退出。
### 按大小切割 + 自动压缩归档
```go
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 分目录
```go
log.Channel("order").Info(ctx, "下单") // 落到 ./log/order/2026-09-13_info.log
log.Channel("pay").Error(ctx, "支付失败")
```
### 同步 / 异步
```go
log.Info(ctx, "默认同步") // 写入内存缓冲(32KB 满才落盘)
log.WriteAsync().Info(ctx, "这条异步") // 交给后台协程消费
loggerx.SetWriteAsync() // 全局异步
```
异步是**有界阻塞队列**(容量 1000):队列满时写入方会等待,不会丢日志。
`Close()` 会等队列排空并把缓冲刷盘。
### Gin 中间件
```go
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` 也收进日志文件时,显式打开:
```go
log := loggerx.NewLogger(ctx, loggerx.SetDir("./log"), loggerx.SetAsGlobalLog())
defer log.Close() // Close 会把全局 log 还原成接管前的 writer
```
`*Logger` 本身实现 `io.Writer`,也可以直接塞给任何需要 writer 的地方:
```go
log.SetOutput(loggerx.NewLogger(ctx, loggerx.SetDir("./log")))
```
### 输出格式与前缀
```go
// 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 的理由是「人读着方便」,不是性能。
### 级别过滤
```go
loggerx.SetMinLevel(loggerx.LevelInfo) // 生产常用:Debug 直接丢弃,连 JSON 都不序列化
loggerx.SetMinLevel(loggerx.LevelOff) // 全关
```
### 把日志库自身的故障暴露出来
磁盘满、句柄失效、归档失败、刷盘超时这类问题默认只体现在返回值里,而调用方通常忽略返回值。
注册回调后可以接到告警或一个「不会失败」的输出:
```go
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. [X] 自动清除过期的日志文件(`.log``.gz` 都清)
2. [X] 支持日志文件压缩(滚动归档自动 gzip)
3. [X] 支持日志文件切割
4. [ ] 支持日志文件归档到对象存储
5. 支持多种文件分割类型
1. [X] 按照时间分割
2. [X] 按照文件大小分割(`SetSizeSplit`
3. [ ] 按照日志行数分割
6. [X] 支持日志级别过滤(`SetMinLevel`
7. [X] 异步落盘(按实例隔离,不再是全局队列)
8. [X] 支持是否转义 HTML
9. [X] 支持定时刷盘(`SetFlushInterval`
10. [X] 支持 text / json 两种输出格式(`SetFormat`)与行前缀(`SetPrefix`
11. [ ] 支持采样(高频日志降采样)