Files
Inbox/Go项目实战/03_基础设施/02_日志/07_日志模块工程化实施标准.md
2025-12-11 07:24:36 +08:00

147 lines
6.5 KiB
Markdown
Raw Permalink 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.

---
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。