6.5 KiB
tags, aliases, date created, date modified
| tags | aliases | date created | date modified | |
|---|---|---|---|---|
|
星期三, 十二月 10日 2025, 10:58:53 晚上 | 星期三, 十二月 10日 2025, 11:42:26 晚上 |
日志模块工程化实施标准
一、 注释与文档规范 (Documentation Standards)
目标:“中文友好 (Chinese Friendly)” 且 “符合 GoDoc 标准”。
我们采用 混合语言策略:结构定义用英文(为了 IDE 兼容性),业务解释用中文(为了团队协作)。
1. 导出的包与函数 (Exported Symbols)
所有对外暴露的函数(首字母大写),必须编写文档注释。
-
格式要求:
- 第一行:
// FunctionName 简短的英文或中文摘要(符合 Go Lint 检查)。 - 空一行。
- 详细说明:必须使用中文,解释函数的行为、副作用(Side Effects)和潜在风险。
- 参数说明:如果有复杂参数,使用
// - param: explanation格式。
- 第一行:
-
范例 (Style Guide):
// WithContext returns a logger with the trace ID injected.
//
// [功能]: 从 context.Context 中提取 TraceID 并附加到 Logger 字段中。
// [注意]: 这是一个轻量级操作,但如果 ctx 为 nil,将返回原始 Logger 的 fallback。
// [场景]: 务必在 Controller 或 Service 的入口处优先调用。
2. 内部实现细节 (Internal Logic)
对于 internal/pkg/log 内部复杂的逻辑(如 lumberjack 的配置转换),必须在代码块上方添加中文注释。
-
原则:解释 “为什么这么做 (Why)”,而不是“做了什么 (What)”。代码本身已经展示了做了什么。
-
范例:
// [Why]: 这里不使用 zap.NewProduction 自带的 OutputPaths,
// 因为我们需要同时输出到控制台 (为了 Docker 采集) 和文件 (为了本地容灾),
// 且文件输出需要通过 Lumberjack 进行轮转控制。
3. README 维护
在 internal/pkg/log/README.md 中维护一份**“速查手册”**。
- 必填内容:
- 如何在
config.yaml中配置(给出默认值)。 - 如何动态调整日志级别(如通过信号或 API)。
- 常见错误码(Code)与日志关键字的对应关系。
- 如何在
二、 可拓展性设计 (Extensibility Design)
虽然我们拒绝“过度封装”,但必须为未来的变化预留接口(Hook Points)。
1. 配置扩展:Functional Options 模式
我们在 Init 函数中,不应列出所有参数,而应使用 Option 模式。
- 设计:
func Init(opts …Option) error - 预留能力: 未来如果需要添加“发送日志到 Kafka”或“开启 Sentry 报警”,只需新增一个
WithKafka(addr)的 Option,而无需修改Init的函数签名,保证了对旧代码的兼容性。
2. 核心扩展:Zap Hooks
Zap 原生支持 Hooks。我们的封装必须暴露这一能力。
- 场景: 当日志级别为
Error或Fatal时,可能需要同步触发飞书/钉钉报警。 - 实现标准: 在
zap.go的构建逻辑中,检查配置是否定义了 Hooks。这允许我们在不侵入日志核心代码的情况下,挂载报警逻辑。
3. 字段扩展:Context Key Registry
随着业务发展,需要记录的元数据会增加(如 TenantID, RequestID, SpanID)。
- 标准: 不要在
context.go里写死 key 的提取逻辑。 - 设计: 定义一个
type ContextExtractor func(ctx) []Field类型。默认提供TraceIDExtractor。允许在初始化时注册新的 Extractor。这使得业务线可以自定义需要提取的 Context 字段。
三、 查漏补缺 (Gap Analysis)
在之前的讨论中,有几个隐蔽但致命的工程细节尚未覆盖,这里作为最后防线进行补充。
1. 关于 Logger.Fatal 的使用禁令
- 风险:
zap.Logger.Fatal会在打印日志后调用os.Exit(1)。 - 工程标准: 在 Web 服务(HTTP Server)中,严禁在业务逻辑层调用
Fatal。- 原因: 这会直接杀死整个进程,导致所有正在处理的请求中断(没有 Graceful Shutdown)。
- 替代: 遇到不可恢复错误,使用
Error级别日志,并返回500错误给客户端,由上层中间件处理。 - 例外: 仅在
main.go启动阶段(如连不上数据库、读不到配置)可以使用Fatal。
2. 时间格式的一致性
- 问题: Zap 默认的时间格式可能是浮点数(Unix Epoch)或非标准字符串。
- 标准: 生产环境统一配置为
ISO8601(2025-12-10T22:00:00.000Z)。- 理由: 这种格式跨时区友好,且能被几乎所有日志分析工具(ELK, Splunk, CloudWatch)自动识别并建立时间索引。
3. 动态日志级别 (Hot Reload)
- 需求: 线上出 Bug 时,需要临时把 Level 调成 Debug,查完再调回 Info,且不能重启服务。
- 实现标准: 利用
zap.AtomicLevel。- 我们需要暴露一个 HTTP 接口(如
PUT /admin/log/level)或监听配置文件的fsnotify事件。 - 收到变更信号后,直接调用
atomicLevel.SetLevel(zap.DebugLevel)。这是线程安全的,无需重启实例。
- 我们需要暴露一个 HTTP 接口(如
4. 测试支持 (Testing Support)
- 问题: 单元测试时,不仅不想看到日志刷屏,有时还需要断言“是否打印了某条错误日志”。
- 标准:
- 提供
pkg/log/test_helper.go。 - 封装
zaptest/observer。 - 允许测试代码通过
log.NewTestLogger()获取一个观察者对象,从而断言logs.FilterMessage("error").Len() == 1。
- 提供
5. 链路完整性保障
- 风险: 开发者容易遗忘在
go func()中传递 Context。 - 标准: 在 Code Review 时,重点检查所有
go关键字后是否跟随了 Context 的传递或播种操作。
6. 框架初始化与 Panic 处理
- 风险:
gin.Default()会自动注册只打印文本日志的 Recovery 中间件,破坏 JSON 格式。 - 标准:
- 必须使用
gin.New()初始化 Engine。 - 必须手动注册我们自定义的
middleware.Recovery和middleware.AccessLog。 - 确保 Panic 日志中包含 TraceID(从
c.Request.Context中尝试恢复)。
- 必须使用
四、 总结与就绪确认
至此,我们已经完成了日志模块的全生命周期设计:
- 架构: 基础设施层,无业务依赖。
- 技术栈: Zap + Lumberjack + Context Adapter。
- 模式: 单例兜底 + 依赖注入,强类型约束。
- 规范: Snake_case 键名,中文友好文档,严禁 Fatal。