OpenTelemetry với SpringBoot Application
1. Overview
Trong một hệ thống phân tán, các lỗi thỉnh thoảng chắc chắn sẽ xảy ra và việc truy vết, xử lý lỗi luôn là vấn đề phức tạp. OpenTelemetry là một nền tảng cung cấp khả năng quan sát, theo dõi, hỗ trợ bằng cách ghi lại dấu vết/nhật ký của ứng dụng và cung cấp giao diện để truy vấn một yêu cầu cụ thể. OpenTelemetry giúp chuẩn hóa quy trình thu thập và truy xuất dữ liệu từ xa.
Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách tích hợp SpringBoot với OpenTelemetry. Ngoài ra, chúng ta sẽ định cấu hình OpenTelemetry để ghi lại các dấu vết của ứng dụng và gửi chúng đến một hệ thống trung tâm để theo dõi các yêu cầu.
Đầu tiên, hãy hiểu một số khái niệm cơ bản.
2. Giới thiệu về OpenTelemetry
OpenTelemetry (Otel) là tập hợp các công cụ, API và SDK được chuẩn hóa dành cho nhà cung cấp. Đó là một dự án CNCF và là sự hợp nhất của các dự án OpenTracing và OpenCensus.
OpenTracing là một API trung lập với nhà cung cấp để gửi dữ liệu đo từ xa tới một dịch vụ phụ trợ khả năng quan sát. Dự án OpenCensus cung cấp một tập hợp các thư viện dành riêng cho ngôn ngữ mà các nhà phát triển có thể sử dụng để cung cấp mã của họ và gửi mã đó tới bất kỳ chương trình phụ trợ được hỗ trợ nào. Otel sử dụng cùng một khái niệm về theo dõi và khoảng thời gian để thể hiện luồng yêu cầu trên các microservices như được sử dụng bởi các dự án tiền nhiệm của nó.
OpenTelemetry cho phép tạo và thu thập dữ liệu từ xa, giúp phân tích hành vi hoặc hiệu suất của ứng dụng. Dữ liệu từ xa có thể bao gồm nhật ký, số liệu và dấu vết.
Sử dụng Otel SDK, chúng tôi có thể dễ dàng ghi đè hoặc thêm nhiều thuộc tính hơn vào log.
Hãy đi sâu vào vấn đề này với một ví dụ.
3. Ứng dụng
Hãy tưởng tượng chúng ta cần xây dựng hai service, trong đó một service tương tác với service còn lại. Chúng ta sẽ tích hợp ứng dụng với Spring Cloud và OpenTelemetry.
3.1. Maven Dependencies
Các phần phụ thuộc spring-cloud-starter-sleuth , spring-cloud-sleuth-otel-autoconfigure và opentelemetry-exporter-otlp sẽ tự động capture và xuất nhật ký sang bất kỳ trình thu thập được hỗ trợ nào.
Đầu tiên, chúng ta sẽ bắt đầu bằng cách tạo một dự án Spring Boot Web và bao gồm các phụ thuộc Spring và OpenTelemetry bên dưới vào cả hai ứng dụng:
<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>
Lưu ý rằng tôi đã loại trừ phần phụ thuộc Spring Cloud Brave để thay thế triển khai theo dõi mặc định bằng Otel.
Ngoài ra, chúng ta sẽ cần BOM quản lý phụ thuộc Spring cho 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. Implement the Downstream Application
Downstream Application sẽ có một endpoint để trả về dữ liệu Price .
Đầu tiên, hãy tạo class Price :
public class Price {
private long productId;
private double priceAmount;
private double discount;
}
Tiếp theo, hãy triển khai PriceController với endpoint GET Price :
@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);
}
}
Sau đó, chúng ta sẽ triển khai phương thức getPrice trong PriceRepository :
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. Implement the Upstream Application
Upstream Application cũng sẽ có một endpoint để lấy chi tiết Product và tích hợp với endpoint get Price ở trên:
Đầu tiên, hãy triển khai class Product :
public class Product {
private long id;
private String name;
private Price price;
}
Sau đó, hãy triển khai class ProductController với endpoint để get Product:
@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;
}
}
Tiếp theo, chúng ta sẽ triển khai phương thức getProduct trong ProductRepository :
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);
}
Cuối cùng, hãy triển khai phương thức getPrice trong PriceClient :
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. Cấu hình SpringBoot với OpenTelemetry
OpenTelemetry cung cấp một trình thu thập được gọi là trình thu thập Otel xử lý và xuất dữ liệu nhật ký từ xa tới bất kỳ chương trình phụ trợ khả năng quan sát nào như Jaeger , Prometheus và các chương trình phụ trợ khác.
Nhật ký có thể được xuất sang Otel Collector bằng cách sử dụng một vài cấu hình Spring Sleuth.
4.1. Định cấu hình Spring Sleuth
Chúng ta sẽ cần định cấu hình ứng dụng với điểm cuối Otel để gửi dữ liệu nhật ký từ xa.
Thêm cấu hình Spring Sleuth trong application.properties :
spring.sleuth.otel.config.trace-id-ratio-based=1.0
spring.sleuth.otel.exporter.otlp.endpoint=http://collector:4317
4.2. Cấu hình OpenTelemetry Collector
Otel Collector là công cụ theo dõi OpenTelemetry. Nó bao gồm các thành phần máy thu, bộ xử lý và bộ xuất. Có một thành phần tiện ích mở rộng tùy chọn giúp kiểm tra tình trạng, khám phá dịch vụ hoặc chuyển tiếp dữ liệu. Thành phần mở rộng không liên quan đến việc xử lý dữ liệu từ xa.
Để nhanh chóng khởi động các dịch vụ Otel, chúng tôi sẽ sử dụng điểm cuối phụ trợ Jaeger được lưu trữ tại cổng 14250 .
Hãy định cấu hình otel-config.yml với các giai đoạn của đường dẫn Otel:
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 ]
Chúng ta nên lưu ý rằng cấu hình bộ xử lý ở trên là tùy chọn và theo mặc định, không được bật. Tùy chọn batch processing giúp nén dữ liệu tốt hơn và giảm số lượng kết nối đi cần thiết để truyền dữ liệu.
Ngoài ra, chúng ta nên lưu ý rằng bộ thu được cấu hình với giao thức GRPC và HTTP .
5. Chạy ứng dụng
Bây giờ chúng ta sẽ định cấu hình và chạy toàn bộ thiết lập, các ứng dụng và trình thu thập Otel.
5.1. Định cấu hình Dockerfile trong Ứng dụng
Hãy triển khai Dockerfile cho Product Service:
FROM adoptopenjdk/openjdk11: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"]
Chúng ta nên lưu ý rằng Dockerfile cho Price Service cũng tương tự như trên .
5.2. Định cấu hình dịch vụ với Docker Compose
Bây giờ, hãy định cấu hình docker-compose.yml với toàn bộ thiết lập:
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"
Bây giờ hãy chạy các dịch vụ thông qua docker-compose :
$ docker-compose up
5.3. Xác thực Dịch vụ Docker đang chạy
Cùng với Product Service và Price Service , chúng tôi đã thêm Collector Service và jaeger vào toàn bộ thiết lập. Product Service và Price Service ở trên sử dụng cổng dịch vụ thu thập 4317 để gửi dữ liệu theo dõi. Ngược lại, dịch vụ thu thập dựa vào điểm cuối dịch vụ jaeger để xuất dữ liệu theo dõi sang phần phụ trợ của Jaeger .
Đối với dịch vụ jaeger , chúng tôi đang sử dụng jaegertracing/all-in-one image, bao gồm các thành phần phụ trợ và giao diện người dùng của nó.
Hãy xác minh trạng thái của dịch vụ bằng lệnh 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. Theo dõi dấu vết trong Collector
Các công cụ thu thập phép đo từ xa như Jaeger cung cấp các dashboard để theo dõi các request. Chúng ta có thể xem các dấu vết yêu cầu trong thời gian thực hoặc sau này.
Hãy theo dõi các dấu vết khi yêu cầu thành công cũng như khi nó thất bại.
6.1. Theo dõi dấu vết khi yêu cầu thành công
Trước tiên, hãy gọi Product endpoint http://localhost:8080/product/100003 .
Yêu cầu sẽ làm cho một số nhật ký xuất hiện:
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 sẽ tự động định cấu hình ProductService để đính kèm spanId vào luồng hiện tại và vào HTTP Header cho các lệnh gọi API downstream. PriceService cũng sẽ tự động bao gồm cùng một spanId trong ngữ cảnh luồng và nhật ký. Dịch vụ Otel sẽ sử dụng id theo dõi này để xác định luồng yêu cầu trên các dịch vụ.
Như mong đợi, spanId ở trên ….f29041a giống nhau trong cả nhật ký PriceService và ProductService .
Hãy hình dung toàn bộ dòng thời gian kéo dài yêu cầu trong giao diện người dùng Jaeger được lưu trữ tại cổng 16686 :
Phần trên hiển thị dòng thời gian của các luồng yêu cầu và chứa siêu dữ liệu để thể hiện yêu cầu.
6.2. Theo dõi dấu vết khi yêu cầu không thành công
Hãy tưởng tượng một kịch bản trong đó service downstream đưa ra một ngoại lệ, dẫn đến yêu cầu không thành công.
Một lần nữa, chúng tôi sẽ tận dụng cùng một giao diện người dùng để phân tích nguyên nhân gốc rễ.
Hãy kiểm tra kịch bản trên với lệnh gọi Điểm cuối sản phẩm /product/100005 khi Sản phẩm không có trong ứng dụng xuôi dòng.
Bây giờ, hãy hình dung các khoảng thời gian yêu cầu không thành công:
Như đã thấy ở trên, chúng tôi có thể theo dõi lại yêu cầu đối với lệnh gọi API cuối cùng nơi bắt nguồn lỗi.
7. Kết luận
Trong bài viết này, chúng ta đã tìm hiểu cách OpenTelemetry giúp chuẩn hóa các mẫu khả năng tracing cho microservice.
Chúng ta cũng đã xem cách cấu hình ứng dụng Spring Boot với OpenTelemetry bằng một ví dụ. Cuối cùng, chúng tôi đã theo dõi luồng yêu cầu API trong Collector.
Mã ví dụ có thể được tìm thấy trên GitHub .
All rights reserved