From e9a913e30b81516c1dff67f4e9c249ced38a29d6 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:51:31 +0800 Subject: [PATCH] fix(observability): tighten tracing integration boundaries Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .env.release.example | 2 +- compose.release.yml | 2 +- docs/09-deployment.md | 10 +- ...6-07-31-observability-construction-plan.md | 12 +- docs/observability-decision-map.md | 24 +-- docs/observability-developer-guide.md | 138 ++++++++++++++++++ .../auth/oauth/GitLabClaimsExtractor.java | 2 + 7 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 docs/observability-developer-guide.md diff --git a/.env.release.example b/.env.release.example index 1b8e38e6..3f8380b2 100644 --- a/.env.release.example +++ b/.env.release.example @@ -56,7 +56,7 @@ SESSION_COOKIE_SECURE=false # Observability defaults require no Collector or tracing backend. # Use json in container deployments when stdout is collected centrally. SKILLHUB_TRACING_MODE=none -SKILLHUB_LOG_FORMAT=text +SKILLHUB_LOG_FORMAT=json SKILLHUB_LOG_ASYNC_QUEUE_SIZE=1024 SKILLHUB_SERVICE_VERSION=unknown SKILLHUB_SERVICE_ENVIRONMENT=production diff --git a/compose.release.yml b/compose.release.yml index db306bda..e9cb2a44 100644 --- a/compose.release.yml +++ b/compose.release.yml @@ -89,7 +89,7 @@ services: SKILLHUB_SECURITY_SCANNER_MODE: upload SKILLHUB_AUTH_DIRECT_ENABLED: ${SKILLHUB_AUTH_DIRECT_ENABLED:-false} SKILLHUB_TRACING_MODE: ${SKILLHUB_TRACING_MODE:-none} - SKILLHUB_LOG_FORMAT: ${SKILLHUB_LOG_FORMAT:-text} + SKILLHUB_LOG_FORMAT: ${SKILLHUB_LOG_FORMAT:-json} SKILLHUB_LOG_ASYNC_QUEUE_SIZE: ${SKILLHUB_LOG_ASYNC_QUEUE_SIZE:-1024} SKILLHUB_SERVICE_VERSION: ${SKILLHUB_SERVICE_VERSION:-unknown} SKILLHUB_SERVICE_ENVIRONMENT: ${SKILLHUB_SERVICE_ENVIRONMENT:-production} diff --git a/docs/09-deployment.md b/docs/09-deployment.md index 847391c7..6a6a5276 100644 --- a/docs/09-deployment.md +++ b/docs/09-deployment.md @@ -331,13 +331,14 @@ override 或部署平台环境变量把上述 `SPRING_SECURITY_*` 变量注入 ` ```dotenv SKILLHUB_TRACING_MODE=none -SKILLHUB_LOG_FORMAT=text +SKILLHUB_LOG_FORMAT=json SKILLHUB_SERVICE_VERSION=v0.2.15 SKILLHUB_SERVICE_ENVIRONMENT=production ``` -部署环境建议将 `SKILLHUB_LOG_FORMAT` 设为 `json`,由 Filebeat、Fluent Bit 或容器平台 -采集 stdout。SkillHub 不直接连接 Elasticsearch。JSON 日志使用以下稳定字段: +发布 Compose 默认使用 ECS 风格 JSON,由 Filebeat、Fluent Bit 或容器平台采集 stdout。 +本地源码开发仍可使用 `SKILLHUB_LOG_FORMAT=text`。SkillHub 不直接连接 Elasticsearch。 +JSON 日志使用以下稳定字段: - `request.id`:SkillHub 请求、响应和审计关联 ID。 - `trace.id`、`span.id`:当前存在有效 Trace 时输出。 @@ -447,6 +448,9 @@ SkyWalking 中没有被保留的 Trace。这是头部采样的预期行为。 关闭 Trace 和 JSON 日志不会改变请求、数据库或异步任务的业务语义。 +开发者接入统一标准的最小步骤、内部/外部 HTTP Client 传播边界和扩展点见: +[可观测性开发者接入指南](./observability-developer-guide.md)。 + ## 11 安全扫描服务 如果要启用 `skill-scanner` 后端链路,当前仓库建议按下面的方式部署: diff --git a/docs/2026-07-31-observability-construction-plan.md b/docs/2026-07-31-observability-construction-plan.md index 68852835..cbbe85fe 100644 --- a/docs/2026-07-31-observability-construction-plan.md +++ b/docs/2026-07-31-observability-construction-plan.md @@ -35,7 +35,8 @@ Issue #597 中“搜索索引可靠异步交付”应作为独立问题处理, Elasticsearch/Kibana。 7. Trace 可以选择通过 OTLP Collector 接入 SkyWalking。 8. Collector、SkyWalking、Elasticsearch 或日志采集器不可用时,SkillHub 业务继续运行。 -9. 同一进程只能有一个实际生效的 Tracer。 +9. SkillHub 应用配置只能启用一个应用内 Tracer;`external-agent` 模式下唯一外部 + Agent 由部署参数和发布检查保证。 本方案按多个小阶段、小提交实施和验证,全部通过后再统一创建一个替代 PR。 @@ -104,14 +105,15 @@ skillhub: 必须保证: - `none` 和 `external-agent` 不创建应用内 OTel Span。 -- `otel-sdk` 不支持同时启用 SkyWalking、OTel 或其他外部 Tracing Agent。 +- `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 冲突;SkyWalking 特有高级能力不进入 SkillHub -核心代码。 +一期实现并验证三种模式的应用上下文互斥边界和日志关联。`external-agent` 只验证 +SkyWalking Agent 接管 Trace 时应用内 OTel Tracer/Exporter 不工作;“只挂载一个外部 +Agent”属于部署验收项。SkyWalking 特有高级能力不进入 SkillHub 核心代码。 ## 6. 关联字段契约 diff --git a/docs/observability-decision-map.md b/docs/observability-decision-map.md index 7ca4483b..3f9ad918 100644 --- a/docs/observability-decision-map.md +++ b/docs/observability-decision-map.md @@ -1,11 +1,14 @@ # 通用可观测性决策图 目标:为 SkillHub 建立独立、通用、可插拔的日志关联、指标和链路追踪基础设施。 -它观察 HTTP、线程池、定时任务和可靠任务等执行边界,但不进入业务模型和业务载荷。 +当前实现覆盖 HTTP、SkillHub 管理的线程池和明确接入的内部 HTTP Client;定时任务、 +Redis Stream 消费循环和 Reclaimer 是独立后台边界,不继承 HTTP 上下文。 +这些边界不进入业务模型和业务载荷。 边界: -- Servlet Filter、执行器装饰器、调度拦截器和任务执行拦截器负责建立/恢复上下文。 +- Servlet Filter、执行器装饰器和明确接入的 Client Builder 负责建立/恢复上下文。 +- 定时任务和 Redis Stream 目前只输出自身执行日志,不自动继承请求或 Trace 上下文。 - 业务代码不读写 MDC,不负责创建通用 Span,也不负责统计任务生命周期指标。 - 使用 W3C Trace Context;日志后端、Metrics 后端和 Trace Exporter 均可替换。 - 上下文是有长度限制的基础设施元数据,不进入业务 payload。 @@ -13,8 +16,9 @@ 必须满足的不变量: -- 每个执行边界都正确建立作用域并在 `finally` 清理,线程复用不得串号。 -- 日志稳定输出 `requestId`、`traceId`、`spanId`;任务执行时额外输出执行资源标识。 +- 每个已纳入本期的执行边界都正确建立作用域并在 `finally` 清理,线程复用不得串号。 +- 日志稳定输出 `requestId`、`traceId`、`spanId`(存在时);任务执行资源标识不由本期 + 可观测性自动生成。 - Trace 与 Metrics 可关闭、可替换;关闭后不得改变业务行为。 - 指标只使用低基数维度,业务 ID 不进入标签。 - 采集端不可用必须异步、限时、限队列并 fail-open。 @@ -40,8 +44,8 @@ Type: Research ### Question -如何区分现有 `X-Request-Id`、W3C `traceId/spanId` 和执行资源标识,并跨 HTTP、线程池、 -调度器和持久化任务边界传播? +如何区分现有 `X-Request-Id`、W3C `traceId/spanId` 和执行资源标识,并跨 HTTP、线程池 +边界传播? ### Answer @@ -50,7 +54,7 @@ Type: Research - `requestId` 是 SkillHub 的请求/审计关联标识,不冒充分布式 Trace。 - `traceId/spanId` 由 Tracer 生成,跨进程只使用 W3C `traceparent/tracestate`。 - 定时任务或可靠任务的执行资源 ID 只作为当前执行作用域属性,不进入业务 payload。 -- HTTP、线程池、调度器和持久化 carrier 的注入/提取全部位于基础设施拦截器。 +- HTTP 和线程池 carrier 的注入/提取位于基础设施拦截器;持久化任务不携带 HTTP Trace。 - 不传播任意 MDC Map;baggage 默认关闭,任何允许项都必须低敏、限长、显式配置。 - 无效或不可信的公网 Trace Context 按 W3C 规则丢弃,服务端控制采样。 @@ -84,8 +88,9 @@ Type: Prototype ### Question -验证线程复用隔离、嵌套作用域、异步/调度/持久化任务边界、采样、Exporter 超时、 -Collector 中断、队列打满和关闭观测能力等场景。 +验证线程复用隔离、嵌套作用域、异步任务边界、采样、Exporter 超时、Collector 中断、 +队列打满和关闭观测能力等场景。调度任务和 Redis Stream 的独立后台边界只验证不继承 +请求上下文。 ### Answer @@ -97,6 +102,7 @@ Collector 中断、队列打满和关闭观测能力等场景。 - Scanner 使用 Spring 管理的 `WebClient.Builder` 传播 W3C `traceparent`。 - 面向用户配置的 GitLab 外部 Client 不传播 Trace Context。 - `none / otel-sdk / external-agent` 的应用上下文和 Exporter 条件符合设计。 +- `@Scheduled` 和 Redis Stream/Reclaimer 不继承请求上下文,保持独立后台执行边界。 Collector 中断、日志背压、采样率和关闭行为仍由 `big-main` 精确 SHA 镜像的远端原型验证。 diff --git a/docs/observability-developer-guide.md b/docs/observability-developer-guide.md new file mode 100644 index 00000000..205a921c --- /dev/null +++ b/docs/observability-developer-guide.md @@ -0,0 +1,138 @@ +# 可观测性开发者接入指南 + +本文说明 SkillHub 代码如何接入统一的日志关联和链路追踪标准。 +开发者不需要直接操作 MDC、OpenTelemetry SDK 或 SkyWalking API。 + +## 1. 统一标准 + +| 信息 | 来源 | 日志字段 | 传播方式 | +|---|---|---|---| +| 请求关联 ID | `RequestIdFilter` / `RequestIdAccessor` | `request.id` | `X-Request-Id` | +| 分布式 Trace ID | Micrometer Tracing | `trace.id` | W3C `traceparent` | +| Span ID | Micrometer Tracing | `span.id` | 当前 Trace Scope | + +`request.id` 是 SkillHub 的请求/审计关联标识,不等同于 `trace.id`。 +请求没有链路追踪时仍应保留 `request.id`。 + +## 2. 运行模式 + +通过 `SKILLHUB_TRACING_MODE` 选择一种模式,修改后重启应用: + +- `none`:默认模式。无应用内 OTel SDK 和 OTLP 导出,只保留 `request.id`。 +- `otel-sdk`:使用 Micrometer Tracing + OTel Bridge;配置 + `MANAGEMENT_OTLP_TRACING_ENDPOINT` 后才向 Collector 导出。 +- `external-agent`:应用内 Tracer 为 NOOP,由部署环境提供唯一的外部 Agent。 + SkillHub 只能校验自身配置,不能识别任意 JVM Agent;唯一 Agent 是部署检查项。 + +`none`/`external-agent` 不能配置 OTLP endpoint;`otel-sdk` 与外部 Tracing Agent +不得在同一进程中叠加。 + +## 3. 开发者接入方式 + +### 3.1 普通 HTTP 请求 + +不需要增加代码。`RequestIdFilter` 会生成或校验 `X-Request-Id`,并在请求结束时清理 +线程上下文。Micrometer Tracing 负责在 `otel-sdk` 模式下创建 HTTP Observation 和 Trace。 + +业务代码不要: + +- `MDC.put` / `MDC.remove` 写入请求关联字段; +- 手工解析或拼接 `traceparent`; +- 在日志中输出完整 MDC Map。 + +### 3.2 Spring 异步任务 + +优先使用已有的 `skillhubEventExecutor`: + +```java +@Async("skillhubEventExecutor") +public void handleEvent(SkillPublishedEvent event) { + // 直接记录日志即可,request.id/trace.id/span.id 会按提交时的上下文恢复 +} +``` + +新增 Spring 管理的线程池时,注入统一的 +`ContextPropagatingTaskDecorator`,不要自己复制 MDC: + +```java +@Bean +ThreadPoolTaskExecutor myExecutor( + ContextPropagatingTaskDecorator contextDecorator +) { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setTaskDecorator(contextDecorator); + executor.initialize(); + return executor; +} +``` + +该装饰器负责捕获、恢复和清理 `RequestIdAccessor` 与 OTel Observation Scope。 + +### 3.3 内部 HTTP 服务 + +内部服务调用必须使用 Spring 管理的 `WebClient.Builder`,这样 `otel-sdk` 模式下会 +自动传播 W3C Trace Context: + +```java +@Bean +HttpClient scannerClient( + WebClient.Builder builder +) { + return new WebClientHttpClient(builder.build()); +} +``` + +Scanner 是当前已接入的内部客户端。新增内部客户端时,应补一个测试,断言请求包含合法 +的 `traceparent`。 + +### 3.4 外部 HTTP 服务 + +面向用户配置的 GitLab、第三方 API 等外部服务不要复用内部观测 Builder,也不要手工 +删除 Header。使用明确不接入 SkillHub Observation 的客户端,并补测试断言请求不包含 +`traceparent`。 + +### 3.5 定时任务和 Redis Stream + +当前实现把定时任务、Redis Stream 消费循环和 Reclaimer 视为独立后台执行边界: + +- 不继承任意 HTTP 请求的 `request.id` 或 `trace.id`; +- 不把 HTTP Trace Context 写入 Redis 业务载荷; +- 日志仍可使用 ECS 格式和固定服务字段; +- 若未来需要任务级关联,应增加独立的任务执行 ID/Observation carrier,并单独设计 + 持久化与重试语义。 + +因此,不要假设在 `@Scheduled` 或 Stream consumer 中能自动查到发起 HTTP 请求的 Trace。 + +## 4. 可扩展点 + +| 扩展需求 | 应扩展的位置 | 不应修改的位置 | +|---|---|---| +| 新增请求关联来源 | `RequestIdFilter` / `RequestIdAccessor` | 业务 Controller、DTO | +| 新增线程上下文 | `RequestIdThreadLocalAccessor` / `ContextRegistry` | 每个任务的 `MDC` 代码 | +| 新增 Tracing 后端 | Micrometer Bridge / Collector 配置 | 业务服务 | +| 新增日志字段 | `SkillHubEcsEncoder` 白名单 | “输出全部 MDC” | +| 新增内部 HTTP 客户端 | Spring `WebClient.Builder` + propagation test | URL 正则删 Header | +| 新增外部 HTTP 客户端 | 独立客户端构建入口 + no-propagation test | 依赖全局默认行为 | + +## 5. 接入验收清单 + +新增一个执行边界或客户端时,至少补充: + +1. `none` 模式下业务结果不变; +2. `otel-sdk` 模式下内部调用的 `traceparent` 合法; +3. 外部调用不携带 `traceparent`; +4. 线程复用后上下文被清理,不发生串号; +5. 日志只出现 `request.id`、`trace.id`、`span.id` 等白名单字段; +6. Collector 不可用时不影响业务结果。 + +运行后端验证使用: + +```bash +make test-backend-app +``` + +部署级变更再运行: + +```bash +make staging +``` diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitLabClaimsExtractor.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitLabClaimsExtractor.java index 7a744a30..6f0a87b4 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitLabClaimsExtractor.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitLabClaimsExtractor.java @@ -3,6 +3,7 @@ package com.iflytek.skillhub.auth.oauth; import com.fasterxml.jackson.annotation.JsonProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; @@ -31,6 +32,7 @@ public class GitLabClaimsExtractor implements OAuthClaimsExtractor { * Uses an external-service client that is intentionally not customized with application * tracing. Trace context must not be propagated to a user-configured GitLab host. */ + @Autowired public GitLabClaimsExtractor() { this(RestClient.builder()); }