1. 概述

在分布式系统中,服务请求时偶尔出现错误是常态。一个集中的可观测性平台通过捕获应用追踪/日志,并提供查询特定请求的接口来解决这个问题。OpenTelemetry 则帮助标准化遥测数据的捕获和导出过程。

本教程将学习如何使用 Micrometer 门面模式将 Spring Boot 应用与 OpenTelemetry 集成。同时,我们将运行 OpenTelemetry 服务捕获应用追踪数据,并将其发送到中央系统监控请求。

首先,理解几个基础概念。

2. OpenTelemetry 简介

OpenTelemetry (Otel) 是一套标准化的、厂商无关的工具、API 和 SDK 集合。它是 CNCF 孵化项目,由 OpenTracing 和 OpenCensus 项目合并而成。

OpenTracing 是厂商中立的 API,用于将遥测数据发送到可观测性后端。OpenCensus 项目提供一组语言特定库,开发者可用其检测代码并发送到任何支持的后端。Otel 使用与前身项目相同的 tracespan 概念表示微服务间的请求流。

OpenTelemetry 允许我们检测、生成和收集遥测数据,帮助分析应用行为或性能。遥测数据包括日志、指标和追踪。我们可以自动或手动检测 HTTP、数据库调用等代码。

Spring Boot 3 通过 Micrometer Tracing 支持 OpenTelemetry——这是一个一致、可插拔、厂商中立的 API。

⚠️ 注意:早期的 Spring Cloud Sleuth 框架在 Spring Boot 3 中已弃用,其追踪功能已迁移到 Micrometer Tracing。

下面通过示例深入探讨。

3. 示例应用

假设需要构建两个微服务,其中一个服务与另一个交互。
为检测应用的遥测数据,我们将应用与 Micrometer tracing 和 OpenTelemetry 导出器库集成。

3.1. Maven 依赖

micrometer-tracingmicrometer-tracing-bridge-otelopentelemetry-exporter-otlp 依赖可自动捕获追踪数据并导出到任何支持的收集器。

首先创建 Spring Boot 3 Web 项目,在两个应用中添加以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.4.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>3.4.4</version>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing</artifactId>
    <version>1.4.4</version>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
    <version>1.4.4</version>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
    <version>1.39.0</version>
</dependency>

接下来实现下游服务。

3.2. 实现下游应用

下游应用将提供一个返回 Price 数据的接口。

首先定义 Price 类:

public class Price {
    private long productId;
    private double priceAmount;
    private double discount;
}

然后实现 PriceController 的获取价格接口:

@RestController(value = "/price")
public class PriceController {

    private static final Logger LOGGER = LoggerFactory.getLogger(PriceController.class);

    @Autowired
    private PriceRepository priceRepository;

    @GetMapping(path = "/{id}")
    public Price getPrice(@PathVariable("id") long productId) {
        LOGGER.info("Getting Price details for Product Id {}", productId);
        return priceRepository.getPrice(productId);
    }
}

PriceRepository 中实现 getPrice() 方法:

public Price getPrice(Long productId){
    LOGGER.info("Getting Price from Price Repo With Product Id {}", productId);
    if (!priceMap.containsKey(productId)){
        LOGGER.error("Price Not Found for Product Id {}", productId);
        throw new PriceNotFoundException("Price Not Found");
    }
    return priceMap.get(productId);
}

上述代码在产品未找到时返回价格或抛出异常。

3.3. 实现上游应用

上游应用将提供获取 Product 详情的接口,并调用上述价格接口。

首先定义 Product 类:

public class Product {
    private long id;
    private String name;
    private Price price;
}

然后实现 ProductController 的获取产品接口:

@RestController
public class ProductController {

    private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class);

    @Autowired
    private PriceClient priceClient;

    @Autowired
    private ProductRepository productRepository;

    @GetMapping(path = "/product/{id}")
    public Product getProductDetails(@PathVariable("id") long productId){
        LOGGER.info("Getting Product and Price Details with Product Id {}", productId);
        Product product = productRepository.getProduct(productId);
        product.setPrice(priceClient.getPrice(productId));
        return product;
    }
}

ProductRepository 类中实现 getProduct() 方法:

public Product getProduct(Long productId){
    LOGGER.info("Getting Product from Product Repo With Product Id {}", productId);
    if (!productMap.containsKey(productId)){
        LOGGER.error("Product Not Found for Product Id {}", productId);
        throw new ProductNotFoundException("Product Not Found");
    }
    return productMap.get(productId);
}

关键点:需使用 RestTemplateBuilder 显式定义 RestTemplate Bean(Spring Boot 3 要求):

@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder.build();
}

最后在 PriceClient 类中实现 getPrice() 方法:

public Price getPrice(@PathVariable("id") long productId){
    LOGGER.info("Fetching Price Details With Product Id {}", productId);
    String url = String.format("%s/price/%d", baseUrl, productId);
    ResponseEntity<Price> price = restTemplate.getForEntity(url, Price.class);
    return price.getBody();
}

上述代码调用下游服务获取价格。

4. 配置 Spring Boot 与 OpenTelemetry

OpenTelemetry 提供名为 Otel 收集器的组件,用于处理遥测数据并导出到 Jaeger、Prometheus 等可观测性后端。

可通过 Spring 管理配置将追踪数据导出到任何 OpenTelemetry 收集器。

4.1. 配置 Spring

需配置 management.tracing.sampling.probabilitymanagement.otlp.tracing.endpoint 属性导出追踪数据。

application.yml 中添加管理配置:

management:
  tracing:
    sampling:
      probability: '1.0'
  otlp:
    tracing:
      endpoint: http://collector:4318/v1/traces

基于概率的追踪采样属性定义了 span 的采样比例。值 1.0 表示导出所有 span。

5. 运行应用

现在配置并运行整个环境:应用和 Otel 收集器服务(如 Jaeger)。

5.1. 配置应用 Dockerfile

为 Product 服务实现 Dockerfile

FROM openjdk:17-alpine
COPY target/spring-cloud-open-telemetry1-1.0.0-SNAPSHOT.jar spring-cloud-open-telemetry.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/spring-cloud-open-telemetry.jar"]

Price 服务的 Dockerfile 基本相同。

5.2. 使用 Docker Compose 配置服务

配置 docker-compose.yml

services:
  product-service:
    build: spring-boot-open-telemetry1/
    ports:
      - "8080:8080"

  price-service:
    build: spring-boot-open-telemetry2/
    ports:
      - "8081"

  collector:
    image: jaegertracing/jaeger:2.5.0
    ports:
      - "4318:4318"
      - "16686:16686"

上述 collector 服务使用 jaegertracing/jaeger v2 一体化镜像。它内置支持 OpenTelemetry,无需单独运行 OpenTelemetry 服务。

整体架构如下:

Spring Services Architecture With OpenTelemetry Setup

架构图中,API 服务将 span 导出到 Jaeger v2 服务。Jaeger v2 内部包含三个主要组件:收集器/接收器、存储和 UI 服务。

⚠️ 生产建议:生产环境应独立运行 OpenTelemetry 收集器、存储和查询/UI 服务,确保职责分离。

通过 docker-compose 启动服务:

$ docker-compose up

验证运行中的 Docker 服务。

5.3. 验证运行中的 Docker 服务

product-serviceprice-service 外,我们添加了 Jaeger 服务。

上述服务使用 HTTP 端口 4318 向 Jaeger 收集器发送追踪数据。

使用 docker container 命令验证服务状态:

$ docker container ls --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"

输出类似:

125c47300f69   spring-boot-open-telemetry-product-service-1   Up 19 seconds   0.0.0.0:8080->8080/tcp
5e8477630211   spring-boot-open-telemetry-price-service-1     Up 19 seconds   0.0.0.0:49775->8081/tcp
6ace8520779a   spring-boot-open-telemetry-collector-1         Up 19 seconds   4317-4318/tcp, 5778-5779/tcp, 9411/tcp, 13132-13133/tcp, 14250/tcp, 14268/tcp, 0.0.0.0:16686->16686/tcp

确认所有服务正在运行。

6. 在收集器中监控追踪

OpenTelemetry 收集器工具(如 Jaeger)提供前端应用监控请求。可实时或后续查看请求追踪。

监控成功和失败请求的追踪。

6.1. 监控成功请求的追踪

调用 Product 接口 http://localhost:8080/product/100001,日志输出:

product-service-1  | 2025-04-08T04:21:08.372Z  INFO 1 --- [product-service] [nio-8080-exec-1] [ca9845ffc9130c579d41f2f2ef61874a-ccb2d4cd80180fe9] c.b.o.repository.ProductRepository       : Getting Product from Product Repo With Product Id 100001
product-service-1  | 2025-04-08T04:21:08.373Z  INFO 1 --- [product-service] [nio-8080-exec-1] [ca9845ffc9130c579d41f2f2ef61874a-ccb2d4cd80180fe9] c.b.o.api.client.PriceClient             : Fetching Price Details With Product Id 100001
price-service-1    | 2025-04-08T04:21:08.731Z  INFO 1 --- [price-service] [nio-8081-exec-1] [ca9845ffc9130c579d41f2f2ef61874a-60bf6b4856b145f6] c.b.o.controller.PriceController         : Getting Price details for Product Id 100001

Micrometer 自动配置 ProductServicetrace 和 span id 附加到当前线程上下文,并作为 HTTP 头传递给下游 API 调用。PriceService 也会在线程上下文和日志中包含相同的 trace id。Jaeger 收集器服务使用此 trace id 确定跨服务的请求流。

如预期,trace id ....f61874aPriceServiceProductService 日志中一致。

在端口 16686 的 Jaeger UI 中查看请求时间线:

Jaegar UI Success Trace

图中显示请求流时间线及元数据。

6.2. 监控失败请求的追踪

模拟下游服务抛出异常导致请求失败的场景。

使用同一 UI 分析根本原因。

调用产品接口 /product/100005(下游应用中不存在该产品)。

查看失败请求的 span:

Error-Trace

可追溯错误发生的最终 API 调用位置。

7. 结论

本文学习了 OpenTelemetry 如何标准化微服务可观测性模式。
通过示例展示了如何使用 Micrometer tracing 门面将 Spring Boot 3 应用与 OpenTelemetry 集成。最后在 Jaeger UI 服务中追踪了 API 请求流。


原始标题:OpenTelemetry Setup in Spring Boot Application | Baeldung