chore(integration): stage observability boundary fixes

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-07-31 15:52:11 +08:00
commit 7f5d5466ff
7 changed files with 171 additions and 19 deletions

View file

@ -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

View file

@ -106,7 +106,7 @@ services:
SKILLHUB_AUTH_CAS_ATTRIBUTES_EMAIL: ${SKILLHUB_AUTH_CAS_ATTRIBUTES_EMAIL:-}
SKILLHUB_AUTH_CAS_ATTRIBUTES_AVATAR_URL: ${SKILLHUB_AUTH_CAS_ATTRIBUTES_AVATAR_URL:-}
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}

View file

@ -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` 后端链路,当前仓库建议按下面的方式部署:

View file

@ -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. 关联字段契约

View file

@ -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 镜像的远端原型验证。

View file

@ -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
```

View file

@ -8,6 +8,7 @@ import com.iflytek.skillhub.auth.identity.ProviderAuthenticationResult;
import com.iflytek.skillhub.auth.identity.SubjectCandidate;
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;
@ -38,6 +39,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());
}