创建仓库
This commit is contained in:
123
Go项目实战/03_基础设施/02_日志/04_架构逻辑.md
Normal file
123
Go项目实战/03_基础设施/02_日志/04_架构逻辑.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
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()` 机制。
|
||||
Reference in New Issue
Block a user