--- tags: [] aliases: - 日志模块工程化实施标准 date created: 星期三, 十二月 10日 2025, 10:58:53 晚上 date modified: 星期三, 十二月 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)`。这是线程安全的,无需重启实例。 ### 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 格式。 - **标准**: 1. 必须使用 `gin.New()` 初始化 Engine。 2. 必须手动注册我们自定义的 `middleware.Recovery` 和 `middleware.AccessLog`。 3. 确保 Panic 日志中包含 TraceID(从 `c.Request.Context` 中尝试恢复)。 --- ## 四、 总结与就绪确认 至此,我们已经完成了日志模块的**全生命周期设计**: 1. **架构**: 基础设施层,无业务依赖。 2. **技术栈**: Zap + Lumberjack + Context Adapter。 3. **模式**: 单例兜底 + 依赖注入,强类型约束。 4. **规范**: Snake_case 键名,中文友好文档,严禁 Fatal。