同一份应用,为何有无 -javaagent 的 Span 不同?

一个只调用 GlobalOpenTelemetry.get() 的程序,不装 Agent 时可以正常运行,却只得到 no-op Span。增加 Agent 后,不改业务代码,Span 变为 recording。问题不在“额外创建一个全局 SDK”:究竟是谁在应用 main 前安装它,又怎样隔离 Agent 自己的类?本文先看启动链,再用同一程序、两个独立 JVM核对应用侧可观察结果。

时点 所在边界 工作
premain JVM Instrumentation 找到发行 JAR,追加 bootstrap 搜索路径
初始化 bootstrap → AgentClassLoader 从内部 inst 加载 tooling 并启动
配置 Agent tooling 自动配置 SDK 为全局值、安装字节码 transformer
main 应用类加载器 通过 API 读取已装好的全局实现

固定源码中的启动、默认值和失败分支

本篇冻结 SDK tag v1.31.0 的完整 SHA c25c0a0ee0da01ab2f74ba83052d1c249ed57020、instrumentation tag v1.31.0 的完整 SHA 97d87f3f3b2bc61aa396d71a98387a1e857618af。以下调用链是静态阅读,本地实验只验证表中应用侧一列,不等于运行了上游测试。

OpenTelemetryAgent.premain L44 将 Instrumentation 交给 startAgent;L52–62 在失败时捕获 Throwable 并打印,而不是保证 JVM 启动失败。L65–97 根据资源定位 JAR、校验清单并调用 appendToBootstrapClassLoaderSearch。AgentInitializer L30 用非空 loader 检查要求自身由 bootstrap 加载,L50–53 从内部 inst 建 AgentClassLoader 并启动;L128–133 的延迟启动仅针对早期 Oracle 8 判断,不能套用到本次 JDK 21。内部 JAR 的读取路径在 AgentClassLoader L85–107。

AgentStarterImpl.start L69–113 负责日志初始化、安装与失败回调;AgentInstaller L97–124 的启用默认值为 true,非 noop 分支先安装 SDK、后装 transformer(L190–192)。OpenTelemetryInstaller L24–43 调用 setResultAsGlobal(),并把 logs exporter 的默认值设为 none;本篇则显式关闭三类 exporter。SDK 的 GlobalOpenTelemetry.set L101–113 拒绝第二次注册,不能在 Agent 之外再把另一套 SDK 注册到同一全局 API。上游 OpenTelemetryInstallerTest L25–40 检查全局对象与日志默认值;没有在本机运行上游测试。

本地两个 JVM 的对照

选用 Maven Central io.opentelemetry.javaagent:opentelemetry-javaagent:1.31.0,发行 JAR SHA-256 为 e866de5fa4e4c2c7d076072798fb9ca65b679d9c6be7792b37d4c94e2bc686f7;manifest Premain-Class 指向 OpenTelemetryAgent。JDK 21.0.12 的实测启动结果,不能外推为所有 JDK 版本支持。此 JAR 与 19 篇的 Logback library appender 不是同一组件。下载及校验值见版本记录;本地运行时 JAR 位于 /tmp/otel-21/opentelemetry-javaagent-1.31.0.jar,不在仓库。

在 examples/opentelemetry-java/,用有 javac 的完整 JDK 21 运行:

1
JAVA_HOME=/tmp/otel-20260930/jdk-extract/usr/lib/jvm/java-21-openjdk-amd64 ./mvnw -q -pl sdk-labs -Dtest=Lab21Test test

退出码 0。Lab21Test 以有期限的 waitFor 启动两个子 JVM,AgentProbe 不自行创建 SDK,也不调用 GlobalOpenTelemetry.set。摘录实际输出(完整行保存在 examples/opentelemetry-java/evidence/21/RUN.md):

1
2
LAB21 noAgent=PROBE global=io.opentelemetry.api.DefaultOpenTelemetry ... recording=false valid=false currentValid=false
LAB21 withAgent=PROBE global=io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_27.ApplicationOpenTelemetry127 ... recording=true valid=true currentValid=false

两个进程的 AppClassLoader@... 地址不具有可比性;应用 API 与全局代理在应用 loader 可见,不能由此声称观测了 tooling 或 bootstrap loader 的运行对象。currentValid=false 也不与 recording 矛盾:创建 Span 不等于 makeCurrent()。exporter 全关闭,本篇不验证网络或 Collector。源码中的异常打印意味着“JVM 退出 0”本身不够,必须同时检查 recording 与错误行。

适用边界、误解与练习

工程原则是:初始化归属只能有一个,且须把加载隔离与全局访问区别开。本实验验证干净示例中 Agent 安装的全局 API;不证明在带应用自建 SDK、其他 Agent 或复杂 classloader 的生产环境仍保持同样对象身份。更不意味着任意方法自动生成 Span:字节码匹配留待下一篇。两个练习:

  1. 修改 AgentProbe,在 Span 上调用 makeCurrent(),断言 Scope 内外 Span.current() 的有效性;与本次 currentValid=false 比较,说明是谁管理 Scope。
  2. 构造禁用 Agent 的独立进程对照(OTEL_JAVAAGENT_ENABLED=false),预测 AgentInstaller L97–104 的分支,并分别检查进程退出、错误行和 recording;不要只凭退出码判定安装成功。

参考资料:instrumentation 源码与上游测试(固定 SHA)、SDK v1.31.0、Agent JAR(Maven Central)。

导航:20 三信号字段治理 · 当前篇:21 Agent 启动。