182 lines
6.9 KiB
Markdown
182 lines
6.9 KiB
Markdown
---
|
||
tags: []
|
||
aliases:
|
||
- 《日志模块开发规范与质量保证手册》
|
||
- 一、 核心开发规范 (The Golden Rules)
|
||
date created: 星期三, 十二月 10日 2025, 10:53:19 晚上
|
||
date modified: 星期三, 十二月 10日 2025, 11:31:04 晚上
|
||
---
|
||
|
||
# 《日志模块开发规范与质量保证手册》
|
||
|
||
---
|
||
|
||
## 一、 核心开发规范 (The Golden Rules)
|
||
|
||
这部分是“软约束”,属于团队共识,通过 Code Review 和 AI 辅助检查来执行。
|
||
|
||
### 1. 键名命名公约 (Key Naming Convention)
|
||
|
||
日志是给机器(ELK/Loki)读的,键名必须统一,方便建立索引。
|
||
|
||
- **规则**: 严禁使用 CamelCase (小驼峰) 或 PascalCase (大驼峰),**必须且只能使用 snake_case (下划线命名)**。
|
||
- **反例**: `userId`, `IPAddress`, `httpStatus`
|
||
- **正例**: `user_id`, `client_ip`, `http_status`
|
||
- **理由**: 多数数据库和搜索引擎(如 Elasticsearch)的分词器对下划线更友好,且 SQL 查询习惯也是下划线。
|
||
|
||
### 2. 类型安全铁律 (Type Safety Strictness)
|
||
|
||
利用 Zap 的强类型优势,拒绝隐式转换。
|
||
|
||
- **规则**: 在业务热点路径(Hot Path)中,**严禁使用 `zap.Any`、`zap.Reflect` 或 `Sugar` 模式**。
|
||
- **例外**: 仅在应用启动(Init)、Panic 恢复或非高频的配置加载阶段允许使用 `SugaredLogger`。
|
||
- **理由**: `zap.Any` 会触发反射(Reflection),导致内存逃逸和 GC 压力。这是高性能系统的“隐形杀手”。
|
||
|
||
### 3. 上下文优先原则 (Context First)
|
||
|
||
日志不是孤岛,必须依附于请求上下文。
|
||
|
||
- **规则**: 所有 Controller、Service、Repository 层的方法,如果需要打印日志,**必须**使用 `log.WithContext(ctx).Info(…)` 及其变体。
|
||
- **禁止**: 严禁在业务流程中直接调用全局的 `log.Info(…)`(除非是系统级事件,如定时任务启动)。
|
||
- **理由**: 只有通过 `WithContext`,才能将 TraceID 串联起来。
|
||
|
||
### 4. 哨兵值与魔法字符串 (Sentinels & Magic Strings)
|
||
|
||
- **规则**: 核心日志字段的 Key 必须定义为常量(Constant)。
|
||
- **实现**: 在 `pkg/log/standard.go` 中定义 `const TraceIDKey = "trace_id"`。
|
||
- **禁止**: 代码中出现手写的 `zap.String("trace_id", …)`,防止拼写错误(如写成 `traceid`)。
|
||
|
||
### 5. 热点路径复用原则 (Hot Path Reuse)
|
||
|
||
针对循环(Loop)或复杂长流程函数,严禁重复构建 Context Logger。
|
||
|
||
- **规则**: 必须在作用域入口处初始化 Logger 实例,并在该作用域内复用。
|
||
- **反例 (Bad)**:
|
||
|
||
```Go
|
||
for _, item := range items {
|
||
// ❌ 每次循环都分配内存
|
||
log.WithContext(ctx).Info("processing", zap.String("id", item.ID))
|
||
}
|
||
```
|
||
|
||
- **正例 (Good)**:
|
||
|
||
```Go
|
||
// ✅ 只分配一次,复用 l
|
||
l := log.WithContext(ctx)
|
||
for _, item := range items {
|
||
l.Info("processing", zap.String("id", item.ID))
|
||
}
|
||
```
|
||
|
||
- **理由**: 减少大量临时的 `zap.Logger` 结构体分配,降低 GC 的 Scavenge 阶段耗时。
|
||
|
||
### 6. 后台任务播种原则 (Background Trace Seeding)
|
||
|
||
所有非 HTTP 触发的后台任务入口(Goroutine, Cron, MQ Handler),必须是“有状态”的。
|
||
|
||
- **规则**: 任务的第一行代码必须调用 `StartBackgroundTrace`。
|
||
- **反例 (Bad)**:
|
||
|
||
```Go
|
||
func ProcessOrder(msg []byte) {
|
||
ctx := context.Background()
|
||
// ❌ 此时 ctx 空空如也,日志将丢失 TraceID
|
||
log.WithContext(ctx).Info("processing order")
|
||
}
|
||
```
|
||
|
||
- **正例 (Good)**:
|
||
|
||
```Go
|
||
func ProcessOrder(msg []byte) {
|
||
// ✅ 自动生成一个新的 TraceID 注入 ctx
|
||
ctx := log.StartBackgroundTrace(context.Background())
|
||
log.WithContext(ctx).Info("processing order")
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 二、 Linter 规则配置 (Automated Enforcement)
|
||
|
||
这部分是“硬约束”,我们将在 `.golangci.yml` 中配置这些规则,强行阻断不合规代码的提交。
|
||
|
||
### 1. 禁用标准库日志 (`depguard`)
|
||
|
||
防止开发人员手滑使用了 Go 原生的 `log` 或 `fmt` 打印日志。
|
||
|
||
Linter: depguard
|
||
|
||
配置策略:
|
||
|
||
- **Deny**:
|
||
- `log`: 标准库日志(无结构化,无法分级)。
|
||
- `fmt.Print*`: 控制台打印(生产环境绝对禁止)。
|
||
- `github.com/sirupsen/logrus`: 防止引入其他日志库。
|
||
|
||
### 2. 强制错误处理 (`errcheck`)
|
||
|
||
Zap 的 `Sync()` 方法可能会返回错误(特别是在 Linux 的 `/dev/stdout` 上),通常需要忽略,但写入文件的错误不能忽略。
|
||
|
||
Linter: errcheck / gosec
|
||
|
||
配置策略:
|
||
|
||
- 对 `logger.Sync()` 的错误处理进行豁免(Exclude),因为在某些 OS 下 stdout sync 必然报错,这是已知 issue。
|
||
- 但对 `logger.Info` 等方法的 IO 错误,原则上 Zap 内部处理了,不需要业务层捕获。
|
||
|
||
### 3. 自定义规则 (`ruleguard` - 高级)
|
||
|
||
标准的 Linter 无法检测“键名必须是 snake_case”。如果需要极致的管控,我们可以引入 `ruleguard`。
|
||
|
||
AI 辅助检查逻辑:
|
||
|
||
由于配置 ruleguard 较复杂,我们约定在 AI 生成代码阶段 执行此逻辑:
|
||
|
||
- **Check 1**: 正则匹配所有 `zap.String("([a-z]+[A-Z][a-z]+)", …)` 模式,如果发现驼峰命名,立刻自我修正。
|
||
- **Check 2**: 扫描代码中是否存在 `fmt.Print`,如有则报错。
|
||
|
||
---
|
||
|
||
## 三、 安全与脱敏规范 (Security & Masking)
|
||
|
||
这是日志系统的“红线”。
|
||
|
||
### 1. PII (个人敏感信息) 零容忍
|
||
|
||
- **黑名单字段**: `password`, `token`, `access_token`, `refresh_token`, `credit_card`, `id_card`.
|
||
- **处理方式**:
|
||
- **方案 A (拦截器)**: 在 `zapcore` 层加 Hook,但这会损耗性能。
|
||
- **方案 B (显式脱敏)**: 要求 AI 在生成代码时,对于敏感字段,自动包裹脱敏函数。例如 `zap.String("mobile", mask.Mobile(u.Mobile))`。
|
||
- **决策**: 采用 **方案 B**。依赖编码时的自觉和 AI 的辅助,性能最优。
|
||
|
||
### 2. 大字段截断
|
||
|
||
- **规则**: 禁止将 Base64 图片数据、巨大的 HTML 内容直接打入日志。
|
||
- **限制**: 单个 Field 的 Value 长度建议限制在 2KB 以内。
|
||
|
||
---
|
||
|
||
## 四、 AI 辅助编码的“质量契约” (AI Quality Contract)
|
||
|
||
为了确保我(AI)生成的代码符合上述规范,请你(用户)在审查我的代码时,使用以下 **Checklist** 进行验证。这也是我对你的承诺:
|
||
|
||
1. **Imports 检查**: 确认没有引入 `log` 或 `fmt`。
|
||
2. **Context 检查**: 确认 `log.WithContext(ctx)` 是日志调用的唯一起手式。
|
||
3. **Keys 检查**: 确认所有 JSON Key 都是 `snake_case`。
|
||
4. **Args 检查**: 确认使用的是 `zap.String/Int` 等强类型构造器,而非 `zap.Any`。
|
||
5. **Config 检查**: 确认没有硬编码的路径(如 `/var/log`),必须来自 `options.go`。
|
||
|
||
---
|
||
|
||
## 五、 总结与下一步
|
||
|
||
我们确立了:
|
||
|
||
1. **命名**: 强制 snake_case。
|
||
2. **类型**: 拒绝 `zap.Any`,拒绝 `fmt`。
|
||
3. **上下文**: 强制 `WithContext`。
|
||
4. **安全**: 显式脱敏。
|