1. 概述
本文档从零开始,在一个 Spring Boot + Spring AI 项目中实现完整的可观测性体系------将 Tracing(链路追踪)、Metrics(指标)、Logging(日志)通过 OpenTelemetry 协议统一导出到 Apache SkyWalking。
实现路径:Micrometer Observation → OTel Bridge → OTLP gRPC Exporter → SkyWalking OAP。
1.1 上报流程
SkyWalking 支持的多种数据上报方式,常用的有:
Java Agent:通过-javaagent:skywalking-agent.jar实现,零代码、零依赖、自动埋点、性能最好。OpenTelemetry OTLP:云原生标准,可对接不同类型的监控平台。
Spring 官方支持 Micrometer + OTLP 上报方案,完整数据流:
#mermaid-svg-jFX0dMEAIBRcLV9H{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jFX0dMEAIBRcLV9H .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jFX0dMEAIBRcLV9H .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jFX0dMEAIBRcLV9H .error-icon{fill:#552222;}#mermaid-svg-jFX0dMEAIBRcLV9H .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jFX0dMEAIBRcLV9H .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jFX0dMEAIBRcLV9H .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jFX0dMEAIBRcLV9H .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jFX0dMEAIBRcLV9H .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jFX0dMEAIBRcLV9H .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jFX0dMEAIBRcLV9H .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jFX0dMEAIBRcLV9H .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jFX0dMEAIBRcLV9H .marker.cross{stroke:#333333;}#mermaid-svg-jFX0dMEAIBRcLV9H svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jFX0dMEAIBRcLV9H p{margin:0;}#mermaid-svg-jFX0dMEAIBRcLV9H .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jFX0dMEAIBRcLV9H .cluster-label text{fill:#333;}#mermaid-svg-jFX0dMEAIBRcLV9H .cluster-label span{color:#333;}#mermaid-svg-jFX0dMEAIBRcLV9H .cluster-label span p{background-color:transparent;}#mermaid-svg-jFX0dMEAIBRcLV9H .label text,#mermaid-svg-jFX0dMEAIBRcLV9H span{fill:#333;color:#333;}#mermaid-svg-jFX0dMEAIBRcLV9H .node rect,#mermaid-svg-jFX0dMEAIBRcLV9H .node circle,#mermaid-svg-jFX0dMEAIBRcLV9H .node ellipse,#mermaid-svg-jFX0dMEAIBRcLV9H .node polygon,#mermaid-svg-jFX0dMEAIBRcLV9H .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jFX0dMEAIBRcLV9H .rough-node .label text,#mermaid-svg-jFX0dMEAIBRcLV9H .node .label text,#mermaid-svg-jFX0dMEAIBRcLV9H .image-shape .label,#mermaid-svg-jFX0dMEAIBRcLV9H .icon-shape .label{text-anchor:middle;}#mermaid-svg-jFX0dMEAIBRcLV9H .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jFX0dMEAIBRcLV9H .rough-node .label,#mermaid-svg-jFX0dMEAIBRcLV9H .node .label,#mermaid-svg-jFX0dMEAIBRcLV9H .image-shape .label,#mermaid-svg-jFX0dMEAIBRcLV9H .icon-shape .label{text-align:center;}#mermaid-svg-jFX0dMEAIBRcLV9H .node.clickable{cursor:pointer;}#mermaid-svg-jFX0dMEAIBRcLV9H .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jFX0dMEAIBRcLV9H .arrowheadPath{fill:#333333;}#mermaid-svg-jFX0dMEAIBRcLV9H .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jFX0dMEAIBRcLV9H .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jFX0dMEAIBRcLV9H .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jFX0dMEAIBRcLV9H .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jFX0dMEAIBRcLV9H .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jFX0dMEAIBRcLV9H .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jFX0dMEAIBRcLV9H .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jFX0dMEAIBRcLV9H .cluster text{fill:#333;}#mermaid-svg-jFX0dMEAIBRcLV9H .cluster span{color:#333;}#mermaid-svg-jFX0dMEAIBRcLV9H div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jFX0dMEAIBRcLV9H .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jFX0dMEAIBRcLV9H rect.text{fill:none;stroke-width:0;}#mermaid-svg-jFX0dMEAIBRcLV9H .icon-shape,#mermaid-svg-jFX0dMEAIBRcLV9H .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jFX0dMEAIBRcLV9H .icon-shape p,#mermaid-svg-jFX0dMEAIBRcLV9H .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jFX0dMEAIBRcLV9H .icon-shape .label rect,#mermaid-svg-jFX0dMEAIBRcLV9H .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jFX0dMEAIBRcLV9H .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jFX0dMEAIBRcLV9H .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jFX0dMEAIBRcLV9H :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Storage
OTLP HTTP:4318 上报Trace/gen_ai指标/日志
依据otel-rules配置+MAL解析指标
HTTP查询 OAP:12800
查询存储返回数据
SpringBoot + SpringAI
集成OpenTelemetry
SkyWalking OAP
持久化存储
BanyanDB:17912
ElasticSearch:9200
Booster UI:8080
可视化展示
链路详情 + GenAI指标 + 服务监控大盘
流程说明:
SpringAI应用通过OTLP采集大模型调用链路、Token、耗时gen_ai_*指标- 使用
OTLP协议 (4318) 推送至OAP OAP加载otel-rules规则文件解析指标,存入BanyanDB/ESBoosterUI通过12800端口请求OAP查询数据OAP读取存储数据,UI渲染监控图表
1.2 环境信息
| 组件 | 版本 |
|---|---|
Spring Boot |
3.5.11 |
Spring AI |
1.1.4 |
Micrometer Tracing |
由 Spring Boot 管理 |
OpenTelemetry |
由 Spring Boot 管理 |
SkyWalking OAP |
10.x |
| 传输协议 | OTLP gRPC |
2. 依赖配置
2.1 Maven 依赖
接入 OTLP 依赖清单:
xml
<!-- 1. Spring Boot Actuator:监控基础设施 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 2. Micrometer Tracing 核心 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
<!-- 3. Micrometer → OpenTelemetry 桥接 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<!-- 4. Micrometer OTLP Registry:Metrics 通过 OTLP 导出 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-otlp</artifactId>
</dependency>
<!-- 5. OTel OTLP Exporter:Tracing 通过 OTLP 导出 -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<!-- 6. OTel Logback Appender:应用日志通过 OTLP 导出 -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-logback-appender-1.0</artifactId>
<version>2.14.0-alpha</version>
</dependency>
依赖关系图:
micrometer-tracing (核心 API)
│
├── micrometer-tracing-bridge-otel (Micrometer → OTel)
│ │
│ └── opentelemetry-exporter-otlp (OTLP 导出)
│
└── micrometer-registry-otlp (Metrics OTLP 导出)
opentelemetry-logback-appender-1.0 (Logback → OTLP)
3. 配置文件
3.1 application.yml --- 可观测性配置
配置说明:
log-prompt/log-completion等高敏感字段默认false,开启会把Prompt存入Span,存在隐私泄露风险。Metrics暂不开启:Spring Boot只支持HTTP上报,SkyWalking 10.x版本又只支持PRC
11.x 版本说明:

