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 版本号和哈希值。
参考资源: