在 Spring Boot 中配置 OpenTelemetry
1、概览
在本教程中,我们将学习如何在 Spring Boot 应用中整合 OpenTelemetry
。以及如何配置 OpenTelemetry
来追踪请求链路,并将其发送到中央系统以监控请求。
2、OpenTelemetry 简介
OpenTelemetry(Otel)是一组标准化的工具(与供应商无关)、API 和 SDK。它是一个 CNCF 孵化项目,是 OpenTracing 和 OpenCensus 项目的合并产物。
OpenTracing 用于向可观测性后端发送 telemetry(遥测)数据。OpenCensus 项目提供了一套特定于语言的库,开发人员可以用它来检测自己的代码,并将其发送到任何受支持的后端。Otel 使用与其前身项目相同的 trace 和 span 概念来表示微服务间的请求链路。
OpenTelemetry 允许我们检测、生成和收集 telemetry 数据,这有助于分析应用程序的行为或性能。telemetry 数据包括日志、指标和链路跟踪。
使用 Otel SDK,我们可以轻松地覆盖或添加更多属性到链路跟踪中。
让我们通过一个例子来深入了解一下。
3、示例应用
假设我们需要构建两个微服务,其中一个服务与另一个服务交互。
3.1、Maven 依赖
首先,我们将创建 Spring Boot Web 项目,并在两个应用中包含以下 Spring 和 OpenTelemetry
依赖项:
spring-cloud-starter-sleuth
、spring-cloud-sleuth-otel-autoconfigure
和 opentelemetry-exporter-otlp
将自动捕获链路跟踪并导出到任何支持的 collector。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-brave</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-otel-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>1.23.1</version>
</dependency>
注意,我们排除了 Spring Cloud Brave 依赖,以替换默认的追踪实现为 OpenTelemetry。
此外,我们需要包含 Spring Dependency Management BOM 来管理 Spring Cloud Sleuth 的依赖版本。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-otel-dependencies</artifactId>
<version>1.1.2</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
3.2、实现下游应用
我们的下游应用有一个端点来返回价格数据。
首先,为 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);
}
最后,在 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 collector 的 collector,用于处理 telemetry 数据并将其导出到任何可观察性后端,如 Jaeger
、Prometheus
等。
使用一些简单 Spring Sleuth 配置,就能将链路追踪导出到 Otel collector。
4.1、配置 Spring Sleuth
在 application.properties
中加入 Spring Sleuth 配置:
spring.sleuth.otel.config.trace-id-ratio-based=1.0
spring.sleuth.otel.exporter.otlp.endpoint=http://collector:4317
trace-id-ratio-based
属性定义了收集 span 的采样率。数值 1.0
表示将导出所有 span。
4.2、配置 OpenTelemetry Collector
Otel collector 是 OpenTelemetry
链路追踪的引擎。它由 receiver、processor 和 exporter 组件组成。还有一个可选的扩展组件,可帮助进行健康检查、服务发现或数据转发。扩展组件不涉及 telemetry 数据的处理。
为了快速启动 Otel 服务,我们将使用监听 14250
端口的 Jaeger
后端端点。
在 otel-config.yml
中配置 Otel pipeline stage:
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
exporters:
logging:
loglevel: debug
jaeger:
endpoint: jaeger-service:14250
tls:
insecure: true
service:
pipelines:
traces:
receivers: [ otlp ]
processors: [ batch ]
exporters: [ logging, jaeger ]
注意,上述 rocessors
配置为可选配置,默认情况下不启用。processors batch
选项有助于更好地压缩数据,减少传输数据所需的外发连接数。
此外,receiver
配置了 GRPC
和 HTTP
协议。
5、运行应用
现在,我们将配置并运行整个设置、应用程序和 Otel collector。
5.1、在应用中配置 Dockerfile
为 Product Service 实现 Dockerfile
。
FROM adoptopenjdk/openjdk11:alpine
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 Service 的 Dockerfile
基本相同。
5.2、使用 Docker Compose 配置服务
现在,让我们把整个设置配置到 docker-compose.yml
:
version: "4.0"
services:
product-service:
build: spring-cloud-open-telemetry1/
ports:
- "8080:8080"
price-service:
build: spring-cloud-open-telemetry2/
ports:
- "8081"
collector:
image: otel/opentelemetry-collector:0.72.0
command: [ "--config=/etc/otel-collector-config.yml" ]
volumes:
- ./otel-config.yml:/etc/otel-collector-config.yml
ports:
- "4317:4317"
depends_on:
- jaeger-service
jaeger-service:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686"
- "14250"
现在,通过 docker-compose
运行这些服务:
$ docker-compose up
5.3、验证正在运行的 Docker 服务
除了 product-service
和 price-service
,我们还在整个设置中添加了 collector-service
和 jaeger-service
。collector 服务反过来依靠 jaeger-service
端点将链路跟踪数据导出到 Jaeger 后端。
对于 jaeger-service
,我们使用的是 jaegertracing/all-in-one
镜像,其中包括后台和 UI 组件。
使用 docker container
命令来验证服务的状态:
$ docker container ls --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"
CONTAINER ID NAMES STATUS PORTS
7b874b9ee2e6 spring-cloud-open-telemetry-collector-1 Up 5 minutes 0.0.0.0:4317->4317/tcp, 55678-55679/tcp
29ed09779f98 spring-cloud-open-telemetry-jaeger-service-1 Up 5 minutes 5775/udp, 5778/tcp, 6831-6832/udp, 14268/tcp, 0.0.0.0:16686->16686/tcp, 0.0.0.0:61686->14250/tcp
75bfbf6d3551 spring-cloud-open-telemetry-product-service-1 Up 5 minutes 0.0.0.0:8080->8080/tcp, 8081/tcp
d2ca1457b5ab spring-cloud-open-telemetry-price-service-1 Up 5 minutes 0.0.0.0:61687->8081/tcp
6、监控 Collector 中的链路追踪
像 Jaeger 这样的 telemetry collector 工具提供了前端应用来监控请求。我们可以实时或稍后查看请求的链路跟踪。
让我们在请求成功和失败时监控这些追踪。
6.1、请求成功时监控链路追踪
首先,调用 Product
端点 http://localhost:8080/product/100003
。
该请求将显示一些日志:
spring-cloud-open-telemetry-price-service-1 | 2023-01-06 19:03:03.985 INFO [price-service,825dad4a4a308e6f7c97171daf29041a,346a0590f545bbcf] 1 --- [nio-8081-exec-1] c.b.opentelemetry.PriceRepository : Getting Price from Price With Product Id 100003
spring-cloud-open-telemetry-product-service-1 | 2023-01-06 19:03:04.432 INFO [,825dad4a4a308e6f7c97171daf29041a,fb9c54565b028eb8] 1 --- [nio-8080-exec-1] c.b.opentelemetry.ProductRepository : Getting Product from Product Repo With Product Id 100003
spring-cloud-open-telemetry-collector-1 | Trace ID : 825dad4a4a308e6f7c97171daf29041a
Spring Sleuth 会自动配置 ProductService
,将链路 trace id 附加到当前线程,并作为 HTTP Header 附加到下游 API 调用。PriceService
也会自动在线程上下文和日志中包含相同的 trace id。Otel service 将使用此 trace id 来确定跨服务的请求链路。
不出所料,上述 Trace id ... f29041a
在 PriceService
和 ProductService
日志中都是一样的。
让我们在 Jaeger UI (16686 端口)中查看整个请求跨度时间线:
上图显示了请求链的时间线,并包含表示请求的元数据。
6.2、请求失败时监控链路追踪
假设下游服务抛出异常,导致请求失败。我们再次使用相同的 UI 来分析根本原因。
让我们调用 Product
端点 /product/100005
调用来测试上述情况,此时下游应用中不存在此 Product
。
现在,让我们查看失败的请求跨度:
如上图所示,我们可以追溯到请求的最后一次 API
调用,错误就发生在这里。
7、总结
在本文中,我们了解了 OpenTelemetry
如何帮助微服务实现可观察性模式的标准化。
我们还通过一个示例了解了如何在 Spring Boot 应用中整合 OpenTelemetry
,以及如何配置 OpenTelemetry
来追踪请求链路。
参考:https://www.baeldung.com/spring-boot-opentelemetry-setup