6.7 KiB
实现方案:纯 Context 驱动的调用链追踪
本方案旨在提供一个绝对安全、符合 Go 语言习惯且对业务代码侵入性最小的调用链追踪方案。其核心思想是:调用链信息完全由标准的 context.Context 承载,并通过 logs 包提供的一系列无状态包级函数进行原子化、安全的操作。
1. 核心设计原则
-
Context是唯一载体: 所有的调用链信息,包括组件名(对象名)和函数名(方法名),都只存储在标准的context.Context中。我们不再定义任何自定义的Context接口,以保证最大的兼容性。 -
纯粹的包级函数:
logs包将提供一系列纯粹的、无状态的包级函数,作为与Context交互的唯一 API。这些函数负责向Context中添加信息,或从Context中生成Logger。 -
无状态的
Logger:Logger对象本身不再携带任何调用链信息。它在被logs.GetLogger(ctx)生成时,才被一次性地赋予包含完整调用链的配置。
2. 实现细节
a. Context 中存储的数据
context.Context 将通过 context.WithValue 在幕后存储两种核心信息,这两种信息都使用 logs 包内部的私有 key 类型,以避免与其他包的键冲突。
- 组件名 (
compNameKey): 用于存储一个字符串,表示当前上下文环境属于哪个组件(例如\"组件1\")。 - 调用链 (
chainKey): 用于存储一个字符串切片 ([]string),记录了从请求开始到当前位置的完整调用路径(例如[\"组件2.Create\", \"组件1.Create\"])。
b. logs 包提供的核心 API
logs 包需要对外提供以下四个核心的包级函数,以提供不同粒度的灵活性:
-
logs.AddCompName(ctx, compName) context.Context- 职责: 将一个组件名(对象名)存入
Context,并返回一个包含该信息的新Context。这通常在依赖注入时完成,用于创建组件的“身份名牌”selfCtx。 - 实现细节: 该函数接收一个
context.Context和一个compName字符串。它内部使用context.WithValue,以私有的compNameKey为键,将compName字符串存入Context,然后返回这个全新的Context。
- 职责: 将一个组件名(对象名)存入
-
logs.AddFuncName(upstreamCtx, selfCtx, funcName) context.Context- 职责: 这是构建调用链的核心原子操作。它智能地合并上游的调用链和当前组件的信息,生成并返回一个包含更新后调用链和当前组件名的 新
Context。此函数用于只需要传递调用链而不需要立即打印日志的场景。 - 实现细节:
- 函数接收
upstreamCtx,selfCtx,funcName。 - 获取上游调用链: 从
upstreamCtx中,通过chainKey读取出已经存在的调用链(oldChain []string)。 - 获取当前组件名: 从
selfCtx中,通过compNameKey读取出当前组件的名称(compName string)。 - 构建新节点: 将
compName和funcName拼接成一个新节点(例如\"组件2.Create\")。 - 生成新调用链: 将这个新节点追加到
oldChain的末尾,形成newChain []string。 - 创建新的 Context:
- 使用
context.WithValue,以chainKey为键,将newChain存入一个新的Context,我们称之为tmpCtx。 - 接着,(关键修正) 基于
tmpCtx,再次调用context.WithValue,以compNameKey为键,将从selfCtx中获取的compName存入,得到最终的newCtx。这确保了传向下游的Context正确地标识了当前组件。
- 使用
- 返回: 返回
newCtx。
- 函数接收
- 职责: 这是构建调用链的核心原子操作。它智能地合并上游的调用链和当前组件的信息,生成并返回一个包含更新后调用链和当前组件名的 新
-
logs.GetLogger(ctx) *Logger- 职责: 从
Context中生成最终的、可用于打印的Logger实例。 - 实现细节:
- 函数接收一个
context.Context。 - 它从
ctx中,通过chainKey读取出完整的调用链[]string。 - 如果调用链不存在或为空,它就生成一个不带
trace字段的普通Logger实例并返回。 - 如果调用链存在,它就用
->符号将切片中的所有节点拼接成一个完整的trace字符串。 - 最后,它创建一个一次性的
Logger实例,将这个trace字符串和底层的zap配置传给它,然后返回这个准备就绪的Logger。
- 函数接收一个
- 职责: 从
-
logs.Trace(upstreamCtx, selfCtx, funcName) (context.Context, *Logger)- 职责: 作为
AddFuncName和GetLogger的便捷封装,一步到位地完成调用链构建和Logger生成。用于需要立即打印日志的场景。 - 实现细节:
- 内部调用
newCtx := logs.AddFuncName(upstreamCtx, selfCtx, funcName)。 - 内部调用
logger := logs.GetLogger(newCtx)。 - 返回
newCtx和logger。
- 内部调用
- 职责: 作为
3. 最终使用模式
a. 依赖注入阶段
(保持不变)在应用启动和组装依赖时,我们不再注入 Logger 对象,而是为每个组件创建一个包含其自身名称的专属 Context,并将这个 Context 注入到组件实例中。
- 流程:
- 在依赖注入的根源处,创建一个全局的、初始的
context.Background()。 - 对于需要被追踪的组件(例如
组件1),调用logs.AddCompName(ctx, \"组件1\")来创建一个ctxForC1。 - 在创建
组件1的实例时,将这个ctxForC1作为其成员变量(例如selfCtx)保存起来。这个selfCtx就成了组件1的“身份名牌”。
- 在依赖注入的根源处,创建一个全局的、初始的
b. 请求处理阶段
开发者可以根据需求灵活选择 API。
-
场景一:需要立即打印日志 (推荐):
- 在方法入口处,立即调用
ctx, logger := logs.Trace(upstreamCtx, z.selfCtx, \"Create\")。 - 使用这个
logger打印日志:logger.Info(\"创建组件2\")。 - 当需要调用下游方法时,将返回的 新
Context(ctx) 传递下去。
- 在方法入口处,立即调用
-
场景二:只需要传递调用链:
- 在方法入口处,调用
ctx := logs.AddFuncName(upstreamCtx, z.selfCtx, \"Create\")。 - 这个方法本身不打印日志,但在调用下游方法时,将这个包含了更新后调用链的 新
Context(ctx) 传递下去。
- 在方法入口处,调用
-
场景三:在方法中间打印日志:
- 一个方法可能在执行了一部分逻辑后才需要打印日志。
ctx := logs.AddFuncName(upstreamCtx, z.selfCtx, \"Create\")// 先在入口更新调用链- // ... 执行一些业务逻辑 ...
logger := logs.GetLogger(ctx)// 在需要时,基于更新后的 ctx 获取 loggerlogger.Info(\"业务逻辑执行到一半\")- // ... 继续执行并传递 ctx ...