接入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自定义格式透传。
例如以下代码:
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方法栈上。child
:parent
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控制台查看parent
和child
在ARMS控制台找到/ot/parent的HTTP入口的内部方法栈,可以看到多了以下Span的展示。更多信息,请参见调用链分析。
在ARMS控制台查看schedule
ARMS控制台支持通过以下几个页面查看:
异步上下文传递
仅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信息。