yaml
# ==================== Spring Boot Actuator 可观测性配置 ====================
management:
endpoints:
web:
base-path: /actuator
exposure:
include: '*' # 暴露所有端点
tracing:
enabled: true
sampling:
probability: 1.0 # 全量采样(生产环境建议 0.1)
otlp:
tracing:
endpoint: http://192.168.1.235:4319
transport: grpc
export:
enabled: true
metrics:
export:
url: http://192.168.1.235:12800/v1/metrics
enabled: false # Metrics 暂不开启
logging:
endpoint: http://192.168.1.235:4319
transport: grpc
export:
enabled: true
# ==================== Spring AI 观测配置 ====================
spring:
ai:
chat:
observations:
include-error-logging: true # 错误时记录日志
log-completion: true # 记录模型回答内容
log-prompt: true # 记录 Prompt 内容
client:
enabled: true
observations:
log-prompt: true
log-completion: true
# ==================== 日志级别 ====================
logging:
level:
io.opentelemetry.exporter.otlp: TRACE
io.opentelemetry.instrumentation.logback: TRACE
io.opentelemetry.sdk.logs: TRACE
io.micrometer.tracing: DEBUG
io.micrometer.registry.otlp: DEBUG
3.2 logback-spring.xml --- Logback OTLP Appender
关键点:
%X{traceId:-}和%X{spanId:-}从MDC中提取当前Span信息,实现日志与Trace的关联OpenTelemetryAppender将日志异步导出到OTLP端点,与application.yml中配置的地址一致
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台输出格式:包含 traceId 和 spanId -->
<property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}][%X{spanId:-}] %-5level %logger{50} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- OTLP Appender:将日志通过 gRPC 上报到 SkyWalking OAP -->
<appender name="OTLP" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
<captureExperimentalAttributes>true</captureExperimentalAttributes>
<captureCodeAttributes>true</captureCodeAttributes>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="OTLP"/>
</root>
</configuration>
4. Java 代码实现
4.1 OtlpLoggingConfig --- 激活 Logback Appender
OpenTelemetryAppender 需要一个已初始化的 OpenTelemetry 实例才能工作。Spring Boot 自动装配了 OpenTelemetry Bean,但 Appender 默认无法获取。此配置类在启动时完成桥接:
java
@Component
public class OtlpLoggingConfig implements InitializingBean {
private final OpenTelemetry openTelemetry;
public OtlpLoggingConfig(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
@Override
public void afterPropertiesSet() {
// 将 Spring Boot 自动装配的 OpenTelemetry 实例
// 注入到 Logback 的 OpenTelemetryAppender 中
OpenTelemetryAppender.install(openTelemetry);
}
}
如果不做这一步,OpenTelemetryAppender 会使用默认的空 OpenTelemetry 实例,日志不会真正被导出。
4.2 自定义 ObservationConvention --- 富化 Span 内容
默认的 DefaultChatModelObservationConvention 只记录参数(temperature、maxTokens 等),不记录 Prompt 和回答的文本内容。通过继承它可以添加业务关心的数据。
4.2.1 ChatModel 观测约定
java
public class ContentEnrichedChatModelObservationConvention
extends DefaultChatModelObservationConvention {
private static final int MAX_CONTENT_LENGTH = 1024;
@Override
public KeyValues getHighCardinalityKeyValues(ChatModelObservationContext context) {
// ① 先拿到父类所有的默认 KeyValues(temperature、token 用量等)
KeyValues keyValues = super.getHighCardinalityKeyValues(context);
// ② 追加 Prompt 内容(截断以防止 Span 过大)
keyValues = appendPromptContent(keyValues, context);
// ③ 追加模型回答内容
keyValues = appendCompletionContent(keyValues, context);
return keyValues;
}
private KeyValues appendPromptContent(KeyValues keyValues,
ChatModelObservationContext context) {
String content = context.getRequest().getInstructions()
.stream()
.map(Content::getText)
.collect(Collectors.joining("\n"));
return keyValues.and("gen_ai.prompt.content",
truncate(content, MAX_CONTENT_LENGTH));
}
private KeyValues appendCompletionContent(KeyValues keyValues,
ChatModelObservationContext context) {
if (context.getResponse() == null) {
return keyValues;
}
String content = context.getResponse().getResults()
.stream()
.map(g -> g.getOutput().getText())
.collect(Collectors.joining("\n"));
return keyValues.and("gen_ai.completion.content",
truncate(content, MAX_CONTENT_LENGTH));
}
private String truncate(String text, int maxLen) {
if (text == null) return "";
return text.length() <= maxLen ? text
: text.substring(0, maxLen) + "...[truncated]";
}
}
4.2.2 ChatClient 观测约定
ChatClient 有自己独立的 ObservationContext,需单独扩展:
java
public class ContentEnrichedChatClientObservationConvention
extends DefaultChatClientObservationConvention {
private static final int MAX_CONTENT_LENGTH = 1024;
@Override
public KeyValues getHighCardinalityKeyValues(ChatClientObservationContext context) {
KeyValues keyValues = super.getHighCardinalityKeyValues(context);
keyValues = appendPromptContent(keyValues, context);
keyValues = appendCompletionContent(keyValues, context);
return keyValues;
}
// ... 实现逻辑与 ChatModel 版本相同,操作 ChatClientObservationContext
}
4.2.3 注册自定义约定
java
@Configuration
public class ChatClientConfig {
// 替换默认 ChatModel 观测约定
@Bean
public ChatModelObservationConvention chatModelObservationConvention() {
return new ContentEnrichedChatModelObservationConvention();
}
// 替换默认 ChatClient 观测约定
@Bean
public ChatClientObservationConvention chatClientObservationConvention() {
return new ContentEnrichedChatClientObservationConvention();
}
}
4.3 ChatController --- 手动创建 Observation 示例
除了框架自动创建的 ChatModel / ChatClient Span,业务代码也可以手动创建自定义 Observation:
java
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatClient chatClient;
private final ObservationRegistry observationRegistry;
public ChatController(ChatClient chatClient,
ObservationRegistry observationRegistry) {
this.chatClient = chatClient;
this.observationRegistry = observationRegistry;
}
@GetMapping
public String chat(@RequestParam String message) {
// 框架自动创建 ChatClient Span(透明)
return chatClient.prompt()
.user(message)
.call()
.content();
}
public void doSomething() {
// 手动创建自定义 Span
Observation observation = Observation.createNotStarted(
"custom.operation", this.observationRegistry);
observation.lowCardinalityKeyValue("locale", "en-US");
observation.highCardinalityKeyValue("userId", "42");
observation.observe(() -> {
// 此代码块的执行耗时和状态会被记录
performBusinessLogic();
});
}
}
5. 数据流转路径
5.1 整体数据流
┌──────────────────────────────────────────────────┐
│ Spring AI Application │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ ChatModel │ │ Custom │ │
│ │ Observation │ │ Observation │ │
│ │ (自动创建) │ │ (手动创建) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ ┌──────▼─────────────────▼───────┐ │
│ │ Micrometer Observation │ │
│ │ (统一 API,不感知后端) │ │
│ └──────────────┬─────────────────┘ │
│ │ │
│ ┌──────────────▼─────────────────┐ │
│ │ micrometer-tracing-bridge-otel │ │
│ │ (Micrometer → OTel 格式转换) │ │
│ └──────────────┬─────────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────────┐ │
│ │ OTLP Trace │ │ Logback OTLP │ │
│ │ Exporter │ │ Appender │ │
│ │ (gRPC) │ │ (gRPC) │ │
│ └──────┬──────┘ └──────┬──────────┘ │
└─────────┼───────────────┼────────────────────────┘
│ │
┌─────▼───────────────▼─────┐
│ SkyWalking OAP │
│ (192.168.1.235:4319) │
│ │
│ ┌─────────────────────┐ │
│ │ Traces + Logs 关联 │ │
│ └─────────────────────┘ │
└───────────────────────────┘
5.2 关键技术点
| 环节 | 组件 | 作用 |
|---|---|---|
| 埋点 | Micrometer Observation |
统一 API,ChatModel/ChatClient 自动埋点 |
| 格式转换 | micrometer-tracing-bridge-otel |
Micrometer Span → OTel Span |
| Trace 导出 | opentelemetry-exporter-otlp |
OTLP gRPC → SkyWalking OAP |
| 日志导出 | OpenTelemetryAppender |
Logback 日志 → OTLP gRPC → SkyWalking OAP |
| 日志关联 | %X{traceId} / %X{spanId} |
MDC 自动注入当前 Span 信息 |
| 内容富化 | 自定义 ObservationConvention |
添加 gen_ai.prompt.content 等业务 Tag |
6. 接入步骤总结
6.1 最小接入清单
| 步骤 | 文件 | 操作 |
|---|---|---|
| 1 | pom.xml |
添加 6 个依赖 |
| 2 | application.yml |
配置 otel + management.otlp + spring.ai.chat.observations |
| 3 | logback-spring.xml |
添加 OpenTelemetryAppender + traceId/spanId 格式 |
| 4 | OtlpLoggingConfig.java |
OpenTelemetryAppender.install(openTelemetry) |
| 5 | 自定义 ObservationConvention |
继承默认实现,添加业务 Key(可选) |
6.2 验证方法
bash
# 1. 启动应用
mvn spring-boot:run -pl ai-chat-demo
# 2. 请求接口,产生 Span
curl "http://localhost:8080/chat?message=hello"
# 3. 检查控制台日志是否包含 traceId
# 输出示例:
# 2026-06-08 10:30:00.123 [http-nio-8080-exec-1] [abc123][def456] INFO ... - ...
# 4. 在 SkyWalking UI 中查看
# - 链路追踪:查看 gen_ai.client.operation Span
# - 日志关联:点击 Span 可见关联的日志内容
# - 自定义 Tag:gen_ai.prompt.content / gen_ai.completion.content
AI 相关指标(SkyWalking 从链路数据中主动解析的):

