1. 概述

本文档从零开始,在一个 Spring Boot + Spring AI 项目中实现完整的可观测性体系——将 Tracing(链路追踪)、Metrics(指标)、Logging(日志)通过 OpenTelemetry 协议统一导出到 Apache SkyWalking

实现路径:Micrometer ObservationOTel BridgeOTLP gRPC ExporterSkyWalking OAP

1.1 上报流程

SkyWalking 支持的多种数据上报方式,常用的有:

  • Java Agent:通过 -javaagent:skywalking-agent.jar 实现,零代码、零依赖、自动埋点、性能最好。
  • OpenTelemetry OTLP:云原生标准,可对接不同类型的监控平台。

Spring 官方支持 Micrometer + OTLP 上报方案,完整数据流:

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指标 + 服务监控大盘

流程说明:

  1. SpringAI 应用通过 OTLP 采集大模型调用链路、Token、耗时gen_ai_*指标
  2. 使用 OTLP 协议 (4318) 推送至 OAP
  3. OAP 加载 otel-rules 规则文件解析指标,存入 BanyanDB/ES
  4. BoosterUI 通过 12800 端口请求 OAP 查询数据
  5. 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 依赖清单:

<!-- 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 版本说明:

在这里插入图片描述

# ==================== 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 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 默认无法获取。此配置类在启动时完成桥接:

@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 只记录参数(temperaturemaxTokens 等),不记录 Prompt 和回答的文本内容。通过继承它可以添加业务关心的数据。

4.2.1 ChatModel 观测约定

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,需单独扩展:

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 注册自定义约定

@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

@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 SpanOTel Span
Trace 导出 opentelemetry-exporter-otlp OTLP gRPCSkyWalking OAP
日志导出 OpenTelemetryAppender Logback 日志 → OTLP gRPCSkyWalking 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 验证方法

# 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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