OpenTelemetry Instrumentation 与 Java Agent

本文最后更新于:2024年7月13日 下午

在之前的 blog: 分布式可观测性,链路追踪与OpenTelemetry 主要介绍了分布式链路追踪的概念和 OpenTelemetry (下文以 Otel 简称) 的起源。从本节开始,我会分享一些 OpenTelemetry 的基本概念,语言主要基于 Java(当然,Otel 本身的 SDK 支持多种语言,可以在 Otel Doc: Language APIs & SDKs 查看)。

本文介绍的内容主要涉及 OpenTelemetry[1] 的 Instrumentation 概念,以及如何将已有的代码接入 OpenTelemetry 以获得可观测性。

1 - OpenTelemetry Instrumentation Startup

如果你想把你的一个项目接入 OpenTelemetry,肯定要接触一个概念:”Instrumentation”,这是一个少有的我感觉没什么准确的一个中文词汇能表达出的意思,OpenTelemetry 将其翻译成「仪表化」,但我感觉仍然不太恰当。

这个词实际上表达的是:

向应用程序中注入跟踪和监控代码的过程,目的是收集有关应用程序运行时性能和行为的监控数据 [2]

OpenTelemetry 对 Instrumentation 主要提供了两种方案:

2 - Java Agent

下文都以 Java 为例,介绍 Otel 如何利用 Java Agent 机制实现 Auto Instrumentation。该部分主要参考官网文档:Doc: Java Agent

本章首先介绍 Java Agent 机制。

2.1 - 什么是 Java Agent

Java代理是一种特殊类型的类,通过使用Java Instrumentation API ,它可以拦截运行在JVM上的应用程序,修改它们的字节码。 (注意:区别于 Otel Instrumentation,虽然用的是一个词,表达的也都是「代码注入」这个概念)

Java代理并不是一项新技术,相反,它们从Java 5开始就存在了。但是即使过了这么长时间,还是有许多开发者对这个概念鲜有接触。

通过 Java Agent,可以轻松实现如下几类应用场景:

  • IDE 的调试功能,例如 Eclipse、IntelliJ IDEA;
  • 热部署功能,例如 JRebel、XRebel、spring-loaded;
  • 各种线上诊断工具,例如 Btrace、Greys,国内阿里的 Arthas;
  • 各种性能分析工具,例如 Visual VM、JConsole 等;
  • 全链路性能检测工具,例如 OpenTelemetry、Skywalking、Pinpoint 等。

2.2 - Java Agent 的运行机制

Java Agent 主要可以通过两种方式启动:Premain AgentAgentmain,分别在 JVM 启动前和启动后加载 Agent,达到的都是动态修改字节码的效果:

  • Premain Agent

Java Agent 在 Java 程序运行前:在Main方法执行之前,通过一个叫 premain方法来执行。

启动时需要在目标程序的启动参数中添加 -javaagent参数,Java Agent 内部通过注册 ClassFileTransformer ,这个转化器在 Java 程序 Main方法前加了一层拦截器。在类加载之前,完成对字节码修改。

1
java -javaagent:/path/to/<your-agent>.jar YourMain

其工作流程大致如下:[4]

  • Agentmain

该模式和 Premain 模式相似,主要区别在进行字节码增强前,拦截入口不同。一个叫Premain,一个叫Agentmain 。 运行时加载,当前 JVM 进程已经启动了。这时借助另一个 JVM 进程通信,调用 Attach API 再把 Agent 启动起来。后面的字节码修改和重加载的过程那就是一样的。

3 - Otel Java Auto Instrumentation

Otel Java Auto Instrumentation 主要通过 Java agent 机制实现。

Java Auto Instrumentation 实现都在 github: https://github.com/open-telemetry/opentelemetry-java-instrumentation/ 这个仓库中。

3.1 - 快速开始

可以参考 doc 快速在一个 java 项目中进行 instrumentation,只需要如下几步:

  1. Download opentelemetry-javaagent.jar from Releases of the opentelemetry-java-instrumentation
  2. 通过 java -javaagent:path/to/opentelemetry-javaagent.jar -Dotel.service.name=your-service-name -jar myapp.jar 启动你的 project app myapp.jar

