Spring AI 可观测性实战

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(日志)

  1. **服务埋点层:**框架内部通过 Micrometer(Spring 官方推荐的可观测中间件)对关键调用(如模型推理、工具调用、外部调用)自动埋点;
  2. **采集导出层:**Micrometer 在运行时提供可插拔的 Tracer 实现,支持将埋点层产生的数据使用 OpenTelemetry SDK 导出为满足 OTLP 协议的格式;
  3. **数据存储层:**兼容任何支持 OTLP 协议格式的可观测存储,如 Prometheus、Langfuse、Jaeger 等;
  4. **指标展示层:**通过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 (观测处理器):它是真正的执行者。
  • 常见实现
    1. DefaultTracingObservationHandler :负责把观测转为 Trace/Span(发往 Langfuse)。
    2. 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 发往远端。
    • 最终目的地 :数据到达 LangfuseJaeger

桥接OTEL io.micrometer.tracing.otel.bridge

核心组件

这些组件主要存在于 io.micrometer.tracing.otel.bridge 包下:

  1. OtelTracer
    • 角色:核心适配器。它实现了 Micrometer 的 Tracer 接口,内部持有 OTel 的 io.opentelemetry.api.trace.Tracer。
    • 作用:当你调用 tracer.nextSpan() 时,它负责调用 OTel 的 API 来真正创建一个 Span。
  2. OtelSpan
    • 角色:包装器。实现了 Micrometer 的 Span 接口。
    • 作用:它包裹了一个 OTel 原生的 Span 实例。当你给 Span 打标签(Tag)时,它会转化为 OTel 的 Attributes。
  3. OtelPropagator
    • 角色:传播器。
    • 作用:负责处理 TraceContext 的注入(Inject)和提取(Extract),即将 TraceID 放入 HTTP Header 或从 Header 中解析出来。
  4. OtelCurrentTraceContext
    • 角色:上下文管理器。
    • 作用:负责管理 ThreadLocal,确保在同一个线程中调用 AI 模型时,能获取到同一个 TraceID。
  5. 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 生成一个 TraceIDSpanID
    • 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;
    };
  }
}
相关推荐
qinaoaini2 小时前
Spring 简介
java·后端·spring
坐在地上想成仙2 小时前
AI工具汇总
人工智能
高山上有一只小老虎2 小时前
java中常用的日期方法
java
Java.慈祥2 小时前
速通-微信小程序 5Day
java·微信小程序·小程序·npm
hamish-wu2 小时前
告别idea,拥抱AI开发环境TRAE
java·ide·编辑器·intellij-idea·intellij idea·visual studio
她说..2 小时前
万字详解WebSocket的用法
java·网络·websocket·网络协议·springboot
IT 行者2 小时前
打造你的家庭 AI 助手(四):企业微信 AI 助手接入你的 OpenClaw
人工智能·企业微信
简佐义的博客2 小时前
15万单细胞、19种实体瘤:系统学习血管内皮细胞泛癌的单细胞与空间转录组联合分析思路
人工智能·学习
一只酸奶牛^_^2 小时前
java实现pdf添加水印
java·pdf