通过OpenTelemetry接入Android Trace数据

本文介绍通过OpenTelemetry Java SDK将Android应用的Trace数据接入到日志服务的操作步骤。

前提条件

已创建Trace实例。更多信息,请参见创建Trace实例

步骤一:SDK集成

在App或Module级别的build.gradle文件中添加如下配置。

// BOM清单,用于同步依赖版本。
implementation(platform("io.opentelemetry:opentelemetry-bom:1.22.0"))
// API接口
implementation("io.opentelemetry:opentelemetry-api")
implementation("io.opentelemetry:opentelemetry-context")
// API extensions
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
// SDK
implementation('io.opentelemetry:opentelemetry-sdk')
implementation('io.opentelemetry:opentelemetry-sdk-logs')
// semantic conventions
implementation("io.opentelemetry:opentelemetry-semconv")
// Exporters
// 官方gRPC Exporter
implementation("io.opentelemetry:opentelemetry-exporter-otlp")

更多信息,请参见OpenTelemetry Java SDK Release

步骤二:权限配置

上报Trace数据时,需申请网络权限,即需要在AndroidManifest.xml文件中添加如下权限申明。

<uses-permission android:name="android.permission.INTERNET" />

步骤三:初始化SDK

一般建议在Application类的onCreate方法中进行SDK初始化。

// 初始化Exporter。Exporter用于导出Trace数据到日志服务Logstore。
OtlpGrpcSpanExporter grpcSpanExporter = OtlpGrpcSpanExporter.builder()
  .setEndpoint("https://${endpoint}") 
  .addHeader("x-sls-otel-project", "${project}")
  .addHeader("x-sls-otel-instance-id", "${instanceId}")
  .addHeader("x-sls-otel-ak-id", "${access-key-id}")
  .addHeader("x-sls-otel-ak-secret", "${access-key-secret}")
  .build();

// 初始化tracer provider。tracer provider用于暴露主要的API接口,对Span进行预处理、自定义Clock。
// 自定义TraceId、SpanId生成规则,自定义采样器等。您可以根据实际需求进行配置。
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
  .addSpanProcessor(BatchSpanProcessor.builder(grpcSpanExporter).build())
  .setResource(Resource.create(Attributes.builder()
                               .put(ResourceAttributes.SERVICE_NAME, "${service}")
                               .put(ResourceAttributes.SERVICE_NAMESPACE, "${service.namespace}")
                               .put(ResourceAttributes.SERVICE_VERSION, "${version}")
                               .put(ResourceAttributes.HOST_NAME, "${host}")
                               .put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "${environment}")
                               .build()
                              )
              )
  .build();

// 初始化OpenTelemetrySdk。
OpenTelemetrySdk telemetrySdk = OpenTelemetrySdk.builder()
  .setTracerProvider(tracerProvider)
  .build(); // 如果通过build() 方法生成实例,则需要根据实际情况判断是否需要全局持有telemetrySdk。

// 您也可以初始化一个全局的OpenTelemetrySdk。
// OpenTelemetrySdk telemetrySdk = OpenTelemetrySdk.builder()
//    .setTracerProvider(tracerProvider)
//    .buildAndRegisterGlobal(); // 后续可通过GlobalOpenTelemetry.get()或GlobalOpenTelemetry.getTracer()进行访问。

变量

说明

示例

${endpoint}

服务入口是访问一个Project及其内部数据的URL,日志服务提供私网域名和公网域名。更多信息,请参见服务入口

  • Port:网络端口,固定为10010。

cn-hangzhou.log.aliyuncs.com:10010

${project}

日志服务Project名称,更多信息,请参见管理Project

test-project

${instance}

Trace服务实例ID。更多信息,请参见创建Trace实例

test-traces

${access-key-id}

AccessKey ID用于标识用户,更多信息,请参见访问密钥

建议您遵循最小化原则,按需授予RAM用户必要的权限。关于授权的具体操作,请参见创建RAM用户及授权RAM自定义授权示例

${access-key-secret}

AccessKey Secret是用户用于加密签名字符串和日志服务用来验证签名字符串的密钥,必须保密。

${service.namespace}

服务归属的命名空间。

order

${service}

服务名,根据您的实际场景配置。

payment

${version}

服务版本号,建议按照va.b.c格式定义。

v1.0.0

${host}

主机名。

localhost

${environment}

部署环境,例如测试环境、生产环境。

pre

步骤四:使用SDK

创建Tracer

建议根据不同的业务场景创建Tracer。创建Tracer时,需传入instrumentation scope name,利于按照scope区分不同的Trace数据。

Tracer tracer = telemetrySdk.getTracer("otel application", "1.0.0");

创建基本Span

Span代表了事务中的操作。每个Span都封装了操作名称、起止时间戳、属性信息、事件信息和Context信息等。

final Span span = tracer.spanBuilder("root span").startSpan();
// do stuff
// ...
span.end();

创建嵌套Span

当您希望为嵌套操作关联Span时,OpenTelemetry支持在进程内和跨远程进程进行跟踪。例如针对methodA调用methodB ,您可以通过以下方式创建嵌套Span。

