全部產品
Search
文件中心

Application Real-Time Monitoring Service:通過OpenTelemetry Java SDK為調用鏈增加自訂埋點

更新時間:Nov 20, 2024

接入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>

ARMS對OpenTelemetry埋點的相容

ARMS對OpenTelemetry埋點的相容性介紹涉及以下名詞,OpenTelemetry相關的其他名稱解釋,請參見OpenTelemetry Specification

  • Span:一次請求的一個具體操作,比如遠程調用入口或者內部方法調用。

  • SpanContext:一次請求追蹤的上下文,用於關聯該次請求下的具體操作。

  • Attribute:Span的附加屬性欄位,用於記錄關鍵資訊。

OpenTelemetry的Span可以分為三類:

  • 入口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入口,但是由於ARMS在Tomcat內建代碼中已經建立了鏈路上下文,因此這裡會作為一個內部方法記錄在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 ID和Span ID等資訊,Trace ID可以通過Span.current().getSpanContext().getTraceId()方法獲得。

在ARMS控制台查看parentchild

在ARMS控制台找到/ot/parent的HTTP入口的內部方法棧,可以看到多了以下Span的展示。更多資訊,請參見調用鏈分析

OTel埋點內部Span

在ARMS控制台查看schedule

ARMS控制台支援通過以下幾個頁面查看:

  • 可以在ARMS控制台應用總覽頁面看到自訂入口的統計。更多資訊,請參見應用總覽OTel埋點出口Span

  • 可以在介面調用頁面查看Span詳細資料。更多資訊,請參見介面調用Otel介面調用頁面

  • 可以在調用鏈路頁面看到獨立的入口。更多資訊,請參見調用鏈分析OTel埋點調用鏈查詢

    單擊放大鏡表徵圖,可以查看入口的詳細資料。Otel埋點調用鏈詳情

    目前ARMS支援將OpenTelemetry Span的Attribute作為Tags展示,單擊Span名稱可看到Span的Attribute。

    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資訊