Files
2025-12-11 07:24:36 +08:00

124 lines
6.7 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:
- 1. 代码组织方式 (Code Organization)
date created: 星期三, 十二月 10日 2025, 10:42:21 晚上
date modified: 星期三, 十二月 10日 2025, 11:38:44 晚上
---
# 1. 代码组织方式 (Code Organization)
我们将遵循 **“高内聚、低耦合”** 的原则,将日志模块放置在 `internal/pkg/log` 下。这里是所有日志逻辑的物理家园。
建议的文件结构如下(逻辑分层):
- **`log.go` (Facade/Entry Point)**:
- 这是对外暴露的统一入口。包含全局单例的定义、初始化函数 (`Init`)、以及最常用的静态方法代理(如 `Info`, `Error`, `WithContext`)。
- **设计意图**: 让其他模块只 import 这一个包就能完成 90% 的工作。
- **`options.go` (Configuration)**:
- 定义配置结构体Level, Filename, MaxSize, MaxAge 等)。
- **设计意图**: 将配置解析逻辑与日志初始化逻辑分离,方便单元测试。
- **`zap.go` (Core Implementation)**:
- 负责 `zap.Logger` 的具体构建。包含 Encoder 配置JSON vs Console、Writer 配置Lumberjack 集成)和 Level 动态调整逻辑。
- 这是“脏活累活”集中的地方,屏蔽 Zap 的复杂构建细节。
- **`context.go` (The Bridge)**:
- **核心组件**。实现 `TraceID` 的提取逻辑。
- 定义如何从 `context.Context` 中挖掘元数据,并将其转化为 `zap.Field`
---
# 2. 调用方式与依赖注入 (Invocation & DI)
这里有一个经典的架构冲突:**Singleton单例 vs Dependency Injection依赖注入**。我们的策略是 **“依赖注入为主,单例为辅”**,但在具体使用上有一个极其重要的**反直觉设计**。
## A. 为什么 Service 层不应保存 Request Logger
你可能会想在 Service 初始化时注入一个带 Context 的 Logger。
- **错误做法**: `type UserService struct { logger *zap.Logger }`,然后在请求进来时试图把 request-scoped 的 logger 塞进去。
- **架构事实**: 在 Wire 依赖注入中,`Service``Repository` 通常是 **单例 (Singleton)** 的(即整个应用生命周期只有一个实例)。
- **结论**: 你**不能**把属于某一次 HTTP 请求的 `TraceID` 注入到单例的 Struct 成员变量中。
## B. 正确的调用范式 (The Best Practice)
Logger 作为**工具能力**被注入Context 作为**请求参数**被传递。
1. **依赖注入 (Setup Phase)**:
-`NewUserUsecase` 时,注入基础的 `*zap.Logger`(不带 TraceID
- 这个 Logger 配置好了输出路径、Level 等全局属性。
2. **方法调用 (Runtime Phase)**:
- 在具体的方法(如 `Register`)中,使用 `log.WithContext(ctx)` 来“临时”生成一个带有 TraceID 的 Logger 实例。
**示例逻辑流**:
- **Struct 定义**: `struct { baseLogger *zap.Logger }`
- **方法内部**: `l := log.WithContext(ctx, u.baseLogger)` -> `l.Info("user registered")`
- **说明**: 这里的 `WithContext` 是一个纯内存操作(浅拷贝),开销极小,可以放心高频调用。
## C. 高性能场景:作用域复用 (Scoped Logger)
虽然 `log.WithContext` 是浅拷贝,但在循环或长链路中频繁调用仍会产生大量临时对象,增加 GC 压力。
- **反模式 (Anti-Pattern)**: 在 `for` 循环内部调用 `log.WithContext(ctx)`
- **最佳实践 (Best Practice)**: **作用域提升**。在函数或循环入口处调用一次 `WithContext`,生成局部变量 `l` (Logger),随后全程复用该变量。
---
# 3. 数据流与 TraceID 传递 (Data Flow)
这是实现“全链路可观测性”的生命线。数据流必须打通以下四个关卡:
## 关卡 1入口 (Entry - Middleware)
- **位置**: `internal/middleware/trace.go` (需新建) 或集成在 `response` 包中。
- **行为**: 当 HTTP 请求到达,生成一个 UUID。
- **动作**: 使用 `c.Set("X-Trace-ID", uuid)` 将其放入 Gin 的上下文存储中。同时,将其放入 HTTP Response **动作**:
1. 调用 `pkg/log.WithTraceID(ctx, uuid)``UUID` 注入标准 `Context`
2. 执行 `c.Request = c.Request.WithContext(newCtx)` 将其回写。
3. (可选) 同时放入 Gin 上下文存储和 Response Header 供前端使用。
## 关卡 2桥接 (Bridge - Context Adapter)
- **位置**: `internal/pkg/log/context.go`
- **设计原则**: `pkg/log` **不依赖** `gin`,只识别标准库 `context.Context`
- **行为**: `log.WithContext(ctx) 调用内部帮助函数 GetTraceID(ctx) 获取 TraceID。`
- **前置条件**: 必须依赖上游Middleware将 TraceID 提前注入到标准 Context 中。
- **输出**: 返回一个预置了 `zap.String("trace_id", id)` 字段的 Logger。
## 关卡 3穿透 (Propagation - Service/Repo)
- **行为**: 所有的业务方法签名必须包含 `ctx context.Context` 作为第一个参数。
- **动作**: 严禁在层级调用中丢弃 Context例如使用 `context.Background()` 替代传入的 ctx这会导致链路断裂。
## 关卡 4异步与后台边界 (Async & Background Boundary)
- **高危场景**: 在 Handler 中启动 Goroutine 处理耗时任务。
- **陷阱**: `gin.Context` 是非线程安全的。如果 Goroutine 执行时 HTTP 请求已结束Gin 会重置该 Context导致数据竞争或脏读。
- **解决方案**: 必须在主协程中执行 `ctx.Copy()`,将副本传递给 Goroutine。日志模块必须支持处理这种副本 Context。
- **新增场景:后台任务 (Background Tasks)**
- **场景**: 定时任务 (Cron)、消息队列消费者 (MQ Consumer)、系统初始化。
- **问题**: 初始 `context.Background()` 不包含 TraceID。
- **动作**: 必须调用 `log.StartBackgroundTrace(ctx)` 进行“播种”。该函数会检测 Context若无 TraceID 则生成新 ID 并注入,确保链路可追踪。
---
# 4. 关键架构思考:防腐层 (Anti-Corruption Layer)
我们在设计时还需考虑一层“防腐”。
- **问题**: 如果未来我们想给所有的日志加一个字段,比如 `env=prod`,或者想把所有的 `trace_id` 改名为 `traceId`
- **对策**: 所有的业务代码**严禁**直接手动构建 `zap.String("trace_id", …)`
- **约束**: 这个字段的 Key 必须定义在 `pkg/log` 的常量中,且只能由 `WithContext` 内部逻辑自动附加。业务开发者只负责传 Context不负责管 ID 怎么拼写。
---
# 总结
- **代码位置**: `internal/pkg/log`,包含 `log.go` (入口), `zap.go` (实现), `context.go` (桥接)。
- **调用方式**: 注入 Base Logger -> 方法内 `WithContext(ctx)` -> 打印。
- **数据流**: Middleware 生成 -> Gin Context 携带 -> Log Adapter 提取 -> Zap Field 输出。
- **并发安全**: 警惕 Gin Context 在 Goroutine 中的误用,强调 `Copy()` 机制。