通过这两步,就已经可以在这些 otel 支持的 library 上运行 opentelemetry 了,可以通过在环境变量或者 properties 中修改 这些配置 来进行更多自定义配置,包括但不限于:

  • 数据导出配置:导出数据的目标类型,如 Jaeger、Zipkin、Prometheus等。
  • 日志输出模式
  • 采样策略
  • 等等

下面通过一个我自己的示例来快速了解一下如何利用 Otel 提供的 @WithSpan@SpanAttribute Annotation 进行 Auto Instrumentation 的 Tracing。[6]

3.2 - Code 示例

首先创建一个 java 项目,并导入 opentelemetry-instrumentation-annotations 这个依赖,可以在 这里 查看 Maven 或者 Gradle 的导入方式。

这里以 gradle 为例,添加如下 dependencies 即可:

1
implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.4.0")

全部的 build.gradle.kts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
plugins {
java
id("com.github.johnrengelman.shadow") version "7.1.2"
}

group = "org.example"
version = "1.0"

repositories {
mavenCentral()
}

dependencies {
implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.4.0")
}

tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}

tasks.shadowJar {
mergeServiceFiles()
manifest {
attributes(
"Main-Class" to "org.example.Main"
)
}
}

tasks.build {
dependsOn(tasks.shadowJar)
}

然后写一个简单的测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.example;

import io.opentelemetry.instrumentation.annotations.SpanAttribute;
import io.opentelemetry.instrumentation.annotations.WithSpan;

import java.util.logging.Logger;

public class Main {
private static final Logger logger = Logger.getLogger(Main.class.getName());

@WithSpan
public static void main(String[] args) {
sampleMethodA("test");
sampleMethodB();
}

@WithSpan
public static void sampleMethodA(@SpanAttribute String arg) {
System.out.println("Sample method A, arg = " + arg);
sampleMethodB();
}

@WithSpan
public static void sampleMethodB() {
System.out.println("Sample method B");
}
}

使用 -Dotel.traces.exporter=logging-otlp 将输出指定为 otlp 的 json 格式(otlp 是 opentelemetry 定义的传输协议),通过 log 输出在控制台,然后把 logs 和 metrics 的 exporter 禁用后启动:

1
java -javaagent:opentelemetry-javaagent.jar -Dotel.traces.exporter=logging-otlp -Dotel.logs.exporter=none -Dotel.metrics.exporter=none -jar ./build/libs/otel-simple-demo-1.0-all.jar

也可以通过环境变量来启动:

1
2
3
4
5
JAVA_TOOL_OPTIONS='-javaagent:./opentelemetry-javaagent.jar' \
OTEL_TRACES_EXPORTER='logging-otlp' \
OTEL_METRICS_EXPORTER=none \
OTEL_LOGS_EXPORTER=none \
java -jar ./build/libs/otel-simple-demo-1.0-all.jar

输出如下:

1
2
3
4
5
6
Picked up JAVA_TOOL_OPTIONS: -javaagent:./opentelemetry-javaagent.jar
[otel.javaagent 2024-06-17 01:16:00:167 +0800] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 2.4.0
Sample method A, arg = test
Sample method B
Sample method B
[otel.javaagent 2024-06-17 01:16:06:098 +0800] [BatchSpanProcessor_WorkerThread-1] INFO io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingSpanExporter - {"resource":{"attributes":[{"key":"host.arch","value":{"stringValue":"aarch64"}},{"key":"host.name","value":{"stringValue":"sxz-mbp.local"}},{"key":"os.description","value":{"stringValue":"Mac OS X 14.4.1"}},{"key":"os.type","value":{"stringValue":"darwin"}},{"key":"process.command_line","value":{"stringValue":"/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/bin/java -javaagent:./opentelemetry-javaagent.jar -jar ./build/libs/otel-simple-demo-1.0-all.jar"}},{"key":"process.executable.path","value":{"stringValue":"/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/bin/java"}},{"key":"process.pid","value":{"intValue":"65608"}},{"key":"process.runtime.description","value":{"stringValue":"Azul Systems, Inc. OpenJDK 64-Bit Server VM 25.372-b07"}},{"key":"process.runtime.name","value":{"stringValue":"OpenJDK Runtime Environment"}},{"key":"process.runtime.version","value":{"stringValue":"1.8.0_372-b07"}},{"key":"service.instance.id","value":{"stringValue":"99dd9ca6-3d76-4aef-86ac-017e16881fbc"}},{"key":"service.name","value":{"stringValue":"otel-simple-demo-1.0-all"}},{"key":"telemetry.distro.name","value":{"stringValue":"opentelemetry-java-instrumentation"}},{"key":"telemetry.distro.version","value":{"stringValue":"2.4.0"}},{"key":"telemetry.sdk.language","value":{"stringValue":"java"}},{"key":"telemetry.sdk.name","value":{"stringValue":"opentelemetry"}},{"key":"telemetry.sdk.version","value":{"stringValue":"1.38.0"}}]},"scopeSpans":[{"scope":{"name":"io.opentelemetry.opentelemetry-instrumentation-annotations-1.16","version":"2.4.0-alpha","attributes":[]},"spans":[{"traceId":"641daaa0e701906be9ca743e4526f471","spanId":"9aed7d245875bdea","parentSpanId":"7159ba878669026b","name":"Main.sampleMethodB","kind":1,"startTimeUnixNano":"1718558166060339167","endTimeUnixNano":"1718558166062393375","attributes":[{"key":"code.namespace","value":{"stringValue":"org.example.Main"}},{"key":"thread.id","value":{"intValue":"1"}},{"key":"code.function","value":{"stringValue":"sampleMethodB"}},{"key":"thread.name","value":{"stringValue":"main"}}],"events":[],"links":[],"status":{},"flags":257},{"traceId":"641daaa0e701906be9ca743e4526f471","spanId":"7159ba878669026b","parentSpanId":"5316ba20f2a4144d","name":"Main.sampleMethodA","kind":1,"startTimeUnixNano":"1718558166060174750","endTimeUnixNano":"1718558166062439667","attributes":[{"key":"code.namespace","value":{"stringValue":"org.example.Main"}},{"key":"thread.id","value":{"intValue":"1"}},{"key":"code.function","value":{"stringValue":"sampleMethodA"}},{"key":"thread.name","value":{"stringValue":"main"}}],"events":[],"links":[],"status":{},"flags":257},{"traceId":"641daaa0e701906be9ca743e4526f471","spanId":"4eee1ecf0422c09e","parentSpanId":"5316ba20f2a4144d","name":"Main.sampleMethodB","kind":1,"startTimeUnixNano":"1718558166062502208","endTimeUnixNano":"1718558166062544292","attributes":[{"key":"code.namespace","value":{"stringValue":"org.example.Main"}},{"key":"thread.id","value":{"intValue":"1"}},{"key":"code.function","value":{"stringValue":"sampleMethodB"}},{"key":"thread.name","value":{"stringValue":"main"}}],"events":[],"links":[],"status":{},"flags":257},{"traceId":"641daaa0e701906be9ca743e4526f471","spanId":"5316ba20f2a4144d","name":"Main.main","kind":1,"startTimeUnixNano":"1718558166058000000","endTimeUnixNano":"1718558166062553750","attributes":[{"key":"code.namespace","value":{"stringValue":"org.example.Main"}},{"key":"thread.id","value":{"intValue":"1"}},{"key":"code.function","value":{"stringValue":"main"}},{"key":"thread.name","value":{"stringValue":"main"}}],"events":[],"links":[],"status":{},"flags":257}]}],"schemaUrl":"https://opentelemetry.io/schemas/1.24.0"}

3.3 - Span 的结构分析

输出的 json 实际上的结构可以在 trace.proto 中查看: https://github.com/open-telemetry/opentelemetry-proto/blob/v1.3.0/opentelemetry/proto/trace/v1/trace.proto

  • 比如 main 方法对应的 span 对应包含如下信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"traceId": "641daaa0e701906be9ca743e4526f471",
"spanId": "5316ba20f2a4144d",
"name": "Main.main",
"kind": 1,
"startTimeUnixNano": "1718558166058000000",
"endTimeUnixNano": "1718558166062553750",
"attributes": [
{
"key": "code.namespace",
"value": {
"stringValue": "org.example.Main"
}
},
{
"key": "thread.id",
"value": {
"intValue": "1"
}
},
{
"key": "code.function",
"value": {
"stringValue": "main"
}
},
{
"key": "thread.name",
"value": {
"stringValue": "main"
}
}
],
"events": [],
"links": [],
"status": {},
"flags": 257
}
  • 对于非 root span,还会额外包含一个 parentSpanId 的信息, 以 sampleMethodB 为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
