OpenTelemetry 入门与实战:Java Agent、Spring Boot Starter与LLM调用追踪示例

OpenTelemetry 入门与实战:Java Agent、Spring Boot Starter与LLM调用追踪示例

本文讲解 OpenTelemetry 的核心概念、架构与 Java 集成方式,涵盖 Java Agent 零代码埋点、Spring Boot Starter 细粒度控制、手动埋点及 GenAI 语义约定,并提供一个可运行的 Spring Boot + OpenTelemetry 示例。

前置知识

  • Java/Spring Boot 基础
  • 了解可观测性基本概念(日志、指标、追踪)
  • Maven 或 Gradle 基础

一、为什么需要 OpenTelemetry

1.1 一个真实的排障困境

假设你的 AI 应用上线后,用户反馈"回答有时候特别慢"。你打开日志,看到大量 INFO 级别的请求记录,但看不出哪一步慢了。你怀疑是 LLM 调用慢,但日志里没有记录每次 LLM 调用的耗时;你怀疑是检索环节慢,但检索服务的日志和主应用的日志分散在不同文件里,无法关联。

类比:没有 OpenTelemetry 的系统就像一个没有仪表盘的汽车。 你只知道车在跑(服务正常运行),但不知道发动机温度、油耗、当前速度。出问题时只能凭经验猜测。

OpenTelemetry 的目标就是给系统装上"仪表盘"------统一采集请求的完整链路(追踪)、关键数值指标(指标)和结构化日志(日志),让你在任何维度上都能快速定位问题。

1.2 可观测性的三个核心信号

OpenTelemetry 处理三种遥测数据(Signals):

信号 回答的问题 类比
Traces(追踪) 请求经过了哪些服务?哪一步慢了? 快递物流轨迹
Metrics(指标) 系统的整体健康趋势如何? 汽车仪表盘
Logs(日志) 某一时刻具体发生了什么? 行车记录仪

它们的核心区别:

  • Metrics 告诉你"发生了改变"------比如错误率从 1% 升到了 5%
  • Traces 告诉你"问题出在哪里"------是检索服务慢了,还是 LLM 调用超时了
  • Logs 告诉你"为什么发生"------具体是哪个参数导致了异常

三者结合,才能提供完整的可观测性视野。

1.3 Trace 与 Span 的关系

Trace(追踪) 是一次完整请求的端到端记录。Span(跨度) 是 Trace 中的单个操作单元,包含开始时间、结束时间、标签、事件和状态。

类比:Trace 是一次旅行,Span 是旅行中的每一段行程。 从北京到上海的一次旅行(Trace),包含"打车到机场"、"安检"、"飞行"、"打车到酒店"四段行程(Span)。如果旅行总时间超预期,查看哪段行程耗时最长就知道了。

复制代码
Trace: 用户请求 GET /api/chat
├── Span: HTTP 接收 (12ms)
├── Span: 查询预处理 (85ms)
├── Span: 向量检索 (320ms)          ← 耗时最长
├── Span: LLM 调用 (1500ms)
│   ├── Span: Token 计算 (5ms)
│   └── Span: API 请求 (1495ms)
└── Span: 响应返回 (8ms)

1.4 OpenTelemetry 的三大组件

OpenTelemetry 由 CNCF 孵化,是 OpenTracing 和 OpenCensus 合并后的产物,已成为云原生可观测性领域的工业级标准。其架构包含三个部分:

Instrumentation(埋点) :负责在应用中生成遥测数据。支持三种方式------API(抽象接口)、SDK(具体实现)、零代码(Java Agent 字节码注入)。

OTLP(OpenTelemetry Protocol) :统一的遥测数据传输协议,支持 gRPC 和 HTTP,可对接 Prometheus、Jaeger、Zipkin 等后端。

Collector(采集器) :厂商无关的服务,用于接收、处理和导出遥测数据。它通过管道(Pipeline) 组织数据处理流程,每个管道包含接收器(Receiver)、处理器(Processor)和导出器(Exporter)。