void methodA() {
  Span parentSpan = tracer.spanBuilder("operation A").startSpan();
  methodB(parentSpan);
  parentSpan.end();
}
void methodB(Span parentSpan) {
  Span childSpan = tracer.spanBuilder("operation B")
    .setParent(parentSpan)
    .startSpan();
  // do stuff
  childSpan.end();
}

OpenTemetry API还提供了一种自动化的方式来传播parentSpan。

void methodA() {
  Span parentSpan = tracer.spanBuilder("operation A").startSpan();
  try (Scope scope = parentSpan.makeCurrent()) {
    methodB();
  } finally {
    parentSpan.end();
  }
}

void methodB() {
  Span childSpan = tracer.spanBuilder("operation B").startSpan();
  // do stuff
  childSpan.end();
}

创建带属性的Span

您可以通过属性在Span上提供特定操作的上下文信息。例如执行结果、关联的其他业务信息等。

Span span = tracer.spanBuilder("GET /resource/catalog").setSpanKind(Span.Kind.CLIENT).startSpan();
span.setAttribute("http.method", "GET");
span.setAttribute("http.url", url.toString());

创建携带事件的Span

您可以通过携带多个事件的方式对Span进行注释。

span.addEvent("start");
// do stuff
// ...
span.addEvent("end");

// 也可以携带属性。
Attributes eventAttributes = Attributes.of(
  "key1", AttributeValue.stringAttributeValue("value1"),
  "key2", AttributeValue.longAttributeValue(111L)
);
span.addEvent("End Computation", eventAttributes);

创建带链接的Span

一个Span可以链接一个或多个因果相关的其他Span。

Link link1 = LinkData.create(parentSpan1.getContext());
Link link2 = LinkData.create(parentSpan2.getContext());
Span child = tracer.spanBuilder("child_with_link")
    .addLink(link1)
    .addLink(link2)
    .addLink(parentSpan3.getContext())
    .startSpan();

从远程进程中读取上下文信息的具体操作,请参见Context Propagation

给Span添加状态

Span包含StatusCode.UNSETStatusCode.OKStatusCode.ERROR三个状态,分别表示默认状态、成功状态、包含错误。

Span span = tracer.spanBuilder("operation name").startSpan();

try (Scope scope = span.makeCurrent()) {
  // do stuff
} catch (Throwable t) {
  span.setStatus(StatusCode.ERROR, "Something bad happened!");
  throw t;
} finally {
  span.end(); // 调用end()方法调用后,无法设置状态信息。
}

给Span添加异常信息

建议在捕获到异常信息之后,把异常信息添加到关联的Span中。同时,也建议同步更新Span状态。

Span span = tracer.spanBuilder("operation name").startSpan();
try (Scope scope = span.makeCurrent()) {
  // do stuff
} catch (Throwable throwable) {
  span.setStatus(StatusCode.ERROR, "Something bad happened!");
  span.recordException(throwable)
} finally {
  span.end();
}

传播上下文信息

OpenTelemetry提供了一种基于文本的方法,传播上下文信息。以下是使用HttpURLConnection发出HTTP请求的示例。

// 基于HttpURLConnection Header setter
TextMapSetter<HttpURLConnection> setter =
  new TextMapSetter<HttpURLConnection>() {
    @Override
    public void set(HttpURLConnection carrier, String key, String value) {
      // Insert the context as Header
      carrier.setRequestProperty(key, value);
    }
};

URL url = new URL("http://127.0.0.1:8088/resource/catalog");
Span httpSpan = tracer.spanBuilder("GET /resource/catalog").setSpanKind(SpanKind.CLIENT).startSpan();
try (Scope scope = httpSpan.makeCurrent()) {
  // 注入属性信息,记录HTTP请求的相关详情。
  httpSpan.setAttribute(SemanticAttributes.HTTP_METHOD, "GET");
  httpSpan.setAttribute(SemanticAttributes.HTTP_URL, url.toString());
  HttpURLConnection transportLayer = (HttpURLConnection) url.openConnection();
  // 把当前的Context信息注入到HTTP请求中。
  telemetrySdk.getPropagators().getTextMapPropagator().inject(Context.current(), transportLayer, setter);
  // do stuff
} finally {
  httpSpan.end();
}

目前,OpenTelemetry SDK支持按照W3C Trace Context标准传播上下文信息。更多信息,请参见W3CTraceContextPropagator类

关于OpenTelemetry SDK的更多信息,请参考官方文档

常见问题

OpenTelemetry Java SDK使用了Java 8+ API特性后,你的设备上可能会出现如下错误信息。

FATAL EXCEPTION: main
Process: xx.xx.xx.xx, PID: 2810
java.lang.NoClassDefFoundError: io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder$$ExternalSyntheticLambda0
	at io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder.<init>(OtlpGrpcSpanExporterBuilder.java:38)
	at io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter.builder(OtlpGrpcSpanExporter.java:39)

针对上述问题,如果你的Android应用支持API 26以下的设备,则您需要打开coreLibraryDesugaringEnabled开关。具体操作,请参见 core library desugaring

打开coreLibraryDesugaringEnabled开关后,你的Android应用最低可以支持到API 21的设备。

说明

目前OpenTelemetry Java SDK不支持API 21以下的设备。