通过OpenTelemetry Java SDK为调用链增加自定义埋点

接入ARMS应用监控以后,ARMS探针对常见的Java框架进行了自动埋点,因此不需要修改任何代码,就可以实现调用链信息的采集。如果您需要在调用链信息中,体现业务方法的执行情况,可以引入OpenTelemetry Java SDK,在业务代码中增加自定义埋点。

ARMS探针支持的组件和框架,请参见ARMS应用监控支持的Java组件和框架

前提条件

引入依赖

请先参考如下Maven代码引入OpenTelemetry Java SDK。更多信息,请参见OpenTelemetry官方文档

<dependencies>
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
    </dependency>
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-sdk-trace</artifactId>
    </dependency>
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-sdk</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-bom</artifactId>
      <version>1.23.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

ARMSOpenTelemetry埋点的兼容

名词介绍

此处只介绍一些常用的名词,其他名称解释,请参见OpenTelemetry Specification

  • Span:一次请求的一个具体操作,比如远程调用入口或者内部方法调用。

  • SpanContext:一次请求追踪的上下文,包含traceId、spanId等信息。

  • Attribute:Span的附加属性字段,用于记录关键信息。

  • Baggae:在整条链路上透传的一些Key、Value信息。

使用OpenTelemetry Java SDK

通过OpenTelemetrySDK主要可以实现以下操作:

  • 埋点生成Span。

  • Span增加Attributes。

  • 在链路上下文透传Baggage。

  • 获取当前Trace上下文并打印traceId、spanId等。

此处通过一段示例代码演示如何使用OpenTelemetry SDK完成上述操作。

重要

以下代码片段需要注意,最终获取OpenTelemetry实例需要通过调用GlobalOpenTelemetry.get()方法获取,不能直接使用上一步通过OpenTelemetry SDK手动构建的Opentelemetry实例。否则会导致在4.x版本探针中无法看到通过SDK埋点生成的Span数据。

@RestController
@RequestMapping("/ot")
public class OpenTelemetryController {

    private Tracer tracer;

    private ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();

    @PostConstruct
    public void init() {
	    OpenTelemetrySdk.builder()
			.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
			.buildAndRegisterGlobal();

		tracer = GlobalOpenTelemetry.get().getTracer("manual-sdk", "1.0.0");

        ses.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                Span span = tracer.spanBuilder("schedule")
                        .setAttribute("schedule.time", System.currentTimeMillis())
                        .startSpan();
                try (Scope scope = span.makeCurrent()) {
                    System.out.println("scheduled!");
                    Thread.sleep(500L);
                    span.setAttribute("schedule.success", true);
                    System.out.println(Span.current().getSpanContext().getTraceId()); // 获取 TraceId
                } catch (Throwable t) {
                    span.setStatus(StatusCode.ERROR, t.getMessage());
                } finally {
                    span.end();
                }
            }
        }, 10, 30, TimeUnit.SECONDS);
    }

    @ResponseBody
    @RequestMapping("/parent")
    public String parent() {
        Span span = tracer.spanBuilder("parent").setSpanKind(SpanKind.SERVER).startSpan();
        try (Scope scope = span.makeCurrent()) {
            // 使用Baggage透传业务自定义标签
            Baggage baggage =  Baggage.current().toBuilder()
                    .put("user.id", "1")
                    .put("user.name", "name")
                    .build();
            try (Scope baggageScope = baggage.storeInContext(Context.current()).makeCurrent()) {
                child();
            }
            span.setAttribute("http.method", "GET");
            span.setAttribute("http.uri", "/parent");
        } finally {
            span.end();
        }
        return "parent";
    }

    private void child() {
        Span span = tracer.spanBuilder("child").startSpan();
        try (Scope scope = span.makeCurrent()) {
            System.out.println("current traceId = " + Span.current().getSpanContext().getTraceId());
            System.out.println("userId in baggage = " + Baggage.current().getEntryValue("user.id"));
            Thread.sleep(1000);
        } catch (Throwable e) {
            span.setStatus(StatusCode.ERROR, e.getMessage());
        } finally {
            span.end();
        }
    }
}

示例代码说明:

  1. OpenTelemetryControllerinit方法中启动了一个定时调度任务,并在调度任务开始时创建一个Span,在调度任务结束时关闭该Span。

  2. OpenTelemetryControllerparent方法中调用了Opentelemetry SDK的多个方法。

    1. 每次被调用时创建一个名为parentSpan,并在方法结束时关闭该Span。

    2. 调用Baggage SDK,新加了名为user.iduser.name的两对Baggage,这两个Baggage后续会传递到下游应用。

    3. 为步骤2.1创建的Span新增了两个Attribute。

  3. OpenTelemetryControllerchild方法中会执行以下操作。

    1. 每次被调用时创建一个名为childSpan,并在方法结束时关闭改Span,需要注意,该Span2.1步骤创建的Span的子Span。

    2. 调用获取Trace上下文的方法并打印traceId。

    3. 调用获取Baggage的方法并打印步骤2.2中新增的Baggage。

不同探针差异点对比

对于上述代码中的不同操作,ARMS 3.x及以下版本探针和ARMS 4.x探针的支持情况有所区别,详情可见下方表格。

步骤

4.x及以上版本探针

3.x及以下版本探针

1

支持,会生成一个新的Span。

支持,会生成一个新的Span。

2.1

支持

支持

2.2

支持

不支持

2.3

支持

支持

3.1

支持

支持,该Span会作为步骤2.1创建出来的Span的方法栈存在。

3.2

支持,和ARMStraceId是一个。

不支持,打印出的traceIdARMS探针中的traceId不同。

3.2

支持

支持

埋点效果展示

4.x及以上版本

  • 步骤1埋点效果:

    可以正常看到通过OpenTelemetry SDK生成的Span。

    image

  • 步骤2.x、3.x埋点效果:

    通过OpenTelemetry SDK生成的Span(红框)和当前探针埋点Tomcat生成的Span(黄框)在一条链路上,且通过OpenTelemetry SDK生成的Span的相关Attribute也设置成功(蓝框)。

    image.png

3.x及以上版本探针

  • 步骤1埋点效果:

    2024-12-16_15-41-45

  • 步骤2.x、3.x埋点效果:

    通过OpenTelemetry SDK生成的Span(红框)和当前探针埋点Tomcat生成的Span(黄框)在一条链路上,其中名为childSpan会作为名为parentSpan的方法栈存在,且通过OpenTelemetry SDK生成的Span的相关Attribute也设置成功(蓝框)。

    image

    image

相关文档

您可以在应用的业务日志中关联调用链的TraceId信息,从而在应用出现问题时,能够通过调用链的TraceId快速关联到业务日志,及时定位、分析解决问题。更多信息,请参见Java应用业务日志关联调用链TraceId