我們知道對于 JAVA 應用可以通過 OpenTelemetry 提供的 Java agent 來實現自動埋點功能,在大多數場景下也完全足夠了,但是有時候我們需要更加精細的控制,這時候我們就需要使用手動埋點的方式來實現了。
使用注解埋點
我們可以在 Java 應用通過手動埋點的方式來實現鏈路追蹤,但如果我們不希望進行太多的代碼更改,那么可以使用注解的方式來實現,OpenTelemetry 提供了一些注解來幫助我們實現手動埋點,比如 @WithSpan
、@SpanAttribute
。
首先我們需要添加依賴庫 opentelemetry-instrumentation-annotations
。
<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>1.29.0</version>
</dependency>
</dependencies>
開發人員可以使用 @WithSpan
注解來向 OpenTelemetry 自動檢測發送信號,每當標記的方法被執行時都應創建一個新的 span。
比如我們在 Order Service 中的 IndexController
中添加一個 @WithSpan
注解,代碼如下所示:
// src/mAIn/java/com/youdianzhishi/orderservice/controller/IndexController.java
package com.youdianzhishi.orderservice.controller;
// ......
import io.opentelemetry.instrumentation.annotations.WithSpan;
@RestController
@RequestMApping("/")
public class IndexController {
@GetMapping
@WithSpan
public ResponseEntity<String> home(HttpServletRequest request) {
return new ResponseEntity<>("Hello OpenTelemetry!", HttpStatus.OK);
}
}
然后我們重建鏡像,重新啟動容器,當我們訪問首頁的時候就可以看到 Jaeger UI 中多了一個 IndexController.home
的 span 了。
每次應用程序調用有注解的方法時,它都會創建一個表示其持續時間并提供任何拋出異常的 span。默認情況下,span 名稱是 <className>.<methodName>
,當然也可以在注解中提供了一個名稱作為參數,比如可以使用 @WithSpan("indexSpan")
來指定 span 的名稱,這樣在 Jaeger UI 中就可以看到 indexSpan
的 span 了。
此外當為一個帶注解的方法創建一個 span 時,可以通過使用 @SpanAttribute
注解來自動將方法調用的參數值添加為創建 span 的屬性。
比如我們在 IndexController
中添加一個 fetchId
函數,并接收一個 id 參數,我們就可以使用 @SpanAttribute
注解來將接收的 id 參數添加為 indexSpanWithAttr
這個 span 的屬性,代碼如下所示:
// src/main/java/com/youdianzhishi/orderservice/controller/IndexController.java
package com.youdianzhishi.orderservice.controller;
// ......
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
@RestController
@RequestMapping("/")
public class IndexController {
@GetMapping
@WithSpan("indexSpan")
public ResponseEntity<String> home(HttpServletRequest request) {
return new ResponseEntity<>("Hello OpenTelemetry!", HttpStatus.OK);
}
@GetMapping("/{id}")
@WithSpan("indexSpanWithAttr")
public ResponseEntity<String> fetchId(@SpanAttribute("id") @PathVariable Long id) {
return new ResponseEntity<>("Hello OpenTelemetry:" + id, HttpStatus.OK);
}
}
然后我們重建鏡像,重新啟動容器,當我們訪問 http://localhost:8081/123
的時候就可以看到 Jaeger UI 中多了一個 indexSpanWithAttr
的 span 了,并且該 span 的屬性中包含了我們傳遞的 id 參數。
使用 API 手動埋點
除了使用注解的方式來實現埋點之外,我們還可以使用 OpenTelemetry 提供的 API 來實現手動埋點,這樣我們就可以更加精細的控制我們的 span 了,當然這樣也會增加我們的代碼量,但就不需要使用 java agent 了。
在 Java 應用中,要實現手動埋點,首先第一步是獲取 OpenTelemetry 接口的實例,我們需要盡早在應用程序中配置一個 OpenTelemetrySdk 的實例,我們可以使用 OpenTelemetrySdk.builder()
方法來完成這個操作。然后可以通過返回的 OpenTelemetrySdkBuilder
實例獲取與信號、跟蹤和指標相關的提供程序,以構建 OpenTelemetry 實例。我們可以使用 SdkTracerProvider.builder()
和 SdkMeterProvider.builder()
方法來構建 Provider
。此外還強烈建議將 Resource
實例定義為生成遙測數據的實體的表示;特別是 service.name
屬性是最重要的遙測源標識信息的一部分。
當然我們需要先在應用中添加相關依賴庫,代碼如下所示:
<!-- pom.xml -->
<project>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.29.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-semconv</artifactId>
<version>1.29.0-alpha</version>
</dependency>
</dependencies>
</project>
在 pom.xml
文件中添加了 opentelemetry-api
、opentelemetry-sdk
、opentelemetry-exporter-otlp
、opentelemetry-semconv
這幾個依賴庫,其中 opentelemetry-semconv
是用來定義一些常用的屬性的,比如 service.name
、http.method
、http.status_code
等,當然現在我們就不需要 opentelemetry-instrumentation-annotations
這個依賴庫了。
在 Spring Boot 項目中,初始化 OpenTelemetry 的一種常見方法是使用 @Configuration
類。這樣的類會在 Spring Boot 應用啟動時自動運行,使得初始化工作更加集中和組織化。
我們這里創建一個如下所示的 OpenTelemetryConfig
類,代碼如下所示:
// src/main/java/com/youdianzhishi/orderservice/config/OpenTelemetryConfig.java
package com.youdianzhishi.orderservice.config;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@Configuration
@Order(2)
public class OpenTelemetryConfig {
@Bean
public OpenTelemetry openTelemetry() {
GlobalOpenTelemetry.resetForTest(); // 初始化之前先重置 GlobalOpenTelemetry
// 從環境變量中獲取 OTLP Exporter 的地址
String exporterEndpointFromEnv = System.getenv("OTLP_EXPORTER_ENDPOINT");
String exporterEndpoint = exporterEndpointFromEnv != null ? exporterEndpointFromEnv
: "http://otel-collector:4317";
String serviceNameFromEnv = System.getenv("SERVICE_NAME");
String serviceName = serviceNameFromEnv != null ? serviceNameFromEnv : "order-service";
// 初始化 OTLP Exporter
OtlpGrpcSpanExporter exporter = OtlpGrpcSpanExporter.builder()
.setEndpoint(exporterEndpoint)
.build();
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(
ResourceAttributes.SERVICE_NAME, serviceName,
ResourceAttributes.TELEMETRY_SDK_LANGUAGE, "java")));
// 初始化 TracerProvider
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(SimpleSpanProcessor.create(exporter))
.setResource(resource)
.build();
// 初始化 ContextPropagators,這里我們配置包含 W3C Trace Context 和 W3C Baggage
ContextPropagators propagators = ContextPropagators.create(
TextMapPropagator.composite(
W3CTraceContextPropagator.getInstance(),
W3CBaggagePropagator.getInstance()));
// 初始化并返回 OpenTelemetry SDK
return OpenTelemetrySdk.builder()
.setPropagators(propagators)
.setTracerProvider(tracerProvider)
.buildAndRegisterGlobal();
}
@Bean
public Tracer tracer() {
return openTelemetry().getTracer(OrderserviceApplication.class.getName());
}
}
在上述代碼中,我們定義了一個 @Configuration
類,并使用 @Bean
注解為 OpenTelemetry 創建了一個 Bean,Spring 會管理這個 Bean 的生命周期,并在需要時自動注入。
這樣,你的 Spring Boot 應用每次啟動時,都會執行這些初始化代碼,從而確保了 OpenTelemetry 的正確配置。
在真正初始化的代碼中,我們首先從環境變量中獲取 OTLP Exporter 的地址,然后初始化 OTLP Exporter,接著初始化 TracerProvider,最后初始化并返回 OpenTelemetry SDK。
比如現在我們在 OrderController
中的 getAllOrders
處理器中來手動埋點,代碼如下所示:
// src/main/java/com/youdianzhishi/orderservice/controller/OrderController.java
package com.youdianzhishi.orderservice.controller;
// ......
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderserviceApplication.class);
@Autowired
private OrderRepository orderRepository;
@Autowired
private WebClient webClient;
@Autowired
private Tracer tracer; // 注入 Tracer
@GetMapping
public ResponseEntity<List<OrderDto>> getAllOrders(HttpServletRequest request) {
// 創建一個新的 Span 并設置 Span 名稱為 "GET /api/orders"
var span = tracer.spanBuilder("GET /api/orders").startSpan();
// 將 Span 注入到上下文中
try (var scope = span.makeCurrent()) {
// 從攔截器中獲取用戶信息
User user = (User) request.getAttribute("user");
// 要根據 orderDate 倒序排列
List<Order> orders = orderRepository.findByUserIdOrderByOrderDateDesc(user.getId());
// 將Order轉換為OrderDto
List<OrderDto> orderDtos = orders.stream().map(order -> {
try {
return order.toOrderDto(webClient);
} catch (Exception e) {
throw new RuntimeException(e);
}
}).collect(Collectors.toList());
span.setAttribute("user_id", user.getId());
span.setAttribute("order_count", orders.size());
return new ResponseEntity<>(orderDtos, HttpStatus.OK);
} catch (Exception e) {
// 記錄 Span 錯誤
span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
// 記錄 Span 結束時間
span.end();
}
}
// 忽略其他......
}
上面代碼中我們首先通過 openTelemetry.getTracer(OrderController.class.getName())
方法來初始化 Tracer,然后通過 tracer.spanBuilder("getAllOrders").startSpan()
方法來創建一個新的 Span,接著通過 span.makeCurrent()
方法將 Span 注入到上下文中,然后就可以在 try
代碼塊中執行我們的業務邏輯了,這里我們添加了兩個屬性,如果出現了異常則會記錄異常信息,最后在 finally
代碼塊中結束 Span。
我們還需要修改 Dockerfile 中的啟動命令,代碼如下所示:
# ......
# CMD ["mvn", "-Pdev", "spring-boot:run"]
CMD ["mvn", "spring-boot:run"]
因為現在我們不需要使用 java agent 了,所以去掉 -Pdev
參數(該 profile 中定義了 java agent 啟動參數),然后重新構建鏡像,重新啟動容器,當我們訪問訂單列表后就可以看到 Jaeger UI 中多了一個 getAllOrders
的 span 了。
很明顯我們可以看到現在的 span 非常簡單,沒有和前端 frontend 服務的 span 關聯起來。
由于前端 frontend 在請求后端接口的時候我們已經注入了 W3CTraceContext
,所以我們只需要在 Java 應用中通過 propagation api
來獲取到 span context,然后將其作為父級 span,這樣就可以將前端的 span 和后端的 span 關聯起來了。
這里我們可以添加一個攔截器來使用 propagation
接口解析 span context,代碼如下所示:
// src/main/java/com/youdianzhishi/orderservice/interceptor/OpenTelemetryInterceptor.java
package com.youdianzhishi.orderservice.interceptor;
// ......
@Component
public class OpenTelemetryInterceptor implements HandlerInterceptor {
@Autowired
private OpenTelemetry openTelemetry;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
TextMapGetter<HttpServletRequest> getter = new TextMapGetter<>() {
@Override
public Iterable<String> keys(HttpServletRequest carrier) {
return Collections.list(carrier.getHeaderNames());
}
@Override
public String get(HttpServletRequest carrier, String key) {
return carrier.getHeader(key);
}
};
// 提取傳入的Trace Context
Context extractedContext = openTelemetry.getPropagators().getTextMapPropagator()
.extract(Context.current(), request, getter);
StringBuilder sb = new StringBuilder();
sb.append(request.getMethod()).append(" ").append(request.getRequestURI());
Span span = tracer.spanBuilder(sb.toString()).setParent(extractedContext)
.startSpan();
// 將解析出來的SpanContext存儲在請求屬性中,以便后續使用
request.setAttribute("currentSpan", span);
return true;
}
}
上面代碼中我們首先通過 openTelemetry.getPropagators().getTextMapPropagator()
方法來獲取到 TextMapPropagator
,然后通過 extract
方法來解析 span context,然后將解析出來的 span context 設置為子 span 的父級 span,最后將 span context 存儲在請求屬性中,以便后續使用。
這里的關鍵是在初始化 OpenTelemetry 的時候需要配置 ContextPropagators
,代碼如下所示:
// 初始化 ContextPropagators,這里我們配置包含 W3C Trace Context 和 W3C Baggage
ContextPropagators propagators = ContextPropagators.create(
TextMapPropagator.composite(
W3CTraceContextPropagator.getInstance(),
W3CBaggagePropagator.getInstance()));
這樣我們才能去解析 TraceContext 和 Baggage 兩種上下文傳播機制。而其中的 getter
就是用來從 HTTP 請求頭中獲取 span context 的方式。
當然最后我們還需要在 WebMvcConfig
中注冊該攔截器,代碼如下所示:
// src/main/java/com/youdianzhishi/orderservice/config/WebMvcConfig.java
package com.youdianzhishi.orderservice.config;
// ......
@Configuration
@Order(4)
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;
@Autowired
private OpenTelemetryInterceptor otelCtxInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(otelCtxInterceptor)
.addPathPatterns("/api/orders/**");
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/api/orders/**") // 指定攔截器應該應用的路徑模式
.excludePathPatterns("/api/login", "/api/register"); // 指定應該排除的路徑模式
}
}
這樣當我們在請求 /api/orders/**
下面的接口時,就可以從請求屬性中獲取父級的 span context 了。
現在我們重新修改 getAllOrders
處理器,代碼如下所示:
@GetMapping
public ResponseEntity<List<OrderDto>> getAllOrders(HttpServletRequest request) {
// 從請求屬性中獲取 Span
Span span = (Span) request.getAttribute("currentSpan");
try {
// 從攔截器中獲取用戶信息
User user = (User) request.getAttribute("user");
// 要根據 orderDate 倒序排列
List<Order> orders = orderRepository.findByUserIdOrderByOrderDateDesc(user.getId());
// 將Order轉換為OrderDto
List<OrderDto> orderDtos = orders.stream().map(order -> {
try {
return order.toOrderDto(webClient);
} catch (Exception e) {
throw new RuntimeException(e);
}
}).collect(Collectors.toList());
span.setAttribute("user_id", user.getId());
span.setAttribute("order_count", orders.size());
return new ResponseEntity<>(orderDtos, HttpStatus.OK);
} catch (Exception e) {
// 記錄 Span 錯誤
span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
// 記錄 Span 結束時間
span.end();
}
}
這里我們首先通過請求屬性獲取到 span context,這里我們添加了兩個屬性,如果出現了異常則會記錄異常信息,最后在 finally
代碼塊中結束 Span。
現在我們重新啟動容器,當我們訪問訂單列表后就可以看到 Jaeger UI 中多了一個 GET /api/orders
的 span 了,并且該 span 和前端 frontend 服務的 span 關聯起來了。
當然這還不夠,因為我們的訂單列表接口還會去請求 user-service
服務來獲取用戶信息,還會去請求 catalog-service
服務獲取書籍信息,所以我們還需要在這兩個請求中也注入我們這里的 span,這樣就可以將整個鏈路串聯起來了。
首先針對 TokenInterceptor
攔截器我們先創建一個子 span,代碼如下所示:
// src/main/java/com/youdianzhishi/orderservice/interceptor/TokenInterceptor.java
package com.youdianzhishi.orderservice.interceptor;
// ......
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private WebClient webClient;
@Autowired
private Tracer tracer;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 先獲取 Span
Span currentSpan = (Span) request.getAttribute("currentSpan");
Context context = Context.current().with(currentSpan);
// 創建新的 Span,作為子 Span
Span span = tracer.spanBuilder("GET /api/userinfo")
.setParent(context).startSpan();
// 將子 Span 設置為當前上下文,相當于切換上下文到子 Span
try (Scope scope = span.makeCurrent()) {
try {
String token = request.getHeader("Authorization");
if (token == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
span.addEvent("Token is null").setStatus(StatusCode.ERROR);
return false;
}
// 從環境變量中獲取 userServiceUrl
String userServiceEnv = System.getenv("USER_SERVICE_URL");
String userServiceUrl = userServiceEnv != null ? userServiceEnv : "http://localhost:8080";
User user = webClient.get()
.uri(userServiceUrl + "/api/userinfo")
.header(HttpHeaders.AUTHORIZATION, token)
.retrieve()
.onStatus(httpStatus -> httpStatus.equals(HttpStatus.UNAUTHORIZED),
clientResponse -> Mono.error(new RuntimeException("Unauthorized")))
.onStatus(
httpStatus -> httpStatus.is4xxClientError()
&& !httpStatus.equals(HttpStatus.UNAUTHORIZED),
clientResponse -> Mono.error(new RuntimeException("Other Client Error")))
.bodyToMono(User.class)
.block();
if (user != null) {
request.setAttribute("user", user);
span.setAttribute("user_id", user.getId());
return true;
} else {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
span.addEvent("User is null").setStatus(StatusCode.ERROR);
return false;
}
} catch (RuntimeException e) {
span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
if (e.getMessage().equals("Unauthorized")) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
} else {
response.setStatus(HttpStatus.BAD_REQUEST.value());
}
return false;
} catch (Exception e) {
span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
return false;
} finally {
request.setAttribute("parentSpan", span);
span.end();
}
}
}
}
在上面代碼中我們首先獲取當前上下文的 Span,然后創建一個名為 GET /api/userinfo
的 span,將其設置為當前上下文的子 span,并將上下文切換到當前子 span,然后執行我們的業務邏輯,最后結束子 span。
然后我們可以統一在 WebClient 中來注入 span context,這樣當我們 Java 服務請求其他服務的時候就可以形成鏈路。
// src/main/java/com/youdianzhishi/orderservice/config/WebClientConfig.java
package com.youdianzhishi.orderservice.config;
// ......
@Configuration
@Order(3)
public class WebClientConfig {
@Autowired
private OpenTelemetry openTelemetry;
@Bean
public WebClient webClient() {
return WebClient.builder().filter(traceExchangeFilterFunction()).build();
}
@Bean
public ExchangeFilterFunction traceExchangeFilterFunction() {
return (clientRequest, next) -> {
// 獲取當前上下文的 Span
Span currentSpan = Span.current();
Context context = Context.current().with(currentSpan);
// 創建新的請求頭并添加跟蹤信息
HttpHeaders newHeaders = new HttpHeaders();
newHeaders.putAll(clientRequest.headers());
TextMapSetter<HttpHeaders> setter = new TextMapSetter<HttpHeaders>() {
@Override
public void set(HttpHeaders carrier, String key, String value) {
carrier.add(key, value);
}
};
// 將當前上下文的 Span 注入到請求頭中
openTelemetry.getPropagators().getTextMapPropagator().inject(context, newHeaders, setter);
// 創建一個新的 ClientRequest 對象
ClientRequest newRequest = ClientRequest.from(clientRequest)
.headers(headers -> headers.addAll(newHeaders))
.build();
return next.exchange(newRequest);
};
}
}
在上面代碼中我們為 WebClient 添加了一個名為 traceExchangeFilterFunction
的過濾器函數,在該函數中我們首先獲取當前上下文的 Span,然后創建一個新的請求頭并添加跟蹤信息,最后將當前上下文的 Span 通過 Propagator
接口注入到請求頭中,這樣當我們請求其他服務的時候就可以形成鏈路了。
現在我們重新啟動容器,當我們訪問訂單列表后就可以看到 Jaeger UI 中多了一個 GET /api/userinfo
的 span 了,并且該 span 和還會和 user-service
服務的 span 關聯起來。
同樣的方式我們還可以在 getAllOrders
處理器中添加數據庫查詢的 span,代碼如下所示:
// 新建一個 DB 查詢的 span
Span dbSpan = tracer.spanBuilder("DB findByUserIdOrderByOrderDateDesc").setParent(context).startSpan();
// 要根據 orderDate 倒序排列
List<Order> orders = orderRepository.findByUserIdOrderByOrderDateDesc(user.getId());
dbSpan.addEvent("OrderRepository findByUserIdOrderByOrderDateDesc From DB");
dbSpan.setAttribute("order_count", orders.size());
dbSpan.end();
將 Order 轉換為 OrderDto 也可以添加一個 span,代碼如下所示:
// src/main/java/com/youdianzhishi/orderservice/model/Order.java
package com.youdianzhishi.orderservice.model;
// ......
public OrderDto toOrderDto(WebClient webClient, Tracer tracer, Context context) throws Exception {
// 創建新的 Span,作為子 Span
Span span = tracer.spanBuilder("GET /api/books/batch").setParent(context).startSpan();
try (Scope scope = span.makeCurrent()) { // 切換上下文到子 Span
span.setAttribute("order_id", this.getId());
span.setAttribute("status", this.getStatus());
OrderDto orderDto = new OrderDto();
orderDto.setId(this.getId());
orderDto.setStatus(this.getStatus());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strDate = formatter.format(this.getOrderDate());
orderDto.setOrderDate(strDate);
List<Integer> bookIds = this.getBookIds(); // 假設你有一個可以獲取書籍ID的方法
// 將 bookIds 轉換為字符串,以便于傳遞給 WebClient
String bookIdsStr = bookIds.stream().map(String::valueOf).collect(Collectors.joining(","));
span.addEvent("get book ids");
span.setAttribute("book_ids", bookIdsStr);
// 用 WebClient 調用批量查詢書籍的服務接口
// 從環境變量中獲取 bookServiceUrl
String catalogServiceEnv = System.getenv("CATALOG_SERVICE_URL");
String catalogServiceUrl = catalogServiceEnv != null ? catalogServiceEnv : "http://localhost:8082";
Mono<List<BookDto>> booksMono = webClient.get() // 假設你有一個webClient實例
.uri(catalogServiceUrl + "/api/books/batch?ids=" + bookIdsStr)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<>() {
});
List<BookDto> books = booksMono.block();
span.addEvent("get books info from catalog service");
// 還需要將書籍數量和總價填充到 OrderDto 對象中
int totalAmount = 0;
int totalCount = 0;
List<BookQuantity> bqs = this.getBookQuantities();
for (BookDto book : books) {
// 如果 book.id 在 bqs 中,那么就將對應的數量設置到 book.quantity 中
int quantity = bqs.stream().filter(bq -> bq.getId() == book.getId()).findFirst().get().getQuantity();
book.setQuantity(quantity);
totalCount += quantity;
totalAmount += book.getPrice() * quantity;
}
orderDto.setBooks(books);
orderDto.setAmount(totalAmount);
orderDto.setTotal(totalCount);
span.addEvent("calculate total amount and total count");
span.end();
return orderDto;
}
}
這里同樣我們會為每一個轉換創建一個子 span,然后將其設置為當前上下文的子 span,最后結束子 span,這樣當我們通過 WebClient 去請求 catalog-service
服務的時候也就可以形成鏈路了。
最后我們再去查看下完整的鏈路,如下圖所示:
完整代碼請查看:https://Github.com/cnych/podemo。