Files

124 lines
6.7 KiB
Markdown
Raw Permalink Normal View History

2025-12-11 07:24:36 +08:00
---
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()` 机制。