Spring AI 可观测性实战
文章目录
- [Spring AI 可观测性实战](#Spring AI 可观测性实战)
-
- 可观测性的架构
- 采集流程io.micrometer
-
- [定义观测点 ------ Observation & Context](#定义观测点 —— Observation & Context)
- [指挥中心 ------ ObservationRegistry](#指挥中心 —— ObservationRegistry)
- [元数据规范 ------ ObservationConvention](#元数据规范 —— ObservationConvention)
- [逻辑处理层 ------ ObservationHandler](#逻辑处理层 —— ObservationHandler)
- [底层转换层 ------ Tracer & MeterRegistry](#底层转换层 —— Tracer & MeterRegistry)
- [数据导出------ Exporter](#数据导出—— Exporter)
- [桥接OTEL io.micrometer.tracing.otel.bridge](#桥接OTEL io.micrometer.tracing.otel.bridge)
-
- 核心组件
- 数据流转生命周期解析
-
- [1. 触发层:Observation 开启](#1. 触发层:Observation 开启)
- [2. 桥接层:Span 创建与关联](#2. 桥接层:Span 创建与关联)
- [3. 加工层:元数据转换 (Attributes Mapping)](#3. 加工层:元数据转换 (Attributes Mapping))
- [4. 结束层:Span 闭环](#4. 结束层:Span 闭环)
- [5. 管道层:Span 导出前处理](#5. 管道层:Span 导出前处理)
- [6. 发送层:转化为 OTLP 协议并导出](#6. 发送层:转化为 OTLP 协议并导出)
- [导出OLTP opentelemetry-exporter-otlp](#导出OLTP opentelemetry-exporter-otlp)
- 可观测代码实战
可观测性的架构
-
Micrometer:它是 Java 生态中的可观测性门面库。它为开发者提供了一套通用的 API。你只需要调用 observation.start(),而不必关心底层的追踪系统是 Zipkin、Jaeger 还是 OpenTelemetry。
-
OpenTelemetry:它提供了一套通用的 API 和 SDK ,让开发者可以用统一的方式在代码里记录 Traces(链路追踪) 、Metrics(指标) 和 Logs(日志)

- **服务埋点层:**框架内部通过 Micrometer(Spring 官方推荐的可观测中间件)对关键调用(如模型推理、工具调用、外部调用)自动埋点;
- **采集导出层:**Micrometer 在运行时提供可插拔的 Tracer 实现,支持将埋点层产生的数据使用 OpenTelemetry SDK 导出为满足 OTLP 协议的格式;
- **数据存储层:**兼容任何支持 OTLP 协议格式的可观测存储,如 Prometheus、Langfuse、Jaeger 等;
- **指标展示层:**通过Grafana、Jaeger、阿里云可观测来可视化的展示指标数据。
采集流程io.micrometer
定义观测点 ------ Observation & Context
这是采集的起点。当 ChatModel.call() 被执行时,系统会创建一个"观测行为"。
- Observation (观测对象):代表一个具体的动作(比如:一次 AI 调用)。它有生命周期:start()(开始计时/追踪)和 stop()(结束并记录)。
- Observation.Context (观测上下文) :这是一个"信息口袋"。在整个 AI 调用过程中,模型名、Prompt、Token 数、错误信息都会被塞进这个口袋。
- 实战关联:ChatModelObservationContext 就是专门存 AI 数据的地方。
指挥中心 ------ ObservationRegistry
这是 Micrometer 的核心枢纽。
- ObservationRegistry (观测注册表):它管理着所有的观测行为。
- 职责 :当一个 Observation 启动时,注册表负责通知所有的"处理器(Handler)"过来干活。
- 实战提示:你在 ChatModel.builder().observationRegistry(registry) 中传入的就是它。
元数据规范 ------ ObservationConvention
数据采集不能乱,必须标准化。
- ObservationConvention (观测公约):它决定了这次采集的名字叫什么,以及带哪些标签(Tags)。
- 职责 :比如它规定了标签必须包含 ai.model 而不是 model_name。
- 实战关联:Spring AI 默认内置了 DefaultChatModelObservationConvention,确保了所有 AI 框架产生的追踪数据在展示端看起来是一致的。
逻辑处理层 ------ ObservationHandler
这是数据开始"分身"的地方。一个观测点可以同时产生监控指标(Metrics)**和**链路追踪(Tracing)。
- ObservationHandler (观测处理器):它是真正的执行者。
- 常见实现 :
- DefaultTracingObservationHandler :负责把观测转为 Trace/Span(发往 Langfuse)。
- DefaultMeterObservationHandler :负责把观测转为 Timer/Counter(发往 Prometheus)。
- 逻辑:它监听 onStart、onStop 等事件。当发现 AI 调用结束时,它从 Context 口袋里拿出 Token 数,记入指标或 Span。
底层转换层 ------ Tracer & MeterRegistry
这是数据进入具体协议的阶段。
- MeterRegistry (指标注册表):专门负责处理数字统计。比如 PrometheusMeterRegistry 把数据转为 Prometheus 格式。
- Tracer (追踪器):通过桥接器(如 micrometer-tracing-bridge-otel)将 Span 包装成 OTLP 格式。
数据导出------ Exporter
这是数据的最后一公里。
- SpanExporter (追踪导出器):比如 OtlpHttpSpanExporter。
- 职责 :将打包好的 OTLP 数据通过 HTTP/gRPC 发往远端。
- 最终目的地 :数据到达 Langfuse 或 Jaeger。
桥接OTEL io.micrometer.tracing.otel.bridge
核心组件
这些组件主要存在于 io.micrometer.tracing.otel.bridge 包下:
- OtelTracer :
- 角色:核心适配器。它实现了 Micrometer 的 Tracer 接口,内部持有 OTel 的 io.opentelemetry.api.trace.Tracer。
- 作用:当你调用 tracer.nextSpan() 时,它负责调用 OTel 的 API 来真正创建一个 Span。
- OtelSpan :
- 角色:包装器。实现了 Micrometer 的 Span 接口。
- 作用:它包裹了一个 OTel 原生的 Span 实例。当你给 Span 打标签(Tag)时,它会转化为 OTel 的 Attributes。
- OtelPropagator :
- 角色:传播器。
- 作用:负责处理 TraceContext 的注入(Inject)和提取(Extract),即将 TraceID 放入 HTTP Header 或从 Header 中解析出来。
- OtelCurrentTraceContext :
- 角色:上下文管理器。
- 作用:负责管理 ThreadLocal,确保在同一个线程中调用 AI 模型时,能获取到同一个 TraceID。
- OtelHandlerContext (与 Observation 配合时) :
- 角色:桥接 Micrometer Observation 与 Tracing。它负责将观察到的元数据映射到 Span 属性中。
数据流转生命周期解析
业务操作 → A P I \xrightarrow{API} API Micrometer Observation → 桥接 \xrightarrow{桥接} 桥接 OtelTracer → 驱动 \xrightarrow{驱动} 驱动 OTel SDK → 协议转换 \xrightarrow{协议转换} 协议转换 OTLP/Protobuf → 网络 \xrightarrow{网络} 网络 Langfuse。
1. 触发层:Observation 开启
- 动作:Spring AI 内部执行 observation.start()。
- 组件:TracingObservationHandler。
- 逻辑:它拦截到观察开始事件,调用桥接器的 OtelTracer 去创建一个新 Span。
2. 桥接层:Span 创建与关联
- 组件:OtelTracer =》 OpenTelemetry SDK
- 逻辑 :
- OtelTracer 调用 OTel 原生 SpanBuilder。
- OTel SDK 生成一个 TraceID 和 SpanID。
- OtelCurrentTraceContext 将这些 ID 绑定到当前线程。
3. 加工层:元数据转换 (Attributes Mapping)
- 动作:你调用 context.addHighCardinalityKeyValue(KeyValue.of("gen_ai.prompt", "..."))。
- 组件:OtelSpan。
- 转换逻辑 :
- Micrometer 调用 OtelSpan.tag(key, value)。
- 桥接器内部执行:otelSpan.setAttribute(AttributeKey.stringKey(key), value)。
- 注意:Micrometer 的 LowCardinality 会变成 Metrics 标签,HighCardinality(如具体的 Prompt 文本)会变成 Tracing 的 Attributes。
4. 结束层:Span 闭环
- 动作:AI 调用结束,observation.stop() 被触发。
- 组件:DefaultTracingObservationHandler。
- 逻辑:它调用 OtelSpan.end()。此时,OTel 的原生 Span 会被标记为 Finished。
5. 管道层:Span 导出前处理
- 组件:io.opentelemetry.sdk.trace.SdkSpanProvider。
- 逻辑 :Span 结束后,会经过 SpanProcessor 。
- 如果你配置了 BatchSpanProcessor,它会先把 Span 存入内存缓冲区,凑够一批再发送,以提高性能。
6. 发送层:转化为 OTLP 协议并导出
- 组件:OtlpHttpSpanExporter (位于 opentelemetry-exporter-otlp 依赖中)。
- 逻辑 :
- 它是真正的"出口"。
- 它将 FinishedSpan 对象转化为 Protobuf 格式(即 OTLP 协议规定的格式)。
- 根据你配置的 management.otlp.tracing.endpoint,通过 HTTP POST 请求发送到 Langfuse 的 /api/public/otel/v1/traces。
导出OLTP opentelemetry-exporter-otlp
它的核心作用是:将内存中已经格式化好的 Traces(链路)、Metrics(指标)数据,通过标准的 OTLP (在实现 Spring AI 的可观测性链路中,`opentelemetry-OpenTelemetry Protocol) 协议发送到远程后端(如 Langfuse、Jaeger、Prometheus)。
可观测代码实战
必要的依赖
java
<!-- 可观测相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer 到 OpenTelemetry 的桥接 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<!-- OTLP 导出器(将数据发往 Langfuse) -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
以上依赖彼此之间的关系如下图所示,实线为直接依赖,虚线为间接依赖:

yml配置
yaml
spring:
ai:
chat:
client:
observations:
# default value is false.
log-prompt: true
observations:
# 显式开启 Spring AI 的观测记录--》没有效果
include-input: true
include-output: true
include-error-logging: true
# 启用 prompt 内容记录(输入)
log-prompt: true
# 启用 completion 内容记录(输出)
log-completion: true
management:
tracing:
sampling:
probability: 1.0 # 采样率,生产环境可调低
observations:
annotations:
enabled: true
otlp:
tracing:
# Langfuse 的 OTLP 端点(通常是其 Web 地址加路径)
# 注意:自托管或云端地址参考 Langfuse 文档
endpoint: "https://us.cloud.langfuse.com/api/public/otel/v1/traces"
headers:
# Langfuse 要求通过 Authorization header 传输:Basic base64(publicKey:secretKey)
Authorization: "Basic Test"
otel:
service:
name: "data-agent-project"
采坑记录
Langfuse中UI界面input和output不展示
-
原因:Langfuse 的 UI 界面在解析 OpenTelemetry (OTel) 数据时,是根据**特定的属性键(Attribute Keys)*来自动填充"Input"和"Output"窗口的。目前 spring-ai-alibaba 使用的键名(如 gen_ai.content.prompt)与 Langfuse 默认期待的键名存在*路径不匹配。
-
解决方案
java
@Configuration
public class AiObservationConfig {
@Bean
public ObservationFilter forcedContentObservationFilter() {
return (context) -> {
if (context instanceof ChatModelObservationContext chatContext) {
// 2. 补救措施:如果上面的开关没能让 OTel 记录,我们手动注入 OTel 语义要求的 Key
// Langfuse 识别 OTel 的标准 Key 是 gen_ai.content.prompt 和 gen_ai.content.completion
if (chatContext.getRequest() != null && chatContext.getRequest().getInstructions() != null) {
String prompt = chatContext.getRequest().getInstructions().stream()
.map(m -> m.getMessageType() + ": " + m.getText())
.collect(Collectors.joining("\n"));
// 注入输入
context.addHighCardinalityKeyValue(KeyValue.of("gen_ai.prompt", prompt));
}
if (chatContext.getResponse() != null && chatContext.getResponse().getResult() != null) {
String completion = chatContext.getResponse().getResult().getOutput().getText();
// 注入输出
context.addHighCardinalityKeyValue(KeyValue.of("gen_ai.completion", completion));
}
}
return context;
};
}
}