6.5 KiB
6.5 KiB
tags, aliases, date created, date modified
| tags | aliases | date created | date modified | |
|---|---|---|---|---|
|
星期三, 十二月 10日 2025, 10:27:39 晚上 | 星期三, 十二月 10日 2025, 10:28:15 晚上 |
1. 核心设计目标 (Core Design Goals)
目标一:全链路上下文关联 (Contextual Traceability)
这是最核心的差异点。传统的 log.Println("Database error") 在并发环境下毫无价值,因为你不知道这条错误属于哪个请求。
- 设计要求:
- 自动注入 TraceID: 必须能够从
context.Context中提取TraceID(目前internal/pkg/app已经生成了 TraceID),并自动将其附加到每一条日志中。 - 请求元数据绑定: 除了 TraceID,还应支持自动绑定
UserID、IP、Method、Path等元数据,形成请求的完整快照。 - 跨组件穿透: 日志对象必须能够在 Layer 之间传递(例如 Controller -> Service -> Repository),且保持上下文不丢失。
- 自动注入 TraceID: 必须能够从
目标二:严格的结构化契约 (Strict Structured Schema)
日志是写给机器看的,不是写给通过 SSH 连上服务器的人看的。
- 设计要求:
- JSON First: 生产环境强制使用 JSON 格式。
- Schema 统一: 字段命名必须统一。例如,不要混用
uid,user_id,userId,必须在设计阶段锁定为 snake_case (如user_id)。 - 类型安全: 时间戳必须统一格式(推荐 ISO8601 或 Unix Nano),数字字段不能变成字符串(便于聚合计算)。
目标三:高性能与零侵入 (High Performance & Zero Allocation)
日志通常是系统中 IO 最密集的组件之一。
- 设计要求:
- 低 GC 压力: 利用 Zap 的核心优势,避免大量的
interface{}反射和字符串拼接,使用强类型的 Field(如zap.Int,zap.String)。 - 异步 IO (可选): 考虑是否引入 Buffer 机制(牺牲极端崩溃下的日志完整性换取吞吐量)。
- Level 级联过滤: 在 Debug 级别关闭时,Debug 级别的日志构造逻辑(如复杂的对象序列化)不应被执行。
- 低 GC 压力: 利用 Zap 的核心优势,避免大量的
目标四:安全与合规 (Security & Compliance)
这往往是被忽视的一点,也是导致安全事故的频发区。
- 设计要求:
- 敏感数据脱敏: 必须具备“黑名单”机制。任何包含
password,token,mobile,credit_card的字段在输出前必须被自动掩盖(Masking)。 - 安全截断: 防止打印过大的 Body(如 Base64 图片上传)导致磁盘爆满或日志系统瘫痪,限制单条日志最大长度。
- 敏感数据脱敏: 必须具备“黑名单”机制。任何包含
2. 场景化行为对比 (Dev Vs Prod)
为了兼顾开发体验和生产运维标准,我们需要在设计中明确区分两种环境的行为。
| 维度 | 开发环境 (Development) | 生产环境 (Production) | 设计意图 |
|---|---|---|---|
| 编码格式 | Console (彩色,人类易读) | JSON (机器易读) | 开发追求直观;生产追求 ELK 解析效率。 |
| 输出目标 | Stdout (控制台) | File + Stdout (双写) | 开发侧容器即焚;生产侧需持久化 + 容器采集。 |
| 日志级别 | Debug | Info / Warn | 生产环境过滤掉大量 Debug 噪音,节省存储成本。 |
| 堆栈追踪 | Error 级别即打印 | Panic 或 Fatal 才打印 | 减少生产环境日志体积,除非发生严重故障。 |
| 调用行号 | 显示 (Caller) | 显示 (Caller) | 快速定位代码位置。 |
3. 架构定位与边界 (Architecture Boundary)
我们需要明确日志模块在架构中的位置:
- 位置: 属于
Infrastructure Layer(Level 0/1)。 - 依赖关系:
- 被谁依赖: 所有层(Handler, Service, Repository)都依赖 Log。
- 依赖谁: 仅依赖标准库和第三方 Log Driver (Zap),不应依赖业务逻辑。
- 与其他模块的关系:
- vs
ecode:ecode定义错误的类型(Code),Log 记录错误的现场(Stack/Trace)。 - vs
app.Response: Response 负责对用户说话(经过清洗的、友好的信息),Log 负责对开发者说话(原始的、包含脏数据的真相)。
- vs
4. 深度反思与自我反驳 (Critical Thinking & Risk Analysis)
在敲定设计目标前,必须审视潜在的矛盾和风险:
反驳点 1:全链路上下文(TraceID)的传递成本
- 挑战: 要想让 Repository 层的日志也打出 TraceID,必须修改所有方法的签名为
func (ctx context.Context, …)。这对现有代码(如果是非 Context 风格)是巨大的重构。 - 回应: 我们的
Repository接口目前设计中已经包含了context.Context。这是一个必须遵守的“硬约束”。如果缺少 Context,日志将断层。 - 结论: 必须在规范中强调:所有层级的方法首个参数必须是 Context。
反驳点 2:脱敏机制的性能损耗
- 挑战: 如果每一条日志都要遍历字段去匹配“黑名单”进行正则替换,CPU 开销极大。
- 回应: 不能使用正则扫描全文。
- 修正方案: 利用 Zap 的
Hook或Core包装,仅针对特定 Key(如password)进行值替换,或者要求开发者在打印敏感结构体时显式调用.Reduct()方法,而非隐式全局扫描。隐式扫描在 Golang 中通常是性能杀手。
反驳点 3:异步写入的丢数据风险
- 挑战: 为了性能使用
Buffered Write,如果进程被kill -9或 Panic 崩溃,缓冲区日志会丢失,而崩溃前的日志往往最重要。 - 回应: 对于交易类系统,可靠性 > 吞吐量。
- 结论: 默认采用 同步写入 (Sync Write)。Zap 本身性能已经足够强(纳秒级),除非达到数万 QPS,否则不需要引入 Buffer。对于 Panic,必须使用
defer logger.Sync()确保刷盘。
5. 待确认问题 (Open Questions)
在进入下一步(编写详细规格说明书)之前,我需要确认你对以下几点的偏好:
-
全局单例 vs 纯依赖注入:
- 选项 A: 提供
log.Info()全局静态方法(方便,但有副作用)。 - 选项 B: 强制必须通过
l.Info()实例方法调用(架构更洁癖,但调用繁琐)。 - 推荐: 选项 A + B。提供全局方法作为快捷方式(底层代理到单例),同时支持 DI 注入。你倾向于哪种?
- 选项 A: 提供
-
日志轮转 (Rotation) 策略:
- 你是倾向于按 大小 切割(如 100MB 一个文件),还是按 时间 切割(每天一个文件)?
- 通常建议: 按大小切割(防止单文件过大无法打开),配合最大文件保留数。