复制代码
应用 → OTLP → Collector → 后端(Jaeger / Prometheus / Grafana Tempo)
         ↑        ↑
      Java Agent  管道:Receiver → Processor → Exporter

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、Java 集成三种方式

将 OpenTelemetry 集成到 Spring Boot 应用有三种主流方式,各有适用场景。

2.1 方式对比

方式 适用版本 代码改动 GraalVM Native 推荐场景
Java Agent 任意 无 不支持 快速上手、零侵入
OTel Spring Boot Starter 2.6+ / 3.x 少量 支持 需要细粒度控制
Spring Boot 4 Starter 4.x 极少 部分支持 已升级到 Spring Boot 4

Java Agent 是大多数 JVM 应用的默认选择------启动时附加即可获得追踪、指标和日志,无需修改代码。OTel Starter 适合需要 GraalVM 原生镜像支持或细粒度控制的场景。

2.2 Java Agent 快速上手

下载 Agent JAR:

bash 复制代码
curl -L -O https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

配置环境变量并启动:

bash 复制代码
export OTEL_SERVICE_NAME=spring-app
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip

java -javaagent:opentelemetry-javaagent.jar -jar your-spring-app.jar

Agent 通过字节码操作自动埋点 150+ 库------Spring MVC、WebFlux、JDBC、JPA、Kafka、gRPC、HTTP 客户端------无需修改源代码。

2.3 OTel Spring Boot Starter

在 pom.xml 中添加依赖:

xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.opentelemetry.instrumentation</groupId>
            <artifactId>opentelemetry-instrumentation-bom</artifactId>
            <version>2.14.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>

在 application.yml 中配置:

yaml 复制代码
otel:
  service:
    name: spring-app
  exporter:
    otlp:
      endpoint: http://localhost:4317
      protocol: grpc
  traces:
    exporter: otlp
  metrics:
    exporter: otlp
  logs:
    exporter: otlp

三、完整实战:Spring Boot + OpenTelemetry

3.1 场景说明

构建一个简单的"骰子滚动"服务,模拟 AI 应用中"请求接收 → 业务处理 → LLM 调用"的流程,演示 OpenTelemetry 的自动埋点和手动埋点。

3.2 项目依赖

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.0</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry.instrumentation</groupId>
        <artifactId>opentelemetry-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry.instrumentation</groupId>
        <artifactId>opentelemetry-instrumentation-annotations</artifactId>
    </dependency>
</project>

3.3 主应用与控制器

java 复制代码
@SpringBootApplication
public class DiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(DiceApplication.class, args);
    }
}
java 复制代码
@RestController
public class RollController {

    private static final Logger logger = LoggerFactory.getLogger(RollController.class);

    private final OpenTelemetry openTelemetry;
    private final Tracer tracer;

    public RollController(OpenTelemetry openTelemetry) {
        this.openTelemetry = openTelemetry;
        this.tracer = openTelemetry.getTracer("dice-tracer");
    }

    @GetMapping("/rolldice")
    public String index(@RequestParam("player") Optional<String> player) {
        // 手动创建 Span 记录业务逻辑
        Span span = tracer.spanBuilder("roll-dice-operation")
            .setAttribute("player.name", player.orElse("anonymous"))
            .startSpan();

        try (Scope scope = span.makeCurrent()) {
            int result = getRandomNumber(1, 6);

            // 记录 Span 事件(带业务上下文)
            span.addEvent("dice-rolled", Attributes.of(
                AttributeKey.longKey("result"), (long) result
            ));

            logger.info("Player {} rolled: {}", player.orElse("anonymous"), result);
            return Integer.toString(result);

        } catch (Exception e) {
            span.setStatus(StatusCode.ERROR, e.getMessage());
            span.recordException(e);
            throw e;
        } finally {
            span.end();
        }
    }

