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

6.5 KiB
Raw Blame History

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。我们的封装必须暴露这一能力。

  • 场景: 当日志级别为 ErrorFatal 时,可能需要同步触发飞书/钉钉报警。
  • 实现标准: 在 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.Recoverymiddleware.AccessLog
    3. 确保 Panic 日志中包含 TraceIDc.Request.Context 中尝试恢复)。

四、 总结与就绪确认

至此,我们已经完成了日志模块的全生命周期设计

  1. 架构: 基础设施层,无业务依赖。
  2. 技术栈: Zap + Lumberjack + Context Adapter。
  3. 模式: 单例兜底 + 依赖注入,强类型约束。
  4. 规范: Snake_case 键名,中文友好文档,严禁 Fatal。