深入 OpenTelemetry 21:premain 如何把 Agent 接进 JVM
同一份应用,为何有无 -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 | |
退出码 0。Lab21Test 以有期限的 waitFor 启动两个子 JVM,AgentProbe 不自行创建 SDK,也不调用 GlobalOpenTelemetry.set。摘录实际输出(完整行保存在 examples/opentelemetry-java/evidence/21/RUN.md):
1 | |
两个进程的 AppClassLoader@... 地址不具有可比性;应用 API 与全局代理在应用 loader 可见,不能由此声称观测了 tooling 或 bootstrap loader 的运行对象。currentValid=false 也不与 recording 矛盾:创建 Span 不等于 makeCurrent()。exporter 全关闭,本篇不验证网络或 Collector。源码中的异常打印意味着“JVM 退出 0”本身不够,必须同时检查 recording 与错误行。
适用边界、误解与练习
工程原则是:初始化归属只能有一个,且须把加载隔离与全局访问区别开。本实验验证干净示例中 Agent 安装的全局 API;不证明在带应用自建 SDK、其他 Agent 或复杂 classloader 的生产环境仍保持同样对象身份。更不意味着任意方法自动生成 Span:字节码匹配留待下一篇。两个练习:
- 修改
AgentProbe,在 Span 上调用makeCurrent(),断言 Scope 内外Span.current()的有效性;与本次currentValid=false比较,说明是谁管理 Scope。 - 构造禁用 Agent 的独立进程对照(
OTEL_JAVAAGENT_ENABLED=false),预测AgentInstallerL97–104 的分支,并分别检查进程退出、错误行和 recording;不要只凭退出码判定安装成功。
参考资料:instrumentation 源码与上游测试(固定 SHA)、SDK v1.31.0、Agent JAR(Maven Central)。
导航:20 三信号字段治理 · 当前篇:21 Agent 启动。