    private int getRandomNumber(int min, int max) {
        return ThreadLocalRandom.current().nextInt(min, max + 1);
    }
}

3.4 使用 @WithSpan 注解简化埋点

在方法上添加 @WithSpan 注解,Agent 会自动创建 Span:

java 复制代码
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;

@Service
public class DiceService {

    @WithSpan("generate-random")
    public int roll(@SpanAttribute("max.value") int max) {
        return ThreadLocalRandom.current().nextInt(1, max + 1);
    }
}

注意 :@WithSpan 使用 Spring AOP 代理,只对外部方法调用生效,同类内部调用不会触发埋点。

3.5 启动与验证

bash 复制代码
# 启动应用(Java Agent 方式)
java -javaagent:opentelemetry-javaagent.jar \
     -Dotel.service.name=dice-service \
     -Dotel.traces.exporter=logging \
     -jar target/dice-app.jar

# 访问端点
curl http://localhost:8080/rolldice?player=Alice

将 otel.traces.exporter 设为 logging 时,追踪数据会输出到控制台,方便快速验证埋点是否生效。验证通过后改为 otlp 导出到后端。


四、LLM 应用的可观测性:GenAI 语义约定

4.1 为什么 LLM 应用需要特殊处理

传统 APM 追踪确定性代码路径,而 LLM 系统打破了这一假设:

  • 非确定性:相同的 Prompt 产生不同的输出,无法仅凭请求日志复现问题
  • Token 成本模型:延迟和成本与 Token 数相关,而非请求数------单个"慢"请求可能消耗 10 倍预算
  • 多步骤 Agent 链:一个请求可能包含 5 次 LLM 调用、3 次工具调用和 2 次向量库查询

4.2 GenAI 语义约定核心字段

OpenTelemetry GenAI 特别兴趣小组定义了标准的 gen_ai.* 属性名:

属性名 类型 说明 示例
gen_ai.system string LLM 提供商 openai, anthropic
gen_ai.operation.name string 操作类型 chat, embeddings
gen_ai.request.model string 请求的模型 gpt-4o
gen_ai.usage.input_tokens int 输入 Token 数 512
gen_ai.usage.output_tokens int 输出 Token 数 128

使用标准字段的价值:无论你调用 OpenAI、Anthropic 还是通义千问,遥测数据使用统一的字段名,后端无需为每个厂商编写自定义解析规则。

4.3 LLM 调用手动埋点示例

java 复制代码
@Service
public class LlmCallService {

    private final Tracer tracer;
    private final ChatClient chatClient;

    public String chat(String userMessage) {
        // 创建 GenAI Span,使用标准语义约定字段
        Span span = tracer.spanBuilder("chat " + "gpt-4o")
            .setAttribute("gen_ai.system", "openai")
            .setAttribute("gen_ai.operation.name", "chat")
            .setAttribute("gen_ai.request.model", "gpt-4o")
            .startSpan();

        try (Scope scope = span.makeCurrent()) {
            long startTime = System.currentTimeMillis();
            String response = chatClient.prompt(userMessage).call().content();
            long latency = System.currentTimeMillis() - startTime;

            // 记录 Token 消耗(从 API 响应中提取)
            span.setAttribute("gen_ai.usage.input_tokens", estimateTokens(userMessage));
            span.setAttribute("gen_ai.usage.output_tokens", estimateTokens(response));
            span.setAttribute("latency.ms", latency);

            return response;
        } catch (Exception e) {
            span.setStatus(StatusCode.ERROR, e.getMessage());
            span.recordException(e);
            throw e;
        } finally {
            span.end();
        }
    }

    private int estimateTokens(String text) {
        return text.length() / 4; // 简化估算
    }
}

五、OpenTelemetry Collector 入门

5.1 Collector 的作用

Collector 是一个可执行文件,接收遥测数据,处理后导出到多个目标。它的核心价值在于解耦------应用只需将数据发送到 Collector,Collector 负责决定数据最终去往哪里(Jaeger、Prometheus 还是云厂商后端)。

