* 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>
21 KiB
SkillHub 日志关联与链路追踪建设方案
日期:2026-07-31
状态:Accepted(2026-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. 建设目标
一期需要实现:
- 每个 HTTP 请求都有合法的
request.id。 - 启用 Tracing 时,日志包含标准
trace.id和span.id。 otel-sdk模式使用 W3Ctraceparent/tracestate传播 Trace Context。- 业务代码不直接读写 MDC,也不直接依赖 OpenTelemetry 或 SkyWalking API。
- 现有 Spring
@Async执行器能够正确传播并清理上下文。 - 日志以结构化 JSON 输出到 stdout,可由 Filebeat/Fluent Bit 采集到 Elasticsearch/Kibana。
- Trace 可以选择通过 OTLP Collector 接入 SkyWalking。
- Collector、SkyWalking、Elasticsearch 或日志采集器不可用时,SkillHub 业务继续运行。
- SkillHub 应用配置只能启用一个应用内 Tracer;
external-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 |
运行模式是启动期不变量,不支持热切换。
必须保证:
none和external-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.id 与 trace.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,但必须同时满足:
- 长度为 1–64 个字符。
- 首字符是字母或数字。
- 其余字符只允许字母、数字、
.、_、:、-。
建议校验表达式:
^[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.type、error.message、error.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。
- 其他现有 Client:GitHub、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 与日志边界
- 增加 Request ID 校验。
- 建立
RequestIdAccessor。 - 移除应用服务、Controller、DTO 对 MDC 的直接读取。
- 增加允许字段明确的结构化日志配置。
- 增加 Request ID 和日志字段测试。
可观察结果:
- 非法 Request ID 被替换。
- API 响应和审计记录仍使用同一 Request ID。
- 业务类不再 import
org.slf4j.MDC。
阶段二:Micrometer + OTel
- 增加 Tracing Bridge 和 OTLP Exporter 依赖。
- 增加
none、otel-sdk、external-agent模式。 - 设置 W3C、关闭 baggage、配置采样率。
- 保证无 endpoint 时不会产生网络连接。
- 保证每个模式只存在一个实际 Tracer。
可观察结果:
none模式只有request.id。otel-sdk模式日志出现标准 Trace 字段。external-agent模式不会产生应用内 OTel Trace。
阶段三:传播边界
- 为
skillhubEventExecutor增加上下文传播。 - 验证线程复用、嵌套任务和
CallerRunsPolicy。 - 让内部 Scanner Client 使用 Spring 管理且可观测的 Client Builder。
- 验证外部 HTTP Client 不发送 Trace Context。
阶段四:部署示例与远端验证
- 提供最小 OTel Collector 配置示例。
- 补充 SkyWalking OTLP 与 Agent 模式差异。
- 将待测分支合入
big-main,记录合入后的精确 Commit SHA。 - 构建绑定
big-mainSHA 的测试镜像。 - 在共享测试机使用独立容器、网络、数据卷和动态端口运行三个原型。
- 生成中文测试报告并保存在本地私有目录,不提交开源仓库。
每个阶段使用独立的小提交并保留在同一实现分支;前一阶段的范围测试通过后再进入下一 阶段。公开 Issue 和 PR 统一在阶段五创建。
阶段五:社区交付(最后执行)
该阶段必须在远端验证全部通过后执行:
- 创建新的可观测性建设 Issue,说明它承接 #597 中的“通用日志关联与链路追踪”部分。
- 搜索索引可靠异步交付继续作为独立问题,不混入新的可观测性 Issue。
- 从经过验证的实现分支创建新的 PR,并关联新 Issue。
- PR 只包含公开代码、配置、自动化测试和公开部署说明;不得包含测试机地址、凭证、 私有端口、原始远端日志或本地中文测试报告。
- 在 #597、#644 及其他被替代的关联项中回复:
- 原问题是否真实存在。
- 为什么不采用原 PR 的实现。
- 新方案的边界和主要改动。
- 已完成的自动化及远端验证摘要。
- 新 Issue 和替代 PR 的链接。
- 确认维护者需要的信息完整后,关闭已被替代的 PR;不在验证完成前抢先关闭。
- #597 等关联 Issue 只根据剩余问题是否已有明确承接决定关闭、缩小范围或继续保留, 不因替代 PR 创建而自动关闭。
- 新 PR 通过 Review 和 CI 后,确认 PR Head 仍等于已验证的功能 SHA,且该 SHA 可从已
测试的
big-mainSHA 到达;满足后才允许更新main。 - 如果 Review 或 CI 修复改变了代码、配置或测试脚本,则原验证证据失效:先将新 SHA
合入
big-main,重新构建镜像并完成受影响的远端验证,再更新main。
13. 验证方案
13.1 自动化测试
至少覆盖:
- 未传 Request ID 时自动生成。
- 合法 Request ID 被保留。
- 空值、超长值和非法字符被替换。
- Filter 正常、异常退出后都清理上下文。
- API 响应、审计和日志中的 Request ID 一致。
- JSON 只输出允许的关联字段。
- Trace 采样率在测试中设为
1.0后可稳定断言。 @Async线程恢复父上下文。- 连续复用同一线程执行不同请求时不串号。
CallerRunsPolicy下上下文正确恢复。none、otel-sdk、external-agent的 Spring Context 互斥。- 未配置 OTLP endpoint 时不创建网络导出。
- 内部 Scanner 请求携带
traceparent。 - Redis Stream Producer/Consumer 保持同一 Trace 和 Request ID,处理结束后线程不串号。
- 重试发布和 Reclaimer 重新消费仍能恢复消息关联上下文。
- 外部 HTTP 请求不携带
traceparent。
13.2 远端原型
原型 A:none
- 不部署 Collector。
- SkillHub 正常启动并完成核心 Smoke Test。
- 日志存在
request.id,不存在伪造的 Trace 字段。
原型 B:otel-sdk
- SkillHub → Collector → SkyWalking 跑通。
- JSON 日志进入 Elasticsearch/Kibana。
- Kibana 与 SkyWalking 能用同一
trace.id查询。 - Collector 停止后 SkillHub API 和异步任务继续工作。
原型 C:external-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.0、0.1和1.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-web和make lint-web通过。- 基于
big-main合入后精确 SHA 构建的远端三个原型通过。 - 中文测试报告保存在本地私有目录。
- 新的可观测性 Issue 和替代 PR 已创建并互相关联。
- #597、#644 等关联项已获得清晰回复,被替代的旧 PR 已关闭。
- 关联 Issue 已根据剩余范围分别关闭、缩小范围或保留,且状态理由清楚。
- 新 PR Head 与已验证功能 SHA 一致,且可从已测试的
big-mainSHA 到达。 - 通过验证后才允许更新
main。
15. 回滚
出现问题时:
- 将
SKILLHUB_TRACING_MODE改为none。 - 删除
MANAGEMENT_OTLP_TRACING_ENDPOINT。 - 将
SKILLHUB_LOG_FORMAT改为text。 - 保留 Request ID 和原有文本日志能力。
- 通过滚动重启恢复,不进行运行时模式切换。
Tracing 和结构化日志关闭后不得影响 SkillHub 的业务状态、数据库状态或任务执行语义。
16. 已知限制
- 10% Head Sampling 下,全量日志中的部分
trace.id在 SkyWalking 中没有对应 Trace。 - SkyWalking OTLP 模式的展示能力弱于原生 Java Agent。
- 日志队列在背压时可能丢弃日志,这是保护业务线程的预期行为。
- External Agent 提供哪些 MDC 字段取决于具体 Agent 和版本。
- 一期只处理通用关联和传播,不保证搜索索引异步交付可靠性。