skillhub/docs/2026-07-31-observability-construction-plan.md
XiaoSeS 55e5c1e10e
feat(observability): add generic request correlation and tracing foundation (#664)
* feat(observability): establish request correlation boundary

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* feat(observability): add selectable tracing modes

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* feat(observability): propagate async trace context

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* docs(observability): document tracing deployment modes

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(observability): tighten tracing integration boundaries

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(observability): harden operational log privacy

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* feat(observability): propagate message trace context

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(observability): document message propagation semantics

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* test(auth): isolate security context between tests

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(observability): skip otlp exporter without endpoint

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

---------

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
2026-08-04 10:26:42 +08:00

21 KiB
Raw Blame History

SkillHub 日志关联与链路追踪建设方案

日期2026-07-31

状态Accepted2026-07-31按本文分阶段实施和验证

关联GitHub Issue #597 适用基线Spring Boot 3.2.3、Java 21、Logback、Micrometer Actuator

1. 背景

SkillHub 已经使用 X-Request-Id 关联 API 响应、业务日志和审计记录,但目前仍存在以下问题:

  • 部分应用服务和 DTO 直接读取 SLF4J MDC可观测性实现泄漏到了业务代码。
  • X-Request-Id 接受任意客户端输入,没有统一的长度和字符约束。
  • @Async 线程池没有显式传播请求和 Trace 上下文,异步日志可能丢失关联信息。
  • 当前没有标准分布式 Trace无法通过一个 ID 串联 SkillHub、Scanner 等服务调用。
  • 日志字段尚未形成适合 Elasticsearch/Kibana 查询的稳定结构。

本方案用最小建设成本建立通用日志关联与链路追踪基础设施。它不负责建设完整的企业 可观测性平台也不把日志、Trace 或 Metrics 逻辑写入业务处理器。

Issue #597 中“搜索索引可靠异步交付”应作为独立问题处理,不属于本文范围。

2. 建设目标

一期需要实现:

  1. 每个 HTTP 请求都有合法的 request.id
  2. 启用 Tracing 时,日志包含标准 trace.idspan.id
  3. otel-sdk 模式使用 W3C traceparent / tracestate 传播 Trace Context。
  4. 业务代码不直接读写 MDC也不直接依赖 OpenTelemetry 或 SkyWalking API。
  5. 现有 Spring @Async 执行器能够正确传播并清理上下文。
  6. 日志以结构化 JSON 输出到 stdout可由 Filebeat/Fluent Bit 采集到 Elasticsearch/Kibana。
  7. Trace 可以选择通过 OTLP Collector 接入 SkyWalking。
  8. Collector、SkyWalking、Elasticsearch 或日志采集器不可用时SkillHub 业务继续运行。
  9. SkillHub 应用配置只能启用一个应用内 Tracerexternal-agent 模式下唯一外部 Agent 由部署参数和发布检查保证。

本方案按多个小阶段、小提交实施和验证,全部通过后再统一创建一个替代 PR。

3. 非目标

一期不建设:

  • 搜索索引可靠队列、重试、死信和重放。
  • 多租户差异化采样和运行时动态采样。
  • Spring Cloud Config、Nacos 或可写 Actuator 配置端点。
  • 应用内 OTLP 熔断器或自定义重试框架。
  • 审计日志归档、物理隔离和 WORM 存储。
  • 通用 PII/DLP 检测平台。
  • Prometheus/Grafana/Kibana 告警模板和容量规划平台。
  • Spring Boot 2.x 或 Java 17 兼容。
  • 在业务类上增加 Trace 注解或要求业务开发者操作 Span。

4. 总体架构

HTTP request
    │
    ├─ RequestIdFilter
    │     └─ request.id
    │
    └─ Micrometer Observation / Tracing
          ├─ MDC correlation
          │     └─ JSON stdout
          │           └─ Filebeat / Fluent Bit
          │                 └─ Elasticsearch / Kibana
          │
          └─ OpenTelemetry Bridge
                └─ OTLP
                      └─ OpenTelemetry Collector
                            └─ SkyWalking OAP

稳定边界是:

  • 应用内使用 Micrometer Observation/Tracing。
  • otel-sdk 模式跨进程使用 W3C Trace Context。
  • Trace 导出使用 OTLP。
  • 日志使用 ECS 风格字段。
  • SkyWalking、Elasticsearch 和 Kibana 都是部署适配器,不进入业务模型。

5. 运行模式

通过一个启动期配置选择运行模式:

skillhub:
  observability:
    tracing-mode: ${SKILLHUB_TRACING_MODE:none}

允许值和确定行为:

模式 Micrometer Tracer OTLP Exporter 外部 Agent 无 Agent/endpoint 时
none NOOP 不支持 只有 request.id
otel-sdk OTel Bridge 配置 endpoint 时创建 不支持 仍建立进程内 Trace但不导出
external-agent NOOP 可选 记录警告并退化为只有 request.id

运行模式是启动期不变量,不支持热切换。

必须保证:

  • noneexternal-agent 不创建应用内 OTel Span。
  • otel-sdk 不支持同时启用 SkyWalking、OTel 或其他外部 Tracing Agent应用只能校验 自身 endpoint/mode 冲突,不能可靠识别任意 JVM Agent。
  • external-agent 不创建 OTLP Exporter。
  • SkillHub 配置能够识别的冲突应在启动时失败;任意 Java Agent 无法被应用可靠识别,因此 部署检查和原型测试还必须验证实际 JVM 参数中只有一个 Tracer。

一期实现并验证三种模式的应用上下文互斥边界和日志关联。external-agent 只验证 SkyWalking Agent 接管 Trace 时应用内 OTel Tracer/Exporter 不工作;“只挂载一个外部 Agent”属于部署验收项。SkyWalking 特有高级能力不进入 SkillHub 核心代码。

6. 关联字段契约

6.1 对外日志字段

日志输出统一使用:

字段 必需性 含义
request.id HTTP 请求或显式任务上下文中存在 SkillHub API、响应和审计关联 ID
trace.id 当前存在有效 Trace 时 分布式 Trace ID
span.id 当前 Tracer 能提供时 当前调用节点 ID
service.name 始终存在 固定为 skillhub
service.version 部署时提供 发布版本或镜像对应 Commit
service.environment 部署时提供 当前部署环境

request.idtrace.id 不能合并:

  • request.id 属于 SkillHub API 契约,可出现在响应和审计记录中。
  • trace.id 属于可选的分布式追踪上下文,可能被采样或关闭。

启动日志以及没有显式任务上下文的后台维护日志允许不包含 request.id

6.2 内部字段映射

日志基础设施负责字段映射,业务代码不感知具体 MDC 键:

来源 内部字段 输出字段
SkillHub Request Context requestId request.id
Micrometer OTel Bridge traceId trace.id
Micrometer OTel Bridge spanId span.id
SkyWalking Logback Toolkit 事件转换器 tid trace.id

SkyWalking Agent 是否能稳定提供独立 span.id 以实际原型结果为准。无法稳定提供时允许只 输出 trace.id,不得解析不稳定的内部字符串格式。

External Agent 模式通过 SkyWalking 官方 Logback Toolkit 从当前日志事件读取 tid 这不是业务代码读取 MDC也不能假定 tid 一定存在于异步日志线程的 MDC 中。日志编码器 只读取允许的关联字段,不得把整个 MDC Map 自动写入 JSON。

7. Request ID

7.1 输入规则

客户端可以传入 X-Request-Id,但必须同时满足:

  • 长度为 164 个字符。
  • 首字符是字母或数字。
  • 其余字符只允许字母、数字、._:-

建议校验表达式:

^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$

请求头缺失、为空或不合法时,服务端生成 UUID。响应始终返回最终采用的 X-Request-Id

7.2 代码边界

新增通用 RequestIdAccessor 和对应的 Request ID Scope

  • Filter 负责解析、校验、建立和清理 Request ID 上下文。
  • 独立 ThreadLocal Scope 是 Request ID 的进程内权威来源。
  • 为该 Scope 注册 Micrometer ThreadLocalAccessor,由 ContextPropagatingTaskDecorator 捕获、恢复和清理。
  • Scope 同步维护日志所需的 MDC 镜像,但读取方不能把 MDC 当作权威来源。
  • API 响应工厂通过该抽象读取 Request ID。
  • 审计编排通过该抽象或明确参数读取 Request ID。
  • 应用服务、Controller 和 DTO 不再直接调用 MDC.get()
  • MDC 只作为日志适配器,不再作为业务上下文的权威来源。

8. Tracing 配置

skillhub-app 使用 Spring Boot 3.2.3 管理的依赖版本:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

基础配置:

management:
  tracing:
    sampling:
      probability: ${SKILLHUB_TRACING_SAMPLING_PROBABILITY:0.1}
    baggage:
      enabled: false
    propagation:
      type: W3C
  otlp:
    tracing:
      timeout: ${SKILLHUB_OTLP_TIMEOUT:5s}
      compression: ${SKILLHUB_OTLP_COMPRESSION:gzip}

基础配置不得为 OTLP endpoint 提供默认地址。只有 otel-sdk 部署显式设置以下标准 Spring Boot 配置时才创建 Exporter

MANAGEMENT_OTLP_TRACING_ENDPOINT=http://otel-collector:4318/v1/traces

一期沿用 OpenTelemetry 1.31 的默认 BatchSpanProcessor 有界队列和丢弃策略,不增加应用内 重试、熔断或自定义队列实现。

9. 日志输出

9.1 输出模式

  • 本地开发默认使用可读的文本日志。
  • SKILLHUB_LOG_FORMAT=json 启用 ECS 风格 JSON stdout。
  • JSON 编码器显式输出标准字段和三个关联字段,不启用“输出全部 MDC”。
  • JSON ConsoleAppender 外包一层 Logback AsyncAppender初始队列容量为 1024并允许通过 SKILLHUB_LOG_ASYNC_QUEUE_SIZE 调整。
  • AsyncAppender 使用非阻塞策略;队列耗尽时日志可能丢失,审计事实不依赖该通道。
  • 异常使用 error.typeerror.messageerror.stack_trace
  • 队列容量保持可配置,默认值在原型压测后固定,不在设计阶段猜测。
  • 异常和队列丢弃行为必须在测试中验证。

示例:

{
  "@timestamp": "2026-07-31T10:10:10.123Z",
  "log.level": "INFO",
  "service.name": "skillhub",
  "service.version": "0.2.15",
  "service.environment": "test",
  "request.id": "req-123",
  "trace.id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span.id": "00f067aa0ba902b7",
  "log.logger": "com.iflytek.skillhub...",
  "message": "..."
}

应用只输出 stdout不直接依赖 Elasticsearch SDK也不直接写 Elasticsearch。

9.2 审计边界

audit_log 数据库记录仍是审计事实来源。stdout 日志不能代替审计记录,审计留存和归档 不在本方案中处理。

10. 上下文传播

10.1 Spring 异步执行器

为现有 skillhubEventExecutor 配置 Spring Framework 6.1 的 ContextPropagatingTaskDecorator

  • 提交任务时捕获 Request ID 和 Trace Context。
  • 执行任务时恢复上下文。
  • 执行完成后在 finally 中清理。
  • CallerRunsPolicy 触发时也必须保持正确的嵌套作用域。

测试必须重复复用同一工作线程,证明不同请求之间不会串号。

10.2 消息队列与长生命周期后台线程

Redis Stream 消费循环和 Reclaimer 不继承应用启动线程或任意请求的 MDC。Producer 通过 通用消息 Observation 把 W3C Trace Context 与受控 Request ID 注入 transport metadata Consumer/Reclaimer 逐条提取、建立 Scope并在处理结束后清理。Scanner HTTP 调用自然成为 Consumer Span 的子调用。

上下文不写入 ScanTask 或搜索业务 payload也不改变可靠任务状态机。普通定时任务没有 上游 carrier仍建立独立执行上下文长期延迟任务使用稳定任务 ID 或 Span Link不维持 超长父 Span。

10.3 HTTP 出站

一期只管理两类 HTTP Client

  • 内部 Scanner Client使用 Spring 管理且带 Observation 的 Builder传播 W3C Trace Context。
  • 其他现有 ClientGitHub、GitLab、内置 Skill 公网下载和 S3 Client 均不在一期新增 Trace Context 传播。

后续新增 Client 必须明确选择内部或外部配置,不能依赖全局 Host 正则或在业务代码中手工 删除 Header。

11. SkyWalking 与 Elasticsearch 接入

11.1 OTel SDK 模式

推荐链路:

SkillHub
  → OTLP/HTTP
  → OpenTelemetry Collector
  → OTLP
  → SkyWalking OAP

Collector 用于协议适配和后端路由,不是 SkillHub 的启动依赖。

SkyWalking 10.3 的 OTLP Trace 会转换为 Zipkin Trace并通过 Zipkin Query/Lens UI 查询。 它不等价于 SkyWalking Java Agent 的原生拓扑、慢 SQL 和 Profiling 能力,部署文档必须 明确该差异。原型报告必须记录实际使用的 Maven 依赖、Collector、OAP 和 Agent 版本及 查询结果。

11.2 External Agent 模式

需要 SkyWalking 原生能力时:

  • 使用 external-agent
  • 不配置 SkillHub OTLP endpoint。
  • 由部署环境挂载并启动 SkyWalking Java Agent。
  • 使用 SkyWalking 官方 Logback Toolkit 提供 Trace ID。
  • 日志基础设施将 tid 映射为 trace.id

11.3 日志链路

SkillHub JSON stdout
  → Filebeat / Fluent Bit
  → Elasticsearch
  → Kibana

Kibana 使用 trace.id 查询日志SkyWalking 使用同一个 Trace ID 查询调用链。

12. 实施步骤

阶段一Request ID 与日志边界

  1. 增加 Request ID 校验。
  2. 建立 RequestIdAccessor
  3. 移除应用服务、Controller、DTO 对 MDC 的直接读取。
  4. 增加允许字段明确的结构化日志配置。
  5. 增加 Request ID 和日志字段测试。

可观察结果:

  • 非法 Request ID 被替换。
  • API 响应和审计记录仍使用同一 Request ID。
  • 业务类不再 import org.slf4j.MDC

阶段二Micrometer + OTel

  1. 增加 Tracing Bridge 和 OTLP Exporter 依赖。
  2. 增加 noneotel-sdkexternal-agent 模式。
  3. 设置 W3C、关闭 baggage、配置采样率。
  4. 保证无 endpoint 时不会产生网络连接。
  5. 保证每个模式只存在一个实际 Tracer。

可观察结果:

  • none 模式只有 request.id
  • otel-sdk 模式日志出现标准 Trace 字段。
  • external-agent 模式不会产生应用内 OTel Trace。

阶段三:传播边界

  1. skillhubEventExecutor 增加上下文传播。
  2. 验证线程复用、嵌套任务和 CallerRunsPolicy
  3. 让内部 Scanner Client 使用 Spring 管理且可观测的 Client Builder。
  4. 验证外部 HTTP Client 不发送 Trace Context。

阶段四:部署示例与远端验证

  1. 提供最小 OTel Collector 配置示例。
  2. 补充 SkyWalking OTLP 与 Agent 模式差异。
  3. 将待测分支合入 big-main,记录合入后的精确 Commit SHA。
  4. 构建绑定 big-main SHA 的测试镜像。
  5. 在共享测试机使用独立容器、网络、数据卷和动态端口运行三个原型。
  6. 生成中文测试报告并保存在本地私有目录,不提交开源仓库。

每个阶段使用独立的小提交并保留在同一实现分支;前一阶段的范围测试通过后再进入下一 阶段。公开 Issue 和 PR 统一在阶段五创建。

阶段五:社区交付(最后执行)

该阶段必须在远端验证全部通过后执行:

  1. 创建新的可观测性建设 Issue说明它承接 #597 中的“通用日志关联与链路追踪”部分。
  2. 搜索索引可靠异步交付继续作为独立问题,不混入新的可观测性 Issue。
  3. 从经过验证的实现分支创建新的 PR并关联新 Issue。
  4. PR 只包含公开代码、配置、自动化测试和公开部署说明;不得包含测试机地址、凭证、 私有端口、原始远端日志或本地中文测试报告。
  5. 在 #597、#644 及其他被替代的关联项中回复:
    • 原问题是否真实存在。
    • 为什么不采用原 PR 的实现。
    • 新方案的边界和主要改动。
    • 已完成的自动化及远端验证摘要。
    • 新 Issue 和替代 PR 的链接。
  6. 确认维护者需要的信息完整后,关闭已被替代的 PR不在验证完成前抢先关闭。
  7. #597 等关联 Issue 只根据剩余问题是否已有明确承接决定关闭、缩小范围或继续保留, 不因替代 PR 创建而自动关闭。
  8. 新 PR 通过 Review 和 CI 后,确认 PR Head 仍等于已验证的功能 SHA且该 SHA 可从已 测试的 big-main SHA 到达;满足后才允许更新 main
  9. 如果 Review 或 CI 修复改变了代码、配置或测试脚本,则原验证证据失效:先将新 SHA 合入 big-main,重新构建镜像并完成受影响的远端验证,再更新 main

13. 验证方案

13.1 自动化测试

至少覆盖:

  • 未传 Request ID 时自动生成。
  • 合法 Request ID 被保留。
  • 空值、超长值和非法字符被替换。
  • Filter 正常、异常退出后都清理上下文。
  • API 响应、审计和日志中的 Request ID 一致。
  • JSON 只输出允许的关联字段。
  • Trace 采样率在测试中设为 1.0 后可稳定断言。
  • @Async 线程恢复父上下文。
  • 连续复用同一线程执行不同请求时不串号。
  • CallerRunsPolicy 下上下文正确恢复。
  • noneotel-sdkexternal-agent 的 Spring Context 互斥。
  • 未配置 OTLP endpoint 时不创建网络导出。
  • 内部 Scanner 请求携带 traceparent
  • Redis Stream Producer/Consumer 保持同一 Trace 和 Request ID处理结束后线程不串号。
  • 重试发布和 Reclaimer 重新消费仍能恢复消息关联上下文。
  • 外部 HTTP 请求不携带 traceparent

13.2 远端原型

原型 Anone

  • 不部署 Collector。
  • SkillHub 正常启动并完成核心 Smoke Test。
  • 日志存在 request.id,不存在伪造的 Trace 字段。

原型 Botel-sdk

  • SkillHub → Collector → SkyWalking 跑通。
  • JSON 日志进入 Elasticsearch/Kibana。
  • Kibana 与 SkyWalking 能用同一 trace.id 查询。
  • Collector 停止后 SkillHub API 和异步任务继续工作。

原型 Cexternal-agent

  • SkyWalking Java Agent 提供原生 Trace。
  • 应用内 OTel Exporter 不工作。
  • 日志能用 SkyWalking Trace ID 关联。
  • 不产生双 Trace、重复 Span 或两个冲突的 Trace ID。

13.3 远端测试场景

  • HTTP 成功、4xx、5xx 和未认证请求。
  • Scanner 成功、超时和失败。
  • 异步事件正常执行和抛出异常。
  • Redis Stream 正常消费、失败重试、Pending Reclaim 和重复投递。
  • 并发请求重复使用线程池。
  • Collector 启动、停止和恢复。
  • 日志采集器停止或消费变慢。
  • 采样率 0.00.11.0
  • 容器收到 SIGTERM 后日志和 Trace 的关闭行为。
  • 日志中不出现 Authorization、Cookie、Token、密码和完整请求体。

14. 验收标准

以下条件全部满足后,一期才算完成:

  • 三种模式行为与本文一致。
  • 业务代码不再直接读取或写入 MDC。
  • Request ID 校验、响应和审计关联测试通过。
  • 日志字段符合约定,且不输出完整 MDC。
  • Spring 异步执行器上下文传播和隔离测试通过。
  • Redis Stream 消息上下文传播、重试、Reclaimer 和隔离测试通过。
  • 内外部 HTTP 传播边界测试通过。
  • 无 OTLP endpoint 时不存在外部连接尝试。
  • Collector 中断不影响 SkillHub 业务结果。
  • OTel SDK 与 SkyWalking Agent 不会同时产生 Trace。
  • make test-backend-app 通过。
  • make typecheck-webmake lint-web 通过。
  • 基于 big-main 合入后精确 SHA 构建的远端三个原型通过。
  • 中文测试报告保存在本地私有目录。
  • 新的可观测性 Issue 和替代 PR 已创建并互相关联。
  • #597、#644 等关联项已获得清晰回复,被替代的旧 PR 已关闭。
  • 关联 Issue 已根据剩余范围分别关闭、缩小范围或保留,且状态理由清楚。
  • 新 PR Head 与已验证功能 SHA 一致,且可从已测试的 big-main SHA 到达。
  • 通过验证后才允许更新 main

15. 回滚

出现问题时:

  1. SKILLHUB_TRACING_MODE 改为 none
  2. 删除 MANAGEMENT_OTLP_TRACING_ENDPOINT
  3. SKILLHUB_LOG_FORMAT 改为 text
  4. 保留 Request ID 和原有文本日志能力。
  5. 通过滚动重启恢复,不进行运行时模式切换。

Tracing 和结构化日志关闭后不得影响 SkillHub 的业务状态、数据库状态或任务执行语义。

16. 已知限制

  • 10% Head Sampling 下,全量日志中的部分 trace.id 在 SkyWalking 中没有对应 Trace。
  • SkyWalking OTLP 模式的展示能力弱于原生 Java Agent。
  • 日志队列在背压时可能丢弃日志,这是保护业务线程的预期行为。
  • External Agent 提供哪些 MDC 字段取决于具体 Agent 和版本。
  • 一期只处理通用关联和传播,不保证搜索索引异步交付可靠性。

17. 参考资料