5.2 最小 Collector 配置

创建 collector-config.yaml:

yaml 复制代码
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

5.3 Docker 运行 Collector

bash 复制代码
docker run -d --name otel-collector \
  -p 4317:4317 \
  -p 4318:4318 \
  -v $(pwd)/collector-config.yaml:/etc/otelcol/config.yaml \
  otel/opentelemetry-collector:latest

5.4 Collector 管道数据流

复制代码
应用 → OTLP → Receiver(otlp) → Processor(batch) → Exporter(debug/Jaeger/Prometheus)
  • Receiver:监听网络端口接收数据,一个 Receiver 可发送数据到多个管道
  • Processor:处理数据(批处理、过滤、采样),batch 处理器将数据压缩以减少网络开销
  • Exporter:将数据发送到后端,多个 Exporter 可接收同一份数据副本

六、常见问题

Q1:Java Agent 和 OTel Starter 应该选哪个?

A:大多数情况选 Java Agent------零代码改动,支持 150+ 库的自动埋点。只有在需要 GraalVM 原生镜像支持或细粒度控制时才选 OTel Starter。

Q2:Span 和 Log 有什么区别?

A:Span 记录一个操作的开始、结束和上下文,属于 Trace 的一部分。Log 是某个时刻的独立事件,不一定关联到具体请求。Span 内的 Log 可以通过 Trace ID 关联。

Q3:如何查看追踪数据?

A:需要部署一个后端。常用选择:Jaeger(开源,UI 友好)、Grafana Tempo(与 Grafana 生态集成)、Uptrace(SaaS)。将 OTEL_EXPORTER_OTLP_ENDPOINT 指向后端地址即可。

Q4:OpenTelemetry 有性能开销吗?

A:Java Agent 通过字节码增强实现,开销通常低于 5%。使用 Batch Processor 和采样策略(如只采集 10% 的正常请求)可以进一步降低开销。

Q5:LLM 的 Prompt 内容能记录在 Span 属性里吗?

A:不建议。Prompt 可能包含 PII 或敏感业务数据,写入 Span 属性存在合规风险。应该使用 Span Event 记录并配合脱敏,或只记录 Prompt 版本号和哈希值。


参考资源:

博客: https://blog.csdn.net/badao_liumang_qizhi

相关推荐
波力海苔夹心脆6751 小时前
C# 机器视觉实战:单相机引导机械手精准贴合,从拍照、偏差计算(平移+旋转)到坐标补偿全解
开发语言·经验分享·数码相机·c#·视觉检测·.net
m0_587383001 小时前
24 小时自助健身房系统软件开发实战指南与案例解析
java·spring boot·spring·系统架构·需求分析
爱编程的小白L1 小时前
基于SpringBoot+Vue的医院门诊排队叫号系统的设计与实现
spring boot
栖凤1 小时前
Java 的 try-catch 在 Agent 里失效了,我用了 5 个模式才兜住
java·前端·python
小师兄吃牛肉1 小时前
Java 同步系列之 ZooKeeper 分布式锁
java·分布式·java-zookeeper
外收内放1 小时前
Python基础语法练习题(62原始版本及其优化版本)
开发语言·python
马剑威(威哥爱编程)1 小时前
【AI全栈后端12-12】Spring Boot 3.x 到 4.x 迁移实操:Jakarta 11、Jackson 3 与 AI 2.0
java·开发语言·spring boot·后端
Gauss松鼠会1 小时前
【GaussDB】破除gaussdb ugin索引支持中文模糊查询的迷思-字符序
java·运维·服务器·网络·数据库·gaussdb·经验总结
IT研究室1 小时前
最新计算机毕业设计选题推荐-基于spring boot的心桥·心理健康综合服务平台-网站-文档指导-Java-springboot
java·spring boot·课程设计