通过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埋点的兼容

ARMSOpenTelemetry埋点的兼容性介绍涉及以下名词,OpenTelemetry相关的其他名称解释,请参见OpenTelemetry Specification

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

  • SpanContext:一次请求追踪的上下文,用于关联该次请求下的具体操作。

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

OpenTelemetrySpan可以分为三类:

  • 入口Span:会创建新的SpanContext,例如Server、Consumer。

    说明

    对于此类Span,ARMS的埋点入口多位于框架内部,手动埋点时链路上下文已存在,ARMS会将OpenTelemetry的入口Span作为内部Span处理。对于ARMS没有埋点的入口,则OpenTelemetry的入口Span保持不变,例如异步调用或者自定义RPC框架入口,同时ARMS会在客户端聚合,生成相关统计数据。

  • 内部Span:会复用已经创建的SpanContext,作为内部方法栈记录。

  • 出口Span:会将SpanContext透传下去,例如Client、Producer。

目前,ARMS对于入口Span和内部Span做了兼容。对于出口Span,ARMS暂不支持按照OpenTelemetry标准透传,而是按照ARMS自定义格式透传。

例如以下代码:

重要

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

package com.alibaba.arms.brightroar.console.controller;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

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

    private Tracer tracer;

    private ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();

    @PostConstruct
    public void init() {
		OpenTelemetry openTelemetry = 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.builder()
                    .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()) {
            span.setAttribute("user.id", Baggage.current().getEntryValue("user.id"));
            span.addEvent("Sleep Start");
            Thread.sleep(1000);
            Attributes attr = Attributes.of(AttributeKey.longKey("cost"), 1000L);
            span.addEvent("Sleep End", attr);
        } catch (Throwable e) {
            span.setStatus(StatusCode.ERROR, e.getMessage());
        } finally {
            span.end();
        }
    }

}

以上示例代码中通过OpenTelemetry SDK创建了三个Span:

  • parent:按照OpenTelemetry标准是HTTP入口,但是由于ARMSTomcat内置代码中已经创建了链路上下文,因此这里会作为一个内部方法记录在ARMS方法栈上。

  • childparent Span的内部Span,作为内部方法记录在方法栈上。

  • schedule:独立线程入口Span,默认情况下ARMS没有为此类入口创建上下文,因此这里会作为一个自定义方法入口,并生成相应的统计数据。

使用OpenTelemetry Baggage API透传业务自定义标签

OpenTelemetry中,Baggage可以作为上下文信息在Span之间传递,通过向Baggage设置键值对,透传业务自定义标签。Baggage存储在HTTP Header中并通过HTTP Header进行传播,因此不应在Baggage中存储敏感数据。

以上示例代码的Parent Span先在Baggage中存储两个键值对,然后在Child Span中获取Baggage中存储的值。

获取Trace ID

SpanContext中包含Trace IDSpan ID等信息,Trace ID可以通过Span.current().getSpanContext().getTraceId()方法获得。

ARMS控制台查看parentchild

ARMS控制台找到/ot/parentHTTP入口的内部方法栈,可以看到多了以下Span的展示。更多信息,请参见调用链分析

OTel埋点内部Span

ARMS控制台查看schedule

ARMS控制台支持通过以下几个页面查看:

  • 可以在ARMS控制台应用总览页面看到自定义入口的统计。更多信息,请参见应用总览OTel埋点出口Span

  • 可以在接口调用页面查看Span详细信息。更多信息,请参见接口调用Otel接口调用页面

  • 可以在调用链路页面看到独立的入口。更多信息,请参见调用链分析OTel埋点调用链查询

    单击放大镜图标,可以查看入口的详细信息。Otel埋点调用链详情

    目前ARMS支持将OpenTelemetry SpanAttribute作为Tags展示,单击Span名称可看到SpanAttribute。

    Span的Attribute

异步上下文传递

说明

4.x及以上探针版本支持。

下方是一个简单的生产者消费者模式代码示例,在生产者中生产事件时,将生产者线程的Trace上下文记录到事件中,在消费事件时,取出上下文并还原。

class Event {
    private Context context;
    private String msg;

    public Event(Context context, String msg) {
        this.context = context;
        this.msg = msg;
    }
}

private final LinkedBlockingQueue<Event> linkedBlockingQueue = new LinkedBlockingQueue<Event>();

public void produce(String msg) {
    linkedBlockingQueue.add(new Event(Context.current(), msg));
}

public void consume() throws Exception {
    Event event = linkedBlockingQueue.take();
    try(Scope scope = event.context.makeCurrent()) {
        processEvent(event);
    }
}

public void processEvent(Event event) {
    //todo process event
}

相关文档

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