100 lines
5.1 KiB
Markdown
100 lines
5.1 KiB
Markdown
|
|
---
|
|||
|
|
tags: []
|
|||
|
|
aliases:
|
|||
|
|
- 1. 核心引擎 (The Engine):Uber Zap
|
|||
|
|
date created: 星期三, 十二月 10日 2025, 10:28:15 晚上
|
|||
|
|
date modified: 星期三, 十二月 10日 2025, 10:29:20 晚上
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 1. 核心引擎 (The Engine):Uber Zap
|
|||
|
|
|
|||
|
|
行业共识 (Consensus):
|
|||
|
|
|
|||
|
|
在 Go 语言的高性能后端领域,go.uber.org/zap 是目前无可争议的事实标准(De Facto Standard)。
|
|||
|
|
|
|||
|
|
我的推荐:
|
|||
|
|
|
|||
|
|
坚定地使用 Zap,不要犹豫。
|
|||
|
|
|
|||
|
|
**老兵的经验谈 (Why & How):**
|
|||
|
|
|
|||
|
|
- **为何不是 Logrus?** Logrus 胜在 API 极其友好(兼容标准库),但它底层大量使用反射(Reflection)和锁,在高并发场景下是严重的性能瓶颈(GC 压力大)。
|
|||
|
|
- **为何不是 Slog (Go 1.21+)?** Slog 是 Go 官方推出的结构化日志接口。虽然它是未来,但目前的生态和性能优化(尤其是在 JSON 序列化的极致性能上)尚未完全超越 Zap。且 Zap 可以很方便地作为 Slog 的 Backend。但在本项目中,为了追求极致性能和成熟度,直接使用 Zap 原生 API 是最高效的。
|
|||
|
|
- **关键决策点**:
|
|||
|
|
- **Field 强类型**: 我们必须强制团队使用 `zap.String("key", "val")` 而非 `zap.Any("key", val)`。`Any` 会导致反射,破坏 Zap 的零内存分配(Zero Allocation)优势。这是代码审查(Code Review)的红线。
|
|||
|
|
- **Logger vs SugaredLogger**:
|
|||
|
|
- **核心业务链路 (Hot Path)**: 使用 `zap.Logger`(极致性能,但语法繁琐)。
|
|||
|
|
- **初始化/非热点代码**: 使用 `zap.SugaredLogger`(语法类似 `printf`,性能稍弱但开发快)。
|
|||
|
|
- **基线**: 我们的封装层默认暴露 `Logger` 能力,保留高性能入口。
|
|||
|
|
|
|||
|
|
# 2. 轮转插件 (Rotation): Lumberjack V2
|
|||
|
|
|
|||
|
|
行业共识 (Consensus):
|
|||
|
|
|
|||
|
|
日志切割看似简单,实则坑多(并发写冲突、文件重命名原子性、不同操作系统的文件锁差异)。
|
|||
|
|
|
|||
|
|
我的推荐:
|
|||
|
|
|
|||
|
|
使用 gopkg.in/natefinch/lumberjack.v2。
|
|||
|
|
|
|||
|
|
**老兵的经验谈:**
|
|||
|
|
|
|||
|
|
- **不要造轮子**: 我见过无数团队尝试自己写 `file.Write` 然后计数切割,最后都在“多进程并发写同一个日志文件”或者“日志压缩时导致 IO 飙升”这些问题上翻车。
|
|||
|
|
- **配置陷阱**:
|
|||
|
|
- `MaxSize`: 建议 **100MB**。太小导致文件碎片化,太大导致像 grep/vim 这种工具打开困难。
|
|||
|
|
- `MaxBackups`: 建议保留 **30-50 个**。
|
|||
|
|
- `MaxAge`: 建议 **7-14 天**。
|
|||
|
|
- **Compress**: 建议 **开启 (True)**。历史日志压缩存储(gzip)能节省 90% 以上的磁盘空间,这对于云盘成本控制非常重要。
|
|||
|
|
|
|||
|
|
# 3. 上下文管理 (Context Awareness): 自研封装层
|
|||
|
|
|
|||
|
|
这是我们作为“架构师”必须介入的地方。原生 Zap 不懂业务上下文,我们需要一个胶水层。
|
|||
|
|
|
|||
|
|
技术难点:
|
|||
|
|
|
|||
|
|
如何优雅地把 TraceID 塞进每一行日志?
|
|||
|
|
|
|||
|
|
设计路线:
|
|||
|
|
|
|||
|
|
我们需要定义一个轻量级的 Wrapper 或者 Helper 函数。
|
|||
|
|
|
|||
|
|
- **不要**:重写 `zap.Logger` 结构体的所有方法(那样维护成本太高)。
|
|||
|
|
- **要**:提供一个入口函数,例如 `log.WithContext(ctx)`。
|
|||
|
|
- **原理**:这个函数会从 `ctx` 取出 `TraceID`,然后调用 `zap.With(zap.String("trace_id", id))`,返回一个携带了该字段的子 Logger 实例。这是一次极低成本的指针操作。
|
|||
|
|
|
|||
|
|
# 4. 抽象策略与混合模式 (Hybrid Pattern)
|
|||
|
|
|
|||
|
|
结合你选择的 **Option A+B**,我们的技术实现路径如下:
|
|||
|
|
|
|||
|
|
1. **全局变量 (The Global)**:
|
|||
|
|
|
|||
|
|
- 在 `internal/pkg/log` 包内部维护一个私有的 `var globalLogger *zap.Logger`。
|
|||
|
|
- 利用 `sync.Once` 确保其并发安全的初始化。
|
|||
|
|
- **兜底策略**: 在 `init()` 函数中先给它一个默认的 `Console Logger`。这样即使开发者忘记调用 `InitLogger`,程序启动时的日志也不会 panic,只会打印到控制台。
|
|||
|
|
|
|||
|
|
2. **依赖注入 (The DI)**:
|
|||
|
|
|
|||
|
|
- 在 `internal/pkg/log` 暴露一个 `Provider` 函数,供 Wire 使用。
|
|||
|
|
- 这个 Provider 返回的必须是**同一个**底层 Zap 实例的引用(或者其派生),确保配置(如 Level、Output Path)是一致的。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 总结:最终的技术栈清单
|
|||
|
|
|
|||
|
|
|**组件**|**选型**|**理由**|
|
|||
|
|
|---|---|---|
|
|||
|
|
|**Logger Core**|`go.uber.org/zap` (v1.27+)|高性能、类型安全、零内存分配。|
|
|||
|
|
|**Rotation**|`gopkg.in/natefinch/lumberjack.v2`|成熟稳定,处理并发写文件不仅是事实标准,更是避坑指南。|
|
|||
|
|
|**Config**|`spf13/viper` (已引入)|利用现有的 Viper 读取 yaml 配置,实现动态等级调整。|
|
|||
|
|
|**Trace Inject**|`Custom Wrapper` (Standard Lib)|基于 `context` 的轻量封装,连接 `gin.Context` 与 `zap.Fields`。|
|
|||
|
|
|
|||
|
|
老师的最后叮嘱 (The Moral of the story):
|
|||
|
|
|
|||
|
|
我们现在的设计,本质上是在 Zap 的高性能 和 业务开发的便利性 之间走钢丝。
|
|||
|
|
|
|||
|
|
最大的风险在于:封装层写得太重。
|
|||
|
|
|
|||
|
|
如果我们在 log.Info 里面加了太多的锁、反射或者字符串拼接,那么引入 Zap 的意义就没了。所以,接下来的详细设计文档中,我们要时刻警惕“过度封装”。
|
|||
|
|
|
|||
|
|
如果这个技术栈基线你没有异议,我们就以此为基础,开始生成《全局日志模块详细设计规格说明书》。
|