Java 应用通过 OpenTelemetry API 实现手动埋点(1)

上一篇 / 下一篇  2023-09-07 13:41:35

  我们知道对于 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 关联起来。

TAG: 软件开发 Java java

 

评分:0

我来说两句

Open Toolbar