<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java 應(yīng)用通過 OpenTelemetry API 實(shí)現(xiàn)手動(dòng)埋點(diǎn)

          共 44658字,需瀏覽 90分鐘

           ·

          2023-09-07 21:06

          我們知道對于 Java 應(yīng)用可以通過 OpenTelemetry 提供的 Java agent 來實(shí)現(xiàn)自動(dòng)埋點(diǎn)功能,在大多數(shù)場景下也完全足夠了,但是有時(shí)候我們需要更加精細(xì)的控制,這時(shí)候我們就需要使用手動(dòng)埋點(diǎn)的方式來實(shí)現(xiàn)了。

          使用注解埋點(diǎn)

          我們可以在 Java 應(yīng)用通過手動(dòng)埋點(diǎn)的方式來實(shí)現(xiàn)鏈路追蹤,但如果我們不希望進(jìn)行太多的代碼更改,那么可以使用注解的方式來實(shí)現(xiàn),OpenTelemetry 提供了一些注解來幫助我們實(shí)現(xiàn)手動(dòng)埋點(diǎn),比如 @WithSpan@SpanAttribute

          首先我們需要添加依賴庫 opentelemetry-instrumentation-annotations

          <dependencies>
            <dependency>
              <groupId>io.opentelemetry.instrumentation</groupId>
              <artifactId>opentelemetry-instrumentation-annotations</artifactId>
              <version>1.29.0</version>
            </dependency>
          </dependencies>

          開發(fā)人員可以使用 @WithSpan 注解來向 OpenTelemetry 自動(dòng)檢測發(fā)送信號,每當(dāng)標(biāo)記的方法被執(zhí)行時(shí)都應(yīng)創(chuàng)建一個(gè)新的 span。

          比如我們在 Order Service 中的 IndexController 中添加一個(gè) @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);
              }
          }

          然后我們重建鏡像,重新啟動(dòng)容器,當(dāng)我們訪問首頁的時(shí)候就可以看到 Jaeger UI 中多了一個(gè) IndexController.home 的 span 了。

          每次應(yīng)用程序調(diào)用有注解的方法時(shí),它都會(huì)創(chuàng)建一個(gè)表示其持續(xù)時(shí)間并提供任何拋出異常的 span。默認(rèn)情況下,span 名稱是 <className>.<methodName>,當(dāng)然也可以在注解中提供了一個(gè)名稱作為參數(shù),比如可以使用 @WithSpan("indexSpan") 來指定 span 的名稱,這樣在 Jaeger UI 中就可以看到 indexSpan 的 span 了。

          此外當(dāng)為一個(gè)帶注解的方法創(chuàng)建一個(gè) span 時(shí),可以通過使用 @SpanAttribute 注解來自動(dòng)將方法調(diào)用的參數(shù)值添加為創(chuàng)建 span 的屬性。

          比如我們在 IndexController 中添加一個(gè) fetchId 函數(shù),并接收一個(gè) id 參數(shù),我們就可以使用 @SpanAttribute 注解來將接收的 id 參數(shù)添加為 indexSpanWithAttr 這個(gè) 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);
              }
          }

          然后我們重建鏡像,重新啟動(dòng)容器,當(dāng)我們訪問 http://localhost:8081/123 的時(shí)候就可以看到 Jaeger UI 中多了一個(gè) indexSpanWithAttr 的 span 了,并且該 span 的屬性中包含了我們傳遞的 id 參數(shù)。

          使用 API 手動(dòng)埋點(diǎn)

          除了使用注解的方式來實(shí)現(xiàn)埋點(diǎn)之外,我們還可以使用 OpenTelemetry 提供的 API 來實(shí)現(xiàn)手動(dòng)埋點(diǎn),這樣我們就可以更加精細(xì)的控制我們的 span 了,當(dāng)然這樣也會(huì)增加我們的代碼量,但就不需要使用 java agent 了。

          在 Java 應(yīng)用中,要實(shí)現(xiàn)手動(dòng)埋點(diǎn),首先第一步是獲取 OpenTelemetry 接口的實(shí)例,我們需要盡早在應(yīng)用程序中配置一個(gè) OpenTelemetrySdk 的實(shí)例,我們可以使用 OpenTelemetrySdk.builder() 方法來完成這個(gè)操作。然后可以通過返回的 OpenTelemetrySdkBuilder 實(shí)例獲取與信號、跟蹤和指標(biāo)相關(guān)的提供程序,以構(gòu)建 OpenTelemetry 實(shí)例。我們可以使用 SdkTracerProvider.builder()SdkMeterProvider.builder() 方法來構(gòu)建 Provider。此外還強(qiáng)烈建議將 Resource 實(shí)例定義為生成遙測數(shù)據(jù)的實(shí)體的表示;特別是 service.name 屬性是最重要的遙測源標(biāo)識信息的一部分。

          當(dāng)然我們需要先在應(yīng)用中添加相關(guān)依賴庫,代碼如下所示:

          <!-- 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-apiopentelemetry-sdkopentelemetry-exporter-otlpopentelemetry-semconv 這幾個(gè)依賴庫,其中 opentelemetry-semconv 是用來定義一些常用的屬性的,比如 service.namehttp.methodhttp.status_code 等,當(dāng)然現(xiàn)在我們就不需要 opentelemetry-instrumentation-annotations 這個(gè)依賴庫了。

          在 Spring Boot 項(xiàng)目中,初始化 OpenTelemetry 的一種常見方法是使用 @Configuration 類。這樣的類會(huì)在 Spring Boot 應(yīng)用啟動(dòng)時(shí)自動(dòng)運(yùn)行,使得初始化工作更加集中和組織化。

          我們這里創(chuàng)建一個(gè)如下所示的 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

                  // 從環(huán)境變量中獲取 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());
              }
          }

          在上述代碼中,我們定義了一個(gè) @Configuration 類,并使用 @Bean 注解為 OpenTelemetry 創(chuàng)建了一個(gè) Bean,Spring 會(huì)管理這個(gè) Bean 的生命周期,并在需要時(shí)自動(dòng)注入。

          這樣,你的 Spring Boot 應(yīng)用每次啟動(dòng)時(shí),都會(huì)執(zhí)行這些初始化代碼,從而確保了 OpenTelemetry 的正確配置。

          在真正初始化的代碼中,我們首先從環(huán)境變量中獲取 OTLP Exporter 的地址,然后初始化 OTLP Exporter,接著初始化 TracerProvider,最后初始化并返回 OpenTelemetry SDK。

          比如現(xiàn)在我們在 OrderController 中的 getAllOrders 處理器中來手動(dòng)埋點(diǎn),代碼如下所示:

          // 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) {
                  // 創(chuàng)建一個(gè)新的 Span 并設(shè)置 Span 名稱為 "GET /api/orders"
                  var span = tracer.spanBuilder("GET /api/orders").startSpan();

                  // 將 Span 注入到上下文中
                  try (var scope = span.makeCurrent()) {
                      // 從攔截器中獲取用戶信息
                      User user = (User) request.getAttribute("user");

                      // 要根據(jù) orderDate 倒序排列
                      List<Order> orders = orderRepository.findByUserIdOrderByOrderDateDesc(user.getId());

                      // 將Order轉(zhuǎn)換為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 錯(cuò)誤
                      span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
                      return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
                  } finally {
                      // 記錄 Span 結(jié)束時(shí)間
                      span.end();
                  }
              }

              // 忽略其他......
          }

          上面代碼中我們首先通過 openTelemetry.getTracer(OrderController.class.getName()) 方法來初始化 Tracer,然后通過 tracer.spanBuilder("getAllOrders").startSpan() 方法來創(chuàng)建一個(gè)新的 Span,接著通過 span.makeCurrent() 方法將 Span 注入到上下文中,然后就可以在 try 代碼塊中執(zhí)行我們的業(yè)務(wù)邏輯了,這里我們添加了兩個(gè)屬性,如果出現(xiàn)了異常則會(huì)記錄異常信息,最后在 finally 代碼塊中結(jié)束 Span。

          我們還需要修改 Dockerfile 中的啟動(dòng)命令,代碼如下所示:

          # ......
          # CMD ["mvn", "-Pdev", "spring-boot:run"]
          CMD ["mvn""spring-boot:run"]

          因?yàn)楝F(xiàn)在我們不需要使用 java agent 了,所以去掉 -Pdev 參數(shù)(該 profile 中定義了 java agent 啟動(dòng)參數(shù)),然后重新構(gòu)建鏡像,重新啟動(dòng)容器,當(dāng)我們訪問訂單列表后就可以看到 Jaeger UI 中多了一個(gè) getAllOrders 的 span 了。

          很明顯我們可以看到現(xiàn)在的 span 非常簡單,沒有和前端 frontend 服務(wù)的 span 關(guān)聯(lián)起來。

          由于前端 frontend 在請求后端接口的時(shí)候我們已經(jīng)注入了 W3CTraceContext,所以我們只需要在 Java 應(yīng)用中通過 propagation api 來獲取到 span context,然后將其作為父級 span,這樣就可以將前端的 span 和后端的 span 關(guān)聯(lián)起來了。

          這里我們可以添加一個(gè)攔截器來使用 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存儲在請求屬性中,以便后續(xù)使用
                  request.setAttribute("currentSpan", span);

                  return true;
              }
          }

          上面代碼中我們首先通過 openTelemetry.getPropagators().getTextMapPropagator() 方法來獲取到 TextMapPropagator,然后通過 extract 方法來解析 span context,然后將解析出來的 span context 設(shè)置為子 span 的父級 span,最后將 span context 存儲在請求屬性中,以便后續(xù)使用。

          這里的關(guān)鍵是在初始化 OpenTelemetry 的時(shí)候需要配置 ContextPropagators,代碼如下所示:

          // 初始化 ContextPropagators,這里我們配置包含 W3C Trace Context 和 W3C Baggage
          ContextPropagators propagators = ContextPropagators.create(
                  TextMapPropagator.composite(
                          W3CTraceContextPropagator.getInstance(),
                          W3CBaggagePropagator.getInstance()));

          這樣我們才能去解析 TraceContext 和 Baggage 兩種上下文傳播機(jī)制。而其中的 getter 就是用來從 HTTP 請求頭中獲取 span context 的方式。

          當(dāng)然最后我們還需要在 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/**"// 指定攔截器應(yīng)該應(yīng)用的路徑模式
                      .excludePathPatterns("/api/login""/api/register"); // 指定應(yīng)該排除的路徑模式
              }

          }

          這樣當(dāng)我們在請求 /api/orders/** 下面的接口時(shí),就可以從請求屬性中獲取父級的 span context 了。

          現(xiàn)在我們重新修改 getAllOrders 處理器,代碼如下所示:

          @GetMapping
          public ResponseEntity<List<OrderDto>> getAllOrders(HttpServletRequest request) {
              // 從請求屬性中獲取 Span
              Span span = (Span) request.getAttribute("currentSpan");

              try {
                  // 從攔截器中獲取用戶信息
                  User user = (User) request.getAttribute("user");

                  // 要根據(jù) orderDate 倒序排列
                  List<Order> orders = orderRepository.findByUserIdOrderByOrderDateDesc(user.getId());

                  // 將Order轉(zhuǎn)換為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 錯(cuò)誤
                  span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
                  return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
              } finally {
                  // 記錄 Span 結(jié)束時(shí)間
                  span.end();
              }

          }

          這里我們首先通過請求屬性獲取到 span context,這里我們添加了兩個(gè)屬性,如果出現(xiàn)了異常則會(huì)記錄異常信息,最后在 finally 代碼塊中結(jié)束 Span。

          現(xiàn)在我們重新啟動(dòng)容器,當(dāng)我們訪問訂單列表后就可以看到 Jaeger UI 中多了一個(gè) GET /api/orders 的 span 了,并且該 span 和前端 frontend 服務(wù)的 span 關(guān)聯(lián)起來了。

          當(dāng)然這還不夠,因?yàn)槲覀兊挠唵瘟斜斫涌谶€會(huì)去請求 user-service 服務(wù)來獲取用戶信息,還會(huì)去請求 catalog-service 服務(wù)獲取書籍信息,所以我們還需要在這兩個(gè)請求中也注入我們這里的 span,這樣就可以將整個(gè)鏈路串聯(lián)起來了。

          首先針對 TokenInterceptor 攔截器我們先創(chuàng)建一個(gè)子 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);

                  // 創(chuàng)建新的 Span,作為子 Span
                  Span span = tracer.spanBuilder("GET /api/userinfo")
                      .setParent(context).startSpan();

                  // 將子 Span 設(shè)置為當(dāng)前上下文,相當(dāng)于切換上下文到子 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;
                          }
                          // 從環(huán)境變量中獲取 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();
                      }
                  }

              }
          }

          在上面代碼中我們首先獲取當(dāng)前上下文的 Span,然后創(chuàng)建一個(gè)名為 GET /api/userinfo 的 span,將其設(shè)置為當(dāng)前上下文的子 span,并將上下文切換到當(dāng)前子 span,然后執(zhí)行我們的業(yè)務(wù)邏輯,最后結(jié)束子 span。

          然后我們可以統(tǒng)一在 WebClient 中來注入 span context,這樣當(dāng)我們 Java 服務(wù)請求其他服務(wù)的時(shí)候就可以形成鏈路。

          // 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) -> {
                      // 獲取當(dāng)前上下文的 Span
                      Span currentSpan = Span.current();
                      Context context = Context.current().with(currentSpan);

                      // 創(chuàng)建新的請求頭并添加跟蹤信息
                      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);
                          }
                      };

                      // 將當(dāng)前上下文的 Span 注入到請求頭中
                      openTelemetry.getPropagators().getTextMapPropagator().inject(context, newHeaders, setter);

                      // 創(chuàng)建一個(gè)新的 ClientRequest 對象
                      ClientRequest newRequest = ClientRequest.from(clientRequest)
                              .headers(headers -> headers.addAll(newHeaders))
                              .build();

                      return next.exchange(newRequest);
                  };
              }
          }

          在上面代碼中我們?yōu)?WebClient 添加了一個(gè)名為 traceExchangeFilterFunction 的過濾器函數(shù),在該函數(shù)中我們首先獲取當(dāng)前上下文的 Span,然后創(chuàng)建一個(gè)新的請求頭并添加跟蹤信息,最后將當(dāng)前上下文的 Span 通過 Propagator 接口注入到請求頭中,這樣當(dāng)我們請求其他服務(wù)的時(shí)候就可以形成鏈路了。

          現(xiàn)在我們重新啟動(dòng)容器,當(dāng)我們訪問訂單列表后就可以看到 Jaeger UI 中多了一個(gè) GET /api/userinfo 的 span 了,并且該 span 和還會(huì)和 user-service 服務(wù)的 span 關(guān)聯(lián)起來。

          同樣的方式我們還可以在 getAllOrders 處理器中添加數(shù)據(jù)庫查詢的 span,代碼如下所示:

          // 新建一個(gè) DB 查詢的 span
          Span dbSpan = tracer.spanBuilder("DB findByUserIdOrderByOrderDateDesc").setParent(context).startSpan();
          // 要根據(jù) orderDate 倒序排列
          List<Order> orders = orderRepository.findByUserIdOrderByOrderDateDesc(user.getId());
          dbSpan.addEvent("OrderRepository findByUserIdOrderByOrderDateDesc From DB");
          dbSpan.setAttribute("order_count", orders.size());
          dbSpan.end();

          將 Order 轉(zhuǎn)換為 OrderDto 也可以添加一個(gè) 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 {
              // 創(chuàng)建新的 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(); // 假設(shè)你有一個(gè)可以獲取書籍ID的方法
                  // 將 bookIds 轉(zhuǎn)換為字符串,以便于傳遞給 WebClient
                  String bookIdsStr = bookIds.stream().map(String::valueOf).collect(Collectors.joining(","));
                  span.addEvent("get book ids");
                  span.setAttribute("book_ids", bookIdsStr);

                  // 用 WebClient 調(diào)用批量查詢書籍的服務(wù)接口
                  // 從環(huán)境變量中獲取 bookServiceUrl
                  String catalogServiceEnv = System.getenv("CATALOG_SERVICE_URL");
                  String catalogServiceUrl = catalogServiceEnv != null ? catalogServiceEnv : "http://localhost:8082";
                  Mono<List<BookDto>> booksMono = webClient.get() // 假設(shè)你有一個(gè)webClient實(shí)例
                          .uri(catalogServiceUrl + "/api/books/batch?ids=" + bookIdsStr)
                          .retrieve()
                          .bodyToMono(new ParameterizedTypeReference<>() {
                          });
                  List<BookDto> books = booksMono.block();

                  span.addEvent("get books info from catalog service");

                  // 還需要將書籍?dāng)?shù)量和總價(jià)填充到 OrderDto 對象中
                  int totalAmount = 0;
                  int totalCount = 0;
                  List<BookQuantity> bqs = this.getBookQuantities();
                  for (BookDto book : books) {
                      // 如果 book.id 在 bqs 中,那么就將對應(yīng)的數(shù)量設(shè)置到 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;
              }
          }

          這里同樣我們會(huì)為每一個(gè)轉(zhuǎn)換創(chuàng)建一個(gè)子 span,然后將其設(shè)置為當(dāng)前上下文的子 span,最后結(jié)束子 span,這樣當(dāng)我們通過 WebClient 去請求 catalog-service 服務(wù)的時(shí)候也就可以形成鏈路了。

          最后我們再去查看下完整的鏈路,如下圖所示:

          完整代碼請查看:https://github.com/cnych/podemo。


          最近我們推出了《OpenTelemetry 實(shí)踐》課程,感興趣的朋友可以直接掃描下方二維碼進(jìn)行購買,課程除了視頻之外,同樣還有完善的課程文檔。

          瀏覽 1188
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产操逼大片 | 日韩亚洲中文字幕 | 真实亲子乱一区二区 | 免费一级黄色片子 | 成人Av无码一区二区三区 |