创建仓库
This commit is contained in:
113
Go项目实战/03_基础设施/02_日志/01_设计目标.md
Normal file
113
Go项目实战/03_基础设施/02_日志/01_设计目标.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 核心设计目标 (Core Design Goals)
|
||||
date created: 星期三, 十二月 10日 2025, 10:27:39 晚上
|
||||
date modified: 星期三, 十二月 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),且保持上下文不丢失。
|
||||
|
||||
## 目标二:严格的结构化契约 (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 级别的日志构造逻辑(如复杂的对象序列化)不应被执行。
|
||||
|
||||
## 目标四:安全与合规 (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 负责**对开发者说话**(原始的、包含脏数据的真相)。
|
||||
|
||||
---
|
||||
|
||||
# 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)
|
||||
|
||||
在进入下一步(编写详细规格说明书)之前,我需要确认你对以下几点的偏好:
|
||||
|
||||
1. **全局单例 vs 纯依赖注入**:
|
||||
|
||||
- _选项 A_: 提供 `log.Info()` 全局静态方法(方便,但有副作用)。
|
||||
- _选项 B_: 强制必须通过 `l.Info()` 实例方法调用(架构更洁癖,但调用繁琐)。
|
||||
- _推荐_: **选项 A + B**。提供全局方法作为快捷方式(底层代理到单例),同时支持 DI 注入。你倾向于哪种?
|
||||
|
||||
2. **日志轮转 (Rotation) 策略**:
|
||||
|
||||
- 你是倾向于按 **大小** 切割(如 100MB 一个文件),还是按 **时间** 切割(每天一个文件)?
|
||||
- _通常建议_: 按大小切割(防止单文件过大无法打开),配合最大文件保留数。
|
||||
99
Go项目实战/03_基础设施/02_日志/02_技术栈基线.md
Normal file
99
Go项目实战/03_基础设施/02_日志/02_技术栈基线.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 核心引擎 (The Engine):Uber Zap
|
||||
date created: 星期三, 十二月 10日 2025, 10:28:15 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 10:29:20 晚上
|
||||
---
|
||||
|
||||
# 1. 核心引擎 (The Engine):Uber Zap
|
||||
|
||||
行业共识 (Consensus):
|
||||
|
||||
在 Go 语言的高性能后端领域,go.uber.org/zap 是目前无可争议的事实标准(De Facto Standard)。
|
||||
|
||||
我的推荐:
|
||||
|
||||
坚定地使用 Zap,不要犹豫。
|
||||
|
||||
**老兵的经验谈 (Why & How):**
|
||||
|
||||
- **为何不是 Logrus?** Logrus 胜在 API 极其友好(兼容标准库),但它底层大量使用反射(Reflection)和锁,在高并发场景下是严重的性能瓶颈(GC 压力大)。
|
||||
- **为何不是 Slog (Go 1.21+)?** Slog 是 Go 官方推出的结构化日志接口。虽然它是未来,但目前的生态和性能优化(尤其是在 JSON 序列化的极致性能上)尚未完全超越 Zap。且 Zap 可以很方便地作为 Slog 的 Backend。但在本项目中,为了追求极致性能和成熟度,直接使用 Zap 原生 API 是最高效的。
|
||||
- **关键决策点**:
|
||||
- **Field 强类型**: 我们必须强制团队使用 `zap.String("key", "val")` 而非 `zap.Any("key", val)`。`Any` 会导致反射,破坏 Zap 的零内存分配(Zero Allocation)优势。这是代码审查(Code Review)的红线。
|
||||
- **Logger vs SugaredLogger**:
|
||||
- **核心业务链路 (Hot Path)**: 使用 `zap.Logger`(极致性能,但语法繁琐)。
|
||||
- **初始化/非热点代码**: 使用 `zap.SugaredLogger`(语法类似 `printf`,性能稍弱但开发快)。
|
||||
- **基线**: 我们的封装层默认暴露 `Logger` 能力,保留高性能入口。
|
||||
|
||||
# 2. 轮转插件 (Rotation): Lumberjack V2
|
||||
|
||||
行业共识 (Consensus):
|
||||
|
||||
日志切割看似简单,实则坑多(并发写冲突、文件重命名原子性、不同操作系统的文件锁差异)。
|
||||
|
||||
我的推荐:
|
||||
|
||||
使用 gopkg.in/natefinch/lumberjack.v2。
|
||||
|
||||
**老兵的经验谈:**
|
||||
|
||||
- **不要造轮子**: 我见过无数团队尝试自己写 `file.Write` 然后计数切割,最后都在“多进程并发写同一个日志文件”或者“日志压缩时导致 IO 飙升”这些问题上翻车。
|
||||
- **配置陷阱**:
|
||||
- `MaxSize`: 建议 **100MB**。太小导致文件碎片化,太大导致像 grep/vim 这种工具打开困难。
|
||||
- `MaxBackups`: 建议保留 **30-50 个**。
|
||||
- `MaxAge`: 建议 **7-14 天**。
|
||||
- **Compress**: 建议 **开启 (True)**。历史日志压缩存储(gzip)能节省 90% 以上的磁盘空间,这对于云盘成本控制非常重要。
|
||||
|
||||
# 3. 上下文管理 (Context Awareness): 自研封装层
|
||||
|
||||
这是我们作为“架构师”必须介入的地方。原生 Zap 不懂业务上下文,我们需要一个胶水层。
|
||||
|
||||
技术难点:
|
||||
|
||||
如何优雅地把 TraceID 塞进每一行日志?
|
||||
|
||||
设计路线:
|
||||
|
||||
我们需要定义一个轻量级的 Wrapper 或者 Helper 函数。
|
||||
|
||||
- **不要**:重写 `zap.Logger` 结构体的所有方法(那样维护成本太高)。
|
||||
- **要**:提供一个入口函数,例如 `log.WithContext(ctx)`。
|
||||
- **原理**:这个函数会从 `ctx` 取出 `TraceID`,然后调用 `zap.With(zap.String("trace_id", id))`,返回一个携带了该字段的子 Logger 实例。这是一次极低成本的指针操作。
|
||||
|
||||
# 4. 抽象策略与混合模式 (Hybrid Pattern)
|
||||
|
||||
结合你选择的 **Option A+B**,我们的技术实现路径如下:
|
||||
|
||||
1. **全局变量 (The Global)**:
|
||||
|
||||
- 在 `internal/pkg/log` 包内部维护一个私有的 `var globalLogger *zap.Logger`。
|
||||
- 利用 `sync.Once` 确保其并发安全的初始化。
|
||||
- **兜底策略**: 在 `init()` 函数中先给它一个默认的 `Console Logger`。这样即使开发者忘记调用 `InitLogger`,程序启动时的日志也不会 panic,只会打印到控制台。
|
||||
|
||||
2. **依赖注入 (The DI)**:
|
||||
|
||||
- 在 `internal/pkg/log` 暴露一个 `Provider` 函数,供 Wire 使用。
|
||||
- 这个 Provider 返回的必须是**同一个**底层 Zap 实例的引用(或者其派生),确保配置(如 Level、Output Path)是一致的。
|
||||
|
||||
---
|
||||
|
||||
# 总结:最终的技术栈清单
|
||||
|
||||
|**组件**|**选型**|**理由**|
|
||||
|---|---|---|
|
||||
|**Logger Core**|`go.uber.org/zap` (v1.27+)|高性能、类型安全、零内存分配。|
|
||||
|**Rotation**|`gopkg.in/natefinch/lumberjack.v2`|成熟稳定,处理并发写文件不仅是事实标准,更是避坑指南。|
|
||||
|**Config**|`spf13/viper` (已引入)|利用现有的 Viper 读取 yaml 配置,实现动态等级调整。|
|
||||
|**Trace Inject**|`Custom Wrapper` (Standard Lib)|基于 `context` 的轻量封装,连接 `gin.Context` 与 `zap.Fields`。|
|
||||
|
||||
老师的最后叮嘱 (The Moral of the story):
|
||||
|
||||
我们现在的设计,本质上是在 Zap 的高性能 和 业务开发的便利性 之间走钢丝。
|
||||
|
||||
最大的风险在于:封装层写得太重。
|
||||
|
||||
如果我们在 log.Info 里面加了太多的锁、反射或者字符串拼接,那么引入 Zap 的意义就没了。所以,接下来的详细设计文档中,我们要时刻警惕“过度封装”。
|
||||
|
||||
如果这个技术栈基线你没有异议,我们就以此为基础,开始生成《全局日志模块详细设计规格说明书》。
|
||||
80
Go项目实战/03_基础设施/02_日志/03_核心设计模式.md
Normal file
80
Go项目实战/03_基础设施/02_日志/03_核心设计模式.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 核心模式:装饰器模式的变体 (Context-Decorator Pattern)
|
||||
date created: 星期三, 十二月 10日 2025, 10:37:54 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 10:38:26 晚上
|
||||
---
|
||||
|
||||
# 1. 核心模式:装饰器模式的变体 (Context-Decorator Pattern)
|
||||
|
||||
这是我们处理 `TraceID` 和上下文的核心手段。
|
||||
|
||||
- 传统误区 (Over-Abstraction):
|
||||
|
||||
定义一个庞大的 MyLogger 结构体,把 zap.Logger 藏在里面,然后重写 Info, Error 等所有方法。
|
||||
|
||||
- _后果_:维护成本极高,每次 Zap 更新或增加新特性(如 `Panic` 或 `DPanic`),你都得跟着改代码。且容易在转发参数时产生逃逸分析(Escape Analysis)导致的内存分配。
|
||||
- 我们的决策 (The Thin Wrapper):
|
||||
|
||||
只封装“获取 Logger”的动作,不封装“Logger 本身”。
|
||||
|
||||
我们将定义一个函数 log.WithContext(ctx context.Context) *zap.Logger。
|
||||
|
||||
- _行为_:这个函数极其轻量。它从 `ctx` 中取出 `TraceID`,调用 `zap.With()` 生成一个新的 Zap 实例并返回。
|
||||
- _优势_:业务代码拿到的依然是原生的 `*zap.Logger`。这意味着开发者可以直接使用 Zap 强大的 `zap.String`, `zap.Int` 等强类型字段构建方法,享受极致性能,没有任何中间层损耗。
|
||||
|
||||
# 2. 接口策略:拒绝通用接口 (Concrete Type Dependency)
|
||||
|
||||
这是 Go 语言工程实践中关于日志的一个特殊共识,也是反直觉的地方。
|
||||
|
||||
- 传统误区 (The Java/Interface Way):
|
||||
|
||||
定义一个 type ILogger interface { Info(msg string, args …interface{}) }。
|
||||
|
||||
- _后果_:`args …interface{}` 会导致大量的反射(Reflection)和装箱(Boxing),这直接抹杀了 Zap 存在的意义。Zap 的核心设计哲学就是通过 `zap.Field` 避免使用 `interface{}`。
|
||||
- 我们的决策 (Concrete Type):
|
||||
|
||||
直接依赖 *zap.Logger 具体类型。
|
||||
|
||||
- _原则_:在 Handler、Service、Repository 层,注入的类型就是 `*zap.Logger`。
|
||||
- _测试怎么办_:不要 Mock 日志接口。在单元测试中,直接传入 `zap.NewNop()`(什么都不做)或者 `zap.NewExample()`(输出到测试控制台)。这比 Mock 一个接口要简单且真实得多。
|
||||
|
||||
# 3. 访问模式:混合单例与依赖注入 (The Hybrid Accessor)
|
||||
|
||||
结合之前讨论的 Option A+B,我们通过设计模式来解决“初始化顺序”和“热加载”的问题。
|
||||
|
||||
- 设计挑战:
|
||||
|
||||
如果 main.go 还没来得及读配置初始化 Logger,其他 init() 函数里就调用了日志,程序会 Panic。
|
||||
|
||||
- **我们的决策 (Thread-Safe Proxy)**:
|
||||
- **原子替换 (Atomic Swap)**:全局变量 `globalLogger` 不会直接暴露给外部修改。我们将使用 `unsafe.Pointer` 或 `atomic.Value` (配合 Zap 的 `ReplaceGlobals`) 来保证在运行时重新加载配置(如动态修改 Log Level)时,不会发生并发读写冲突。
|
||||
- **懒汉式兜底 (Lazy Fallback)**:在 `internal/pkg/log` 的 `init()` 中,我们会默认初始化一个 `Console Logger`。这样即使 `main` 函数一行代码都没跑,只要引用了包,日志功能就是可用的(虽然配置是默认的)。这极大提升了开发体验(DX)。
|
||||
|
||||
# 4. 字段构建模式:结构化优先 (Field-First API)
|
||||
|
||||
这关乎团队的编码规范,属于 API 设计模式。
|
||||
|
||||
- 传统误区 (Printf Style):
|
||||
|
||||
使用 SugaredLogger 的 Infof("User %s login failed, error: %v", user, err)。
|
||||
|
||||
- _后果_:日志分析系统(ELK)只能拿到一串文本,无法对 `user` 进行聚合统计。
|
||||
- 我们的决策 (Structured Style):
|
||||
|
||||
默认只暴露 Logger(强类型),在必要时才暴露 SugaredLogger。
|
||||
|
||||
- _强制规范_:代码中必须写成 `log.Info("user login failed", zap.String("user", user), zap.Error(err))`。
|
||||
- _设计意图_:通过 API 的设计,“强迫”开发者思考每一个字段的语义。这虽然写起来繁琐一点,但对于后期的运维和排查是无价的。
|
||||
|
||||
---
|
||||
|
||||
# 总结:设计规格书的基调
|
||||
|
||||
基于以上讨论,在接下来的规格说明书中,我们将确立以下基调:
|
||||
|
||||
1. **不造轮子**:核心逻辑全权委托给 `zap` 和 `lumberjack`。
|
||||
2. **薄封装**:`pkg/log` 代码行数应控制在 200 行以内,只做配置解析和 Context 桥接。
|
||||
3. **强类型**:严禁在核心路径使用 `interface{}`。
|
||||
4. **显式传递**:通过 `WithContext` 显式传递上下文,而不是依赖某些黑魔法(如 Goroutine Local Storage)。
|
||||
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()` 机制。
|
||||
76
Go项目实战/03_基础设施/02_日志/05_目录结构与职责.md
Normal file
76
Go项目实战/03_基础设施/02_日志/05_目录结构与职责.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 目录结构与职责
|
||||
date created: 星期三, 十二月 10日 2025, 10:45:40 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:40:48 晚上
|
||||
---
|
||||
|
||||
# 目录结构与职责
|
||||
|
||||
## 1. 目录结构设计 (Directory Structure)
|
||||
|
||||
该结构旨在实现 **“配置分离”**、**“核心隐藏”** 与 **“上下文桥接”**。
|
||||
|
||||
```Plaintext
|
||||
internal/
|
||||
├── middleware/ # [Global] 全局中间件层
|
||||
│ ├── access_log.go # [New] HTTP 请求访问日志 (请求入/出记录, 耗时统计)
|
||||
│ └── trace.go # [New] 链路追踪 (生成/透传 TraceID -> 注入 Context)
|
||||
│
|
||||
└── pkg/
|
||||
└── log/ # [Level 0] 全局日志核心包 (基于 Zap)
|
||||
├── log.go # [Facade] 对外入口 (Init, Global L(), Static Proxies)
|
||||
├── options.go # [Config] 配置定义 (Level, FilePath, MaxSize)
|
||||
├── zap.go # [Core] Zap 实例构建 (Encoder, Core, AtomicLevel)
|
||||
├── writer.go # [IO] 输出源管理 (Lumberjack 轮转, Console/File 双写)
|
||||
├── context.go # [Bridge] 上下文桥接 (WithContext, TraceID 提取)
|
||||
└── standard.go # [Schema] 标准字段定义 (Standardized Field Constructors)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 文件职责详解 (Responsibilities)
|
||||
|
||||
### A. `internal/pkg/log` (核心日志包)
|
||||
|
||||
这是一个基础设施包,不应依赖任何业务逻辑(User, Order 等)。
|
||||
|
||||
| **文件名** | **职责描述** | **关键设计点 (Design Decisions)** |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`log.go`** | **门面 (Facade) 与单例管理**。<br>1. 维护私有全局变量 `globalLogger`。<br>2. 提供 `Init(opts)` 初始化入口。<br>3. 提供 `L()` 获取底层 `*zap.Logger`。<br>4. 提供 `Info/Error` 等静态代理方法。 | **单例兜底**:在 `init()` 中初始化一个默认的 `Nop` 或 `Console` Logger,防止未初始化调用导致 Panic。<br>**Caller 修正**:<br>1. 底层 `globalLogger` 配置 `AddCallerSkip(0)`。<br>2. 静态代理方法 (`Info`, `Error`) 内部使用 `WithOptions(AddCallerSkip(1))`。<br>3. `L()` 和 `WithContext()` 返回原生 Logger (Skip 0),确保业务层直接调用时行号正确。 |
|
||||
| **`options.go`** | **配置对象 (DTO)**。<br>定义 `Options` 结构体,用于接收 Viper 的配置映射。 | **配置解耦**:只定义 struct,不包含逻辑。支持从 `config.yaml` 的 `log` 节点自动 Unmarshal。 |
|
||||
| **`zap.go`** | **核心构建工厂 (Factory)**。<br>负责组装 Encoder (JSON/Console)、Writer 和 Level。<br>实现 `New(opts)` 函数。 | **环境隔离**:<br>- Dev: ConsoleEncoder + StackTrace (Warn 级)<br>- Prod: JsonEncoder + StackTrace (Panic 级) |
|
||||
| **`writer.go`** | **IO 输出管理**。<br>封装 `lumberjack.Logger`。<br>实现 `zapcore.WriteSyncer` 接口。 | **可靠性**:配置 `Lumberjack` 的 `Compress: true` 和 `MaxSize: 100MB`。实现 Console + File 的 **Tee (双写)** 模式。 |
|
||||
| **`context.go`** | **上下文装饰器与播种器 (Decorator & Seeder)**。<br>1. `WithContext(ctx)`: 提取 TraceID。<br>2. **[New] `StartBackgroundTrace(ctx)`**: 为后台任务生成并注入根 TraceID。 | **零侵入**:仅通过 `zap.With()` 附加字段,返回 **派生 Logger**,不修改全局 Logger,线程安全。 |
|
||||
| **`standard.go`** | **标准化字段与存取器 (Schema & Accessor)**。<br>1. 定义**私有** Context Key 类型,防止碰撞。<br>2. 提供 `WithTraceID(ctx, id)` 和 `GetTraceID(ctx)` 公开方法。<br>3. 定义标准字段构造器 (如 `zap.String("trace_id", …)`)。 | **规范约束**:<br>- 统一使用 snake_case。<br>- 防止拼写错误 (如 `uid` vs `user_id`)。 |
|
||||
|
||||
### B. `internal/middleware` (中间件集成)
|
||||
|
||||
这是日志模块与 HTTP 框架 (Gin) 结合的触点。
|
||||
|
||||
| **文件名** | **职责描述** | **交互逻辑** |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| **`trace.go`** | **链路起点**。<br>链路追踪 (生成 TraceID -> **注入标准 Context** -> 挂载回 Gin Request) | **上下游打通**:保证 TraceID 在微服务或网关间的透传能力。 |
|
||||
| **`access_log.go`** | **流量审计**。<br>1. 记录 `Start Time`。<br>2. 执行 `c.Next()`。<br>3. 计算 `Latency`。<br>4. 打印结构化日志。 | **字段映射**:<br>`path`, `method`, `status`, `client_ip`, `latency`, `user_agent`。**必须使用 `log.WithContext(c)`**。 |
|
||||
| `recovery.go` | 结构化灾难恢复。<br>1. `defer recover()` 捕获 Panic。<br>2. 获取 Stack Trace。<br>3. **调用 `pkg/log` 记录 JSON 格式的 Error 日志** (包含 `stack` 字段)。<br>4. 返回 500 响应。 | **替代 Gin 默认组件**:必须使用 `gin.New()` 启动,手动注册此中间件,杜绝默认的控制台文本打印。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据流转图示 (Data Flow)
|
||||
|
||||
为了确保你理解“上下文”是如何流动的,以下是逻辑路径:
|
||||
|
||||
1. **Request In** -> `middleware/trace.go` -> 生成 `trace_id` -> **Wrap 进 `std.Context`**。 …
|
||||
2. **`pkg/log/context.go`** -> 从 **`std.Context`** 取出 `trace_id` -> …
|
||||
3. **`pkg/log/context.go`** -> 从 `gin.Context` 取出 `trace_id` -> 创建带字段的 `zap.Logger`。
|
||||
4. **`pkg/log/zap.go`** -> 序列化为 JSON `{…"trace_id":"xyz"…}`。
|
||||
5. **`pkg/log/writer.go`** -> 写入 `app.log` 文件 (由 Lumberjack 轮转)。
|
||||
|
||||
## 4. 依赖关系检查 (Dependency Check)
|
||||
|
||||
- `pkg/log` **不依赖** `middleware` (防止循环依赖)。
|
||||
- `middleware` **依赖** `pkg/log` (调用日志打印)。
|
||||
- `pkg/log` **仅依赖** `uber-go/zap`, `natefinch/lumberjack`。**严禁依赖** `gin` 或其他 Web 框架。所有 Context 操作均基于 Go 标准库接口。
|
||||
|
||||
这个结构完全穷尽了我们在前几轮讨论中确定的技术决策。如果确认无误,我们将在下一步生成具体的代码实现。
|
||||
181
Go项目实战/03_基础设施/02_日志/06_日志模块开发规范与质量保证手册.md
Normal file
181
Go项目实战/03_基础设施/02_日志/06_日志模块开发规范与质量保证手册.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 《日志模块开发规范与质量保证手册》
|
||||
- 一、 核心开发规范 (The Golden Rules)
|
||||
date created: 星期三, 十二月 10日 2025, 10:53:19 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:31:04 晚上
|
||||
---
|
||||
|
||||
# 《日志模块开发规范与质量保证手册》
|
||||
|
||||
---
|
||||
|
||||
## 一、 核心开发规范 (The Golden Rules)
|
||||
|
||||
这部分是“软约束”,属于团队共识,通过 Code Review 和 AI 辅助检查来执行。
|
||||
|
||||
### 1. 键名命名公约 (Key Naming Convention)
|
||||
|
||||
日志是给机器(ELK/Loki)读的,键名必须统一,方便建立索引。
|
||||
|
||||
- **规则**: 严禁使用 CamelCase (小驼峰) 或 PascalCase (大驼峰),**必须且只能使用 snake_case (下划线命名)**。
|
||||
- **反例**: `userId`, `IPAddress`, `httpStatus`
|
||||
- **正例**: `user_id`, `client_ip`, `http_status`
|
||||
- **理由**: 多数数据库和搜索引擎(如 Elasticsearch)的分词器对下划线更友好,且 SQL 查询习惯也是下划线。
|
||||
|
||||
### 2. 类型安全铁律 (Type Safety Strictness)
|
||||
|
||||
利用 Zap 的强类型优势,拒绝隐式转换。
|
||||
|
||||
- **规则**: 在业务热点路径(Hot Path)中,**严禁使用 `zap.Any`、`zap.Reflect` 或 `Sugar` 模式**。
|
||||
- **例外**: 仅在应用启动(Init)、Panic 恢复或非高频的配置加载阶段允许使用 `SugaredLogger`。
|
||||
- **理由**: `zap.Any` 会触发反射(Reflection),导致内存逃逸和 GC 压力。这是高性能系统的“隐形杀手”。
|
||||
|
||||
### 3. 上下文优先原则 (Context First)
|
||||
|
||||
日志不是孤岛,必须依附于请求上下文。
|
||||
|
||||
- **规则**: 所有 Controller、Service、Repository 层的方法,如果需要打印日志,**必须**使用 `log.WithContext(ctx).Info(…)` 及其变体。
|
||||
- **禁止**: 严禁在业务流程中直接调用全局的 `log.Info(…)`(除非是系统级事件,如定时任务启动)。
|
||||
- **理由**: 只有通过 `WithContext`,才能将 TraceID 串联起来。
|
||||
|
||||
### 4. 哨兵值与魔法字符串 (Sentinels & Magic Strings)
|
||||
|
||||
- **规则**: 核心日志字段的 Key 必须定义为常量(Constant)。
|
||||
- **实现**: 在 `pkg/log/standard.go` 中定义 `const TraceIDKey = "trace_id"`。
|
||||
- **禁止**: 代码中出现手写的 `zap.String("trace_id", …)`,防止拼写错误(如写成 `traceid`)。
|
||||
|
||||
### 5. 热点路径复用原则 (Hot Path Reuse)
|
||||
|
||||
针对循环(Loop)或复杂长流程函数,严禁重复构建 Context Logger。
|
||||
|
||||
- **规则**: 必须在作用域入口处初始化 Logger 实例,并在该作用域内复用。
|
||||
- **反例 (Bad)**:
|
||||
|
||||
```Go
|
||||
for _, item := range items {
|
||||
// ❌ 每次循环都分配内存
|
||||
log.WithContext(ctx).Info("processing", zap.String("id", item.ID))
|
||||
}
|
||||
```
|
||||
|
||||
- **正例 (Good)**:
|
||||
|
||||
```Go
|
||||
// ✅ 只分配一次,复用 l
|
||||
l := log.WithContext(ctx)
|
||||
for _, item := range items {
|
||||
l.Info("processing", zap.String("id", item.ID))
|
||||
}
|
||||
```
|
||||
|
||||
- **理由**: 减少大量临时的 `zap.Logger` 结构体分配,降低 GC 的 Scavenge 阶段耗时。
|
||||
|
||||
### 6. 后台任务播种原则 (Background Trace Seeding)
|
||||
|
||||
所有非 HTTP 触发的后台任务入口(Goroutine, Cron, MQ Handler),必须是“有状态”的。
|
||||
|
||||
- **规则**: 任务的第一行代码必须调用 `StartBackgroundTrace`。
|
||||
- **反例 (Bad)**:
|
||||
|
||||
```Go
|
||||
func ProcessOrder(msg []byte) {
|
||||
ctx := context.Background()
|
||||
// ❌ 此时 ctx 空空如也,日志将丢失 TraceID
|
||||
log.WithContext(ctx).Info("processing order")
|
||||
}
|
||||
```
|
||||
|
||||
- **正例 (Good)**:
|
||||
|
||||
```Go
|
||||
func ProcessOrder(msg []byte) {
|
||||
// ✅ 自动生成一个新的 TraceID 注入 ctx
|
||||
ctx := log.StartBackgroundTrace(context.Background())
|
||||
log.WithContext(ctx).Info("processing order")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、 Linter 规则配置 (Automated Enforcement)
|
||||
|
||||
这部分是“硬约束”,我们将在 `.golangci.yml` 中配置这些规则,强行阻断不合规代码的提交。
|
||||
|
||||
### 1. 禁用标准库日志 (`depguard`)
|
||||
|
||||
防止开发人员手滑使用了 Go 原生的 `log` 或 `fmt` 打印日志。
|
||||
|
||||
Linter: depguard
|
||||
|
||||
配置策略:
|
||||
|
||||
- **Deny**:
|
||||
- `log`: 标准库日志(无结构化,无法分级)。
|
||||
- `fmt.Print*`: 控制台打印(生产环境绝对禁止)。
|
||||
- `github.com/sirupsen/logrus`: 防止引入其他日志库。
|
||||
|
||||
### 2. 强制错误处理 (`errcheck`)
|
||||
|
||||
Zap 的 `Sync()` 方法可能会返回错误(特别是在 Linux 的 `/dev/stdout` 上),通常需要忽略,但写入文件的错误不能忽略。
|
||||
|
||||
Linter: errcheck / gosec
|
||||
|
||||
配置策略:
|
||||
|
||||
- 对 `logger.Sync()` 的错误处理进行豁免(Exclude),因为在某些 OS 下 stdout sync 必然报错,这是已知 issue。
|
||||
- 但对 `logger.Info` 等方法的 IO 错误,原则上 Zap 内部处理了,不需要业务层捕获。
|
||||
|
||||
### 3. 自定义规则 (`ruleguard` - 高级)
|
||||
|
||||
标准的 Linter 无法检测“键名必须是 snake_case”。如果需要极致的管控,我们可以引入 `ruleguard`。
|
||||
|
||||
AI 辅助检查逻辑:
|
||||
|
||||
由于配置 ruleguard 较复杂,我们约定在 AI 生成代码阶段 执行此逻辑:
|
||||
|
||||
- **Check 1**: 正则匹配所有 `zap.String("([a-z]+[A-Z][a-z]+)", …)` 模式,如果发现驼峰命名,立刻自我修正。
|
||||
- **Check 2**: 扫描代码中是否存在 `fmt.Print`,如有则报错。
|
||||
|
||||
---
|
||||
|
||||
## 三、 安全与脱敏规范 (Security & Masking)
|
||||
|
||||
这是日志系统的“红线”。
|
||||
|
||||
### 1. PII (个人敏感信息) 零容忍
|
||||
|
||||
- **黑名单字段**: `password`, `token`, `access_token`, `refresh_token`, `credit_card`, `id_card`.
|
||||
- **处理方式**:
|
||||
- **方案 A (拦截器)**: 在 `zapcore` 层加 Hook,但这会损耗性能。
|
||||
- **方案 B (显式脱敏)**: 要求 AI 在生成代码时,对于敏感字段,自动包裹脱敏函数。例如 `zap.String("mobile", mask.Mobile(u.Mobile))`。
|
||||
- **决策**: 采用 **方案 B**。依赖编码时的自觉和 AI 的辅助,性能最优。
|
||||
|
||||
### 2. 大字段截断
|
||||
|
||||
- **规则**: 禁止将 Base64 图片数据、巨大的 HTML 内容直接打入日志。
|
||||
- **限制**: 单个 Field 的 Value 长度建议限制在 2KB 以内。
|
||||
|
||||
---
|
||||
|
||||
## 四、 AI 辅助编码的“质量契约” (AI Quality Contract)
|
||||
|
||||
为了确保我(AI)生成的代码符合上述规范,请你(用户)在审查我的代码时,使用以下 **Checklist** 进行验证。这也是我对你的承诺:
|
||||
|
||||
1. **Imports 检查**: 确认没有引入 `log` 或 `fmt`。
|
||||
2. **Context 检查**: 确认 `log.WithContext(ctx)` 是日志调用的唯一起手式。
|
||||
3. **Keys 检查**: 确认所有 JSON Key 都是 `snake_case`。
|
||||
4. **Args 检查**: 确认使用的是 `zap.String/Int` 等强类型构造器,而非 `zap.Any`。
|
||||
5. **Config 检查**: 确认没有硬编码的路径(如 `/var/log`),必须来自 `options.go`。
|
||||
|
||||
---
|
||||
|
||||
## 五、 总结与下一步
|
||||
|
||||
我们确立了:
|
||||
|
||||
1. **命名**: 强制 snake_case。
|
||||
2. **类型**: 拒绝 `zap.Any`,拒绝 `fmt`。
|
||||
3. **上下文**: 强制 `WithContext`。
|
||||
4. **安全**: 显式脱敏。
|
||||
146
Go项目实战/03_基础设施/02_日志/07_日志模块工程化实施标准.md
Normal file
146
Go项目实战/03_基础设施/02_日志/07_日志模块工程化实施标准.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 日志模块工程化实施标准
|
||||
date created: 星期三, 十二月 10日 2025, 10:58:53 晚上
|
||||
date modified: 星期三, 十二月 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`。我们的封装必须暴露这一能力。
|
||||
|
||||
- **场景**: 当日志级别为 `Error` 或 `Fatal` 时,可能需要同步触发飞书/钉钉报警。
|
||||
- **实现标准**: 在 `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.Recovery` 和 `middleware.AccessLog`。
|
||||
3. 确保 Panic 日志中包含 TraceID(从 `c.Request.Context` 中尝试恢复)。
|
||||
|
||||
---
|
||||
|
||||
## 四、 总结与就绪确认
|
||||
|
||||
至此,我们已经完成了日志模块的**全生命周期设计**:
|
||||
|
||||
1. **架构**: 基础设施层,无业务依赖。
|
||||
2. **技术栈**: Zap + Lumberjack + Context Adapter。
|
||||
3. **模式**: 单例兜底 + 依赖注入,强类型约束。
|
||||
4. **规范**: Snake_case 键名,中文友好文档,严禁 Fatal。
|
||||
233
Go项目实战/03_基础设施/02_日志/AI 辅助基础设施构建 SOP (v1.0)-全局日志篇.md
Normal file
233
Go项目实战/03_基础设施/02_日志/AI 辅助基础设施构建 SOP (v1.0)-全局日志篇.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🏗️ AI 辅助基础设施构建 SOP (v1.0) - [全局日志篇]
|
||||
date created: 星期三, 十二月 10日 2025, 11:50:40 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:52:08 晚上
|
||||
---
|
||||
|
||||
# 🏗️ AI 辅助基础设施构建 SOP (v1.0) - [全局日志篇]
|
||||
|
||||
**核心理念:**
|
||||
|
||||
1. **Configuration Driven (配置驱动):** 先定义配置结构与 Viper 映射,再实现逻辑。
|
||||
2. **Zero Allocation Constraint (零分配约束):** 在 Prompt 层面封杀 `zap.Any`,强制使用强类型字段。
|
||||
3. **Layered Delivery (分层交付):** 先交付 `pkg/log` (Level 0),再交付 `middleware` (Level 1)。
|
||||
|
||||
---
|
||||
|
||||
## 📋 准备工作:上下文注入
|
||||
|
||||
在使用以下 Prompt 前,请确保 AI 已理解《全局日志模块详细设计说明书》的全部内容。
|
||||
|
||||
- `{语言/框架}`: Go 1.24+ / Uber Zap / Lumberjack v2
|
||||
- `{模块路径}`: `internal/pkg/log` (核心) & `internal/middleware` (集成)
|
||||
- `{关键约束}`: `pkg/log` **严禁依赖** `gin` 或 `viper` (仅接收 Config struct)。
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: 依赖隔离与任务拆解 (The Dependency-Aware MECE)
|
||||
|
||||
**目的:** 防止 AI 在编写日志核心时引入业务层代码(如 Gin),导致循环依赖。
|
||||
|
||||
### 🤖 拆解者 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
你现在是我的 **System Architect (系统架构师)**。
|
||||
我们要实现 `Global Logging Infrastructure`。基于《详细设计说明书》,请执行 **“依赖隔离任务拆解”**。
|
||||
|
||||
**1. 架构红线 (Architecture Rules):**
|
||||
- **Level 0 (Core):** `internal/pkg/log`。只依赖 `zap`, `lumberjack`, standard `context`。**严禁依赖 `gin`**。
|
||||
- **Level 1 (Integration):** `internal/middleware`。依赖 `internal/pkg/log` 和 `gin`。
|
||||
|
||||
**2. 原子化切分:**
|
||||
请将工作拆解为两个独立的 Batch,每个 Batch 包含若干 Step。
|
||||
- **Batch A (Core)**: 必须按 `options.go` (配置) -> `zap.go` (构造) -> `context.go` (桥接) -> `log.go` (门面) 的顺序。
|
||||
- **Batch B (Middleware)**: 包含 `trace.go`, `access_log.go`, `recovery.go`。
|
||||
|
||||
**3. 输出格式:**
|
||||
请输出一个 **Markdown Checklist**。
|
||||
格式示例:
|
||||
- [ ] **Batch A - Step 1: {文件名}** - {核心职责} (关键设计点: …)
|
||||
…
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 0.5: API 签名锁定 (API Surface Lock)
|
||||
|
||||
**目的:** 在实现 `zap` 复杂构建逻辑前,先锁死对外暴露的“门面”方法,确保调用体验。
|
||||
|
||||
### 🤖 Prompt 0.5: 定义门面接口
|
||||
|
||||
```Markdown
|
||||
在实现具体逻辑前,让我们先锁定 `internal/pkg/log` 的 **Public API**。
|
||||
请只输出 `log.go` 和 `context.go` 中 **Exported Functions** 的签名(无需函数体)。
|
||||
|
||||
**关键要求:**
|
||||
1. **初始化:** `Init(opts …Option)` 设计为 Functional Options 模式还是直接传 Struct?(依据设计文档应为 Struct 传入,但保留 Option 扩展性)。
|
||||
2. **上下文注入:** `WithContext(ctx context.Context) *zap.Logger` 的签名确认。
|
||||
3. **静态代理:** `Info`, `Error` 等静态方法如何处理 `CallerSkip`?请在注释中说明。
|
||||
4. **后台任务:** 必须包含 `StartBackgroundTrace(ctx)` 的定义。
|
||||
|
||||
请输出带有完整 Go Doc 的接口定义代码块。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 配置契约定义 (Configuration Contract)
|
||||
|
||||
**目的:** 只有确定了“怎么配”,才能决定“怎么写”。
|
||||
|
||||
### 🤖 Prompt 1: 定义配置结构与 Schema
|
||||
|
||||
```Markdown
|
||||
你现在是 **DevOps 专家**。
|
||||
请定义日志模块的配置结构 (`options.go`) 以及对应的 YAML 写法。
|
||||
|
||||
**任务:**
|
||||
1. **Go Struct:** 定义 `Options` 结构体。
|
||||
- 包含 `Level`, `Format` (json/console), `Filename`, `MaxSize`, `MaxBackups`, `MaxAge`, `Compress`。
|
||||
- Tag 必须适配 `mapstructure` (Viper 使用)。
|
||||
2. **Default Value:** 提供一个 `NewOptions()` 函数返回生产环境推荐的默认值 (100MB, 30个文件, JSON 格式)。
|
||||
3. **YAML Example:** 给出一个 `config.yaml` 的片段示例。
|
||||
|
||||
**约束:**
|
||||
- 字段类型必须明确(如 `MaxSize` 是 int 还是 string? 建议 int 单位 MB)。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 体验验证 (DX Verification)
|
||||
|
||||
**目的:** 验证开发者在业务代码中打印日志是否顺手,防止过度封装导致 API 臃肿。
|
||||
|
||||
### 🤖 Prompt 2: 伪代码验证 (复制使用)
|
||||
|
||||
```Markdown
|
||||
配置和接口已锁定。请写一段 **Service 层** 的伪代码,展示如何使用该日志库。
|
||||
|
||||
**场景验证:**
|
||||
1. **标准调用:** 在 `UserRegister` 方法中,如何打日志并自动带上 TraceID?
|
||||
2. **强类型约束:** 展示使用 `zap.String`, `zap.Int` 的写法。**严禁出现 `zap.Any`**。
|
||||
3. **子 Context:** 在 `go func()` 中如何使用 `StartBackgroundTrace` 保证链路不断?
|
||||
4. **Error 处理:** 遇到 DB 错误时,如何记录 log 并返回 error?
|
||||
|
||||
请展示代码,并自我评价是否符合“低心智负担”原则。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 核心防御式实现 (Core Defensive Implementation)
|
||||
|
||||
**核心机制:** 这是一个**循环步骤**。针对 `internal/pkg/log` 的每个文件执行。
|
||||
|
||||
### 🔄 循环动作 A: 生成代码
|
||||
|
||||
**[发送 Prompt]:**
|
||||
|
||||
```Markdown
|
||||
我们现在执行 **Batch A - Step {N}**。
|
||||
|
||||
**任务目标:**
|
||||
生成 `{文件名}` (例如 `zap.go`)。
|
||||
|
||||
**设计文档引用:**
|
||||
- 引用《设计说明书》中关于 `{章节名}` 的要求。
|
||||
|
||||
**代码质量硬性约束 (Hard Constraints):**
|
||||
1. **Snake Case:** 所有的 JSON Key (包括 TraceID) 必须手动指定为 snake_case (如 `zap.String("trace_id", v)`)。
|
||||
2. **No Zap Any:** 严禁在核心逻辑中使用 `zap.Any`。如果是 map/struct,必须手动拆解或实现 `zapcore.ObjectMarshaler`。
|
||||
3. **Safety:**
|
||||
- `writer.go`: Lumberjack 的 `Compress` 必须默认为 true。
|
||||
- `log.go`: `globalLogger` 必须有 `sync.Once` 保护,且默认初始化为 Console (避免 nil pointer)。
|
||||
4. **Caller Skip:** 确保静态方法 (log.Info) 和实例方法 (logger.Info) 的 Caller 层级正确,都能定位到业务代码行号。
|
||||
|
||||
请生成完整代码。
|
||||
```
|
||||
|
||||
### 🔄 循环动作 B: 质量检查锚点
|
||||
|
||||
**[发送 Prompt]:**
|
||||
|
||||
```Markdown
|
||||
代码已生成。请进行 **Self-Correction (自我修正)**:
|
||||
1. 检查是否有 `fmt.Print` 残留?
|
||||
2. 检查 `log.go` 中的静态方法是否使用了 `WithOptions(zap.AddCallerSkip(1))`?如果没用,业务层行号会报错。
|
||||
3. 检查是否引入了 `gin` 或其他业务包?(Level 0 严禁依赖)。
|
||||
|
||||
确认无误后,存入记忆,继续下一步。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 中间件集成 (Middleware Integration)
|
||||
|
||||
**目的:** 只有当核心 Log 库稳定后,才实现 Gin 中间件。
|
||||
|
||||
### 🤖 Prompt 4: 实现链路追踪与访问日志
|
||||
|
||||
```Markdown
|
||||
现在进入 **Batch B**。我们需要实现 `internal/middleware/trace.go` 和 `access_log.go`。
|
||||
|
||||
**任务要求:**
|
||||
1. **Trace Middleware:**
|
||||
- 从 Request Header (`X-Trace-ID`) 读取,若无则生成 UUID。
|
||||
- **关键点:** 必须调用 `log.WithTraceID(ctx, id)` 将 ID 注入 Standard Context,再回写到 `c.Request`。
|
||||
2. **Access Log Middleware:**
|
||||
- 记录 Start Time, End Time, Latency。
|
||||
- 使用 `log.WithContext(c.Request.Context()).Info(…)` 打印。
|
||||
- **字段映射:** `method`, `path`, `ip`, `status`, `latency` (ms)。
|
||||
3. **Recovery Middleware:**
|
||||
- 捕获 Panic。
|
||||
- 打印包含 Stack Trace 的 JSON Error 日志 (非 Console 文本)。
|
||||
- 返回 500 响应。
|
||||
|
||||
请一次性生成这三个文件的核心逻辑。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 极限防御测试 (Extreme Defensive Testing)
|
||||
|
||||
**目的:** 验证并发安全、文件轮转和敏感数据脱敏。
|
||||
|
||||
### 🤖 Prompt 5: 生成红队测试用例
|
||||
|
||||
```Markdown
|
||||
核心代码已就绪。请为 `pkg/log` 编写单元测试 `log_test.go`。
|
||||
|
||||
**请覆盖以下 3 个高危场景 (Test Cases):**
|
||||
|
||||
1. **并发竞争 (Race Detection):**
|
||||
- 启动 100 个 Goroutine,同时调用 `log.WithContext(ctx).Info(…)`。
|
||||
- 断言:`go test -race` 不报错,且 TraceID 不串号。
|
||||
|
||||
2. **Caller 准确性验证:**
|
||||
- 编写一个测试辅助函数,解析输出的 JSON,断言 `caller` 字段指向的是测试代码行号,而不是 `log.go` 内部。
|
||||
|
||||
3. **敏感数据脱敏 (Mock):**
|
||||
- 模拟打印 `zap.String("password", "123456")`。
|
||||
- (注意:如果我们在 Zap Core 层没做拦截,这里需要验证是否通过 AI 辅助代码生成时的规范来避免。此处请测试:如果定义了 Masking Hook,密码是否被替换为 `******`)。
|
||||
- *注:根据设计文档,我们采用“编码规范+AI辅助”策略,故此处测试应侧重于“TraceID 是否在 Log 中正确出现”。*
|
||||
|
||||
请输出 Test 代码。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: SRE 验收 (SRE Review)
|
||||
|
||||
### 🤖 Prompt 6: 运维视角审查
|
||||
|
||||
```Markdown
|
||||
切换角色为 **SRE**。请审查生成的日志模块。
|
||||
|
||||
**检查清单:**
|
||||
1. **磁盘爆炸风险:** `Lumberjack` 配置是否真的生效了?(检查 WriteSyncer 的封装)。
|
||||
2. **性能损耗:** `WithContext` 是否每次都创建了过多的 Zap 对象?(确认是否只是 Shallow Copy)。
|
||||
3. **索引友好度:** 时间戳是否为 ISO8601 (`2025-12-10T…`)?如果是 Epoch float,ELK 处理会麻烦。
|
||||
4. **容灾:** 如果 `logs/` 目录不可写(权限问题),程序会 Panic 还是降级输出到 Stdout?
|
||||
|
||||
请给出 1-2 个具体的 `TODO` 优化项。
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 📋 Global Logging Infrastructure - Task Checklist
|
||||
date created: 星期三, 十二月 10日 2025, 11:55:42 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:56:24 晚上
|
||||
---
|
||||
收到。作为系统架构师,我已根据《全局日志模块详细设计说明书》及 SOP 要求,完成了**依赖隔离任务拆解 (Dependency-Aware MECE Breakdown)**。
|
||||
|
||||
此拆解方案严格遵循 **Level 0 (Core)** 与 **Level 1 (Integration)** 的物理隔离,确保核心日志包零依赖于业务框架。
|
||||
|
||||
---
|
||||
|
||||
# 📋 Global Logging Infrastructure - Task Checklist
|
||||
|
||||
## Phase 1: Batch A - Core Implementation (Level 0)
|
||||
|
||||
> 目录: internal/pkg/log
|
||||
>
|
||||
> 约束: 仅依赖 zap, lumberjack, context。严禁 import gin, viper。
|
||||
|
||||
- [ ] **Batch A - Step 1: 配置与标准定义 (`options.go`, `standard.go`)**
|
||||
|
||||
- **核心职责:** 定义日志配置结构体 (Config Struct) 及全局统一的键名常量 (Standard Keys)。
|
||||
- **关键设计:**
|
||||
- `Options` 结构体需包含 `mapstructure` tag 以适配外部 Viper 解析。
|
||||
- 预定义 `trace_id`, `user_id` 等常量为 `snake_case`,杜绝魔法字符串。
|
||||
- 包含 `DefaultOptions()` 返回生产环境推荐配置 (100MB, Compress=true)。
|
||||
|
||||
- [ ] **Batch A - Step 2: 核心构建与 IO (`writer.go`, `zap.go`)**
|
||||
|
||||
- **核心职责:** 封装 Lumberjack 文件轮转逻辑,构建 `zap.Core` 与 `zap.Logger` 实例。
|
||||
- **关键设计:**
|
||||
- **IO 分离:** `writer.go` 实现 `zapcore.WriteSyncer`,强制开启 `Compress: true`。
|
||||
- **环境隔离:** `zap.go` 根据配置决定使用 `JSON Encoder` (Prod) 或 `Console Encoder` (Dev)。
|
||||
- **双写机制:** 实现 Tee 模式,同时输出到文件和控制台 (Stdout)。
|
||||
|
||||
- [ ] **Batch A - Step 3: 上下文桥接 (`context.go`)**
|
||||
|
||||
- **核心职责:** 实现标准 `context.Context` 到 `zap.Field` 的转换逻辑。
|
||||
- **关键设计:**
|
||||
- **TraceID 注入:** 实现 `WithContext(ctx)`,从 Context 提取 TraceID 并返回带有 `trace_id` 字段的 `*zap.Logger`。
|
||||
- **后台播种:** 实现 `StartBackgroundTrace(ctx)`,为 Cron/Goroutine 任务生成根 TraceID。
|
||||
- **零侵入:** 仅依赖标准库 Context,不依赖 Gin Context。
|
||||
|
||||
- [ ] **Batch A - Step 4: 全局门面 (`log.go`)**
|
||||
|
||||
- **核心职责:** 管理全局单例 (Singleton),提供静态代理方法 (Static Proxy)。
|
||||
- **关键设计:**
|
||||
- **懒汉兜底:** `globalLogger` 默认初始化为 Console Logger (Nop),防止未调用 `Init` 时 Panic。
|
||||
- **Caller 修正:** 静态方法 (`log.Info`) 必须使用 `AddCallerSkip(1)`,确保日志行号指向业务代码而非 `log.go`。
|
||||
- **并发安全:** `Init()` 必须使用 `sync.Once` 或互斥锁保护。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Batch B - Middleware Integration (Level 1)
|
||||
|
||||
> 目录: internal/middleware
|
||||
>
|
||||
> 约束: 依赖 internal/pkg/log, gin, pkg/app。
|
||||
|
||||
- [ ] **Batch B - Step 1: 链路追踪中间件 (`trace.go`)**
|
||||
|
||||
- **核心职责:** 请求入口处的 TraceID 生成与注入。
|
||||
- **关键设计:**
|
||||
- 优先读取 Header `X-Trace-ID`,无则生成 UUID。
|
||||
- **关键动作:** 调用 `log.WithTraceID(ctx, id)` 将 ID 注入 **Standard Context**,再回写到 `c.Request`,打通后续所有层的日志链路。
|
||||
|
||||
- [ ] **Batch B - Step 2: 访问日志中间件 (`access_log.go`)**
|
||||
|
||||
- **核心职责:** 记录 HTTP 请求的黄金指标 (Golden Signals)。
|
||||
- **关键设计:**
|
||||
- 必须使用 `log.WithContext(c.Request.Context())` 打印,确保包含 TraceID。
|
||||
- 记录字段:`method`, `path`, `status`, `latency` (ms), `client_ip`。
|
||||
|
||||
- [ ] **Batch B - Step 3: 灾难恢复中间件 (`recovery.go`)**
|
||||
|
||||
- **核心职责:** 替换 Gin 默认 Recovery,提供结构化 Panic 日志。
|
||||
- **关键设计:**
|
||||
- 捕获 `panic` -> 获取 Stack Trace -> 构造 JSON Error 日志 (包含 `stack` 字段)。
|
||||
- 联动 `pkg/app` 返回标准 JSON 500 响应,通过 `pkg/log` 记录系统级错误。
|
||||
|
||||
---
|
||||
|
||||
# 🏁 Next Action
|
||||
|
||||
建议按照 Checklist 顺序,从 **Batch A - Step 1** 开始执行。是否现在开始 Phase 0.5 (API 签名锁定) 或直接生成 Step 1 代码?
|
||||
Reference in New Issue
Block a user