日志:

链路需要通过 http://192.168.1.1:8080/zipkin 查询:

6.3 生产环境调优建议
| 配置项 | 开发环境 | 生产环境 |
|---|---|---|
sampling.probability |
1.0 |
0.1 ~ 0.3 |
MAX_CONTENT_LENGTH |
1024 |
256 ~ 512 |
| OTel 日志级别 | TRACE / DEBUG |
WARN |
log-completion / log-prompt |
true |
false(避免日志过大) |
7. 关键设计模式
7.1 埋点与后端解耦
业务代码 → Micrometer Observation API (稳定)
│
├── Bridge A → OTel → SkyWalking
├── Bridge B → OTel → Jaeger
└── Bridge C → Brave → Zipkin
应用层代码只依赖 Micrometer Observation API,后端存储(SkyWalking / Jaeger / Zipkin)通过更换 Bridge 即可切换,无需改动业务代码。
7.2 Convention 扩展模式
DefaultChatModelObservationConvention
│ 提供 17 个默认 KeyValue(参数 + 用量)
│
└── ContentEnrichedChatModelObservationConvention
│ 追加 gen_ai.prompt.content
│ 追加 gen_ai.completion.content
通过继承而非修改来扩展,调用 super.getHighCardinalityKeyValues() 保留框架默认行为,仅追加自定义数据。
7.3 日志与 Trace 的关联
不是通过配置文件中的固定规则关联,而是通过 MDC 的实时注入:
请求到达 → Micrometer 创建 Span → traceId/spanId 写入 MDC
│
Logback %X{traceId:-} 从 MDC 读取
│
日志携带 traceId → OTLP Appender 导出
│
SkyWalking 按 traceId 自动关联日志与 Span
这一切是自动的 ------开发者不需要在代码中手动传递 traceId。