"traceId": "641daaa0e701906be9ca743e4526f471",
"spanId": "4eee1ecf0422c09e",
"parentSpanId": "5316ba20f2a4144d",
"name": "Main.sampleMethodB",
"kind": 1,
"startTimeUnixNano": "1718558166062502208",
"endTimeUnixNano": "1718558166062544292",
"attributes": [
{
"key": "code.namespace",
"value": {
"stringValue": "org.example.Main"
}
},
{
"key": "thread.id",
"value": {
"intValue": "1"
}
},
{
"key": "code.function",
"value": {
"stringValue": "sampleMethodB"
}
},
{
"key": "thread.name",
"value": {
"stringValue": "main"
}
}
],
"events": [],
"links": [],
"status": {},
"flags": 257
},

关于其中字段的详细解释可以参考:https://opentelemetry.io/docs/concepts/signals/traces/

除了在上一篇文章提到的 Tracing 通用的 spanId, traceId 这些通用的 Span Attributes 外, OpenTelemetry 还提供了两种新的称为 Span Events 和 Span Links 的东西:

  • Span Events: 用于标记一个 Span 内部的一个关键事件, 例如:
1
2
3
span.addEvent("Init");
...
span.addEvent("End");

也可以在 events 中添加 Attributes:

1
2
3
4
5
Attributes eventAttributes = Attributes.of(
AttributeKey.stringKey("key"), "value",
AttributeKey.longKey("result"), 0L);

span.addEvent("End Computation", eventAttributes);
  • Span Links: 用于标记一个 Span 与其他 Span 之间的关联关系,例如:
1
2
3
4
5
6
Span child = tracer.spanBuilder("childWithLink")
.addLink(parentSpan1.getSpanContext())
.addLink(parentSpan2.getSpanContext())
.addLink(parentSpan3.getSpanContext())
.addLink(remoteSpanContext)
.startSpan();

这些 API 可以简单的在 Agent 的基础上使用,例如我们修改上面的示例代码:

1
2
3
4
5
6
7
8
9
@WithSpan
public static void main(String[] args) {
Span currentSpan = Span.current();
currentSpan.setAttribute("custom-attribute", "custom-attribute-value");
currentSpan.addEvent("custom-event");
currentSpan.addLink(currentSpan.getSpanContext());
sampleMethodA("test");
sampleMethodB();
}

然后重新运行,就可以得到如下的 payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
{
"traceId": "f23dd7607ebec83961036bd1f5aadbfb",
"spanId": "8f0e5a157098c520",
"name": "Main.main",
"kind": 1,
"startTimeUnixNano": "1718608407274000000",
"endTimeUnixNano": "1718608407319309792",
"attributes": [
{
"key": "code.namespace",
"value": {
"stringValue": "org.example.Main"
}
},
{
"key": "thread.id",
"value": {
"intValue": "1"
}
},
{
"key": "code.function",
"value": {
"stringValue": "main"
}
},
{
"key": "custom-attribute",
"value": {
"stringValue": "custom-attribute-value"
}
},
{
"key": "thread.name",
"value": {
"stringValue": "main"
}
}
],
"events": [
{
"timeUnixNano": "1718608407309075542",
"name": "custom-event",
"attributes": [

]
}
],
"links": [
{
"traceId": "f23dd7607ebec83961036bd1f5aadbfb",
"spanId": "8f0e5a157098c520",
"attributes": [

],
"flags": 257
}
],
"status": {

},
"flags": 257
}

可以看到里面已经包含了刚才添加的自定义 attributes, events 和 links 信息。

Conclusion

这一章还处于比较浅的阶段,后面会再写写 otel 一些内部 code 结构的分析,以及如何通过 agent extensions 等机制来自定义 agent 行为等。

Reference


OpenTelemetry Instrumentation 与 Java Agent
https://moreality.net/posts/23154/
作者
Moreality
发布于
2024年6月15日
许可协议