# 实现方案：纯 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` 包需要对外提供以下四个核心的包级函数，以提供不同粒度的灵活性：

1.  **`logs.AddCompName(ctx, compName) context.Context`**
    -   **职责**: 将一个组件名（对象名）存入 `Context`，并返回一个包含该信息的新 `Context`。这通常在依赖注入时完成，用于创建组件的“身份名牌” `selfCtx`。
    -   **实现细节**: 该函数接收一个 `context.Context` 和一个 `compName` 字符串。它内部使用 `context.WithValue`，以私有的 `compNameKey` 为键，将 `compName` 字符串存入 `Context`，然后返回这个全新的 `Context`。

2.  **`logs.AddFuncName(upstreamCtx, selfCtx, funcName) context.Context`**
    -   **职责**: 这是构建调用链的核心原子操作。它智能地合并上游的调用链和当前组件的信息，生成并返回一个包含更新后调用链和当前组件名的 **新 `Context`**。此函数用于只需要传递调用链而不需要立即打印日志的场景。
    -   **实现细节**:
        1.  函数接收 `upstreamCtx`, `selfCtx`, `funcName`。
        2.  **获取上游调用链**: 从 `upstreamCtx` 中，通过 `chainKey` 读取出已经存在的调用链（`oldChain []string`）。
        3.  **获取当前组件名**: 从 `selfCtx` 中，通过 `compNameKey` 读取出当前组件的名称（`compName string`）。
        4.  **构建新节点**: 将 `compName` 和 `funcName` 拼接成一个新节点（例如 `\"组件2.Create\"`）。
        5.  **生成新调用链**: 将这个新节点追加到 `oldChain` 的末尾，形成 `newChain []string`。
        6.  **创建新的 Context**:
            -   使用 `context.WithValue`，以 `chainKey` 为键，将 `newChain` 存入一个新的 `Context`，我们称之为 `tmpCtx`。
            -   接着，**（关键修正）** 基于 `tmpCtx`，再次调用 `context.WithValue`，以 `compNameKey` 为键，将从 `selfCtx` 中获取的 `compName` 存入，得到最终的 `newCtx`。这确保了传向下游的 `Context` 正确地标识了当前组件。
        7.  **返回**: 返回 `newCtx`。

3.  **`logs.GetLogger(ctx) *Logger`**
    -   **职责**: 从 `Context` 中生成最终的、可用于打印的 `Logger` 实例。
    -   **实现细节**:
        1.  函数接收一个 `context.Context`。
        2.  它从 `ctx` 中，通过 `chainKey` 读取出完整的调用链 `[]string`。
        3.  如果调用链不存在或为空，它就生成一个不带 `trace` 字段的普通 `Logger` 实例并返回。
        4.  如果调用链存在，它就用 `->` 符号将切片中的所有节点拼接成一个完整的 `trace` 字符串。
        5.  最后，它创建一个**一次性**的 `Logger` 实例，将这个 `trace` 字符串和底层的 `zap` 配置传给它，然后返回这个准备就绪的 `Logger`。

4.  **`logs.Trace(upstreamCtx, selfCtx, funcName) (context.Context, *Logger)`**
    -   **职责**: 作为 `AddFuncName` 和 `GetLogger` 的便捷封装，一步到位地完成调用链构建和 `Logger` 生成。用于需要立即打印日志的场景。
    -   **实现细节**:
        1.  内部调用 `newCtx := logs.AddFuncName(upstreamCtx, selfCtx, funcName)`。
        2.  内部调用 `logger := logs.GetLogger(newCtx)`。
        3.  返回 `newCtx` 和 `logger`。

### 3. 最终使用模式

#### a. 依赖注入阶段

（保持不变）在应用启动和组装依赖时，我们不再注入 `Logger` 对象，而是为每个组件创建一个包含其自身名称的专属 `Context`，并将这个 `Context` 注入到组件实例中。

- **流程**:
    1.  在依赖注入的根源处，创建一个全局的、初始的 `context.Background()`。
    2.  对于需要被追踪的组件（例如 `组件1`），调用 `logs.AddCompName(ctx, \"组件1\")` 来创建一个 `ctxForC1`。
    3.  在创建 `组件1` 的实例时，将这个 `ctxForC1` 作为其成员变量（例如 `selfCtx`）保存起来。这个 `selfCtx` 就成了 `组件1` 的“身份名牌”。

#### b. 请求处理阶段

开发者可以根据需求灵活选择 API。

- **场景一：需要立即打印日志 (推荐)**:
    1.  在方法入口处，立即调用 `ctx, logger := logs.Trace(upstreamCtx, z.selfCtx, \"Create\")`。
    2.  使用这个 `logger` 打印日志：`logger.Info(\"创建组件2\")`。
    3.  当需要调用下游方法时，将返回的 **新 `Context` (`ctx`)** 传递下去。

- **场景二：只需要传递调用链**:
    1.  在方法入口处，调用 `ctx := logs.AddFuncName(upstreamCtx, z.selfCtx, \"Create\")`。
    2.  这个方法本身不打印日志，但在调用下游方法时，将这个包含了更新后调用链的 **新 `Context` (`ctx`)** 传递下去。

- **场景三：在方法中间打印日志**:
    1.  一个方法可能在执行了一部分逻辑后才需要打印日志。
    2.  `ctx := logs.AddFuncName(upstreamCtx, z.selfCtx, \"Create\")` // 先在入口更新调用链
    3.  // ... 执行一些业务逻辑 ...
    4.  `logger := logs.GetLogger(ctx)` // 在需要时，基于更新后的 ctx 获取 logger
    5.  `logger.Info(\"业务逻辑执行到一半\")`
    6.  // ... 继续执行并传递 ctx ...
