库没有现成的自动插桩时,业务代码可以显式创建 Span;如果不便改业务代码,也可以让 Agent 扩展匹配库的方法。困难不在调用 spanBuilder,而在确定扩展何时被找到、增强了哪个版本、辅助类由哪个加载器提供,以及异常或禁用时是否留下重复或未结束的 Span。

本文固定 JDK 21.0.12、Java Agent 1.31.0。发行 Agent 的 SHA-256 是 e866de5fa4e4c2c7d076072798fb9ca65b679d9c6be7792b37d4c94e2bc686f7。扩展的编译 API 是 io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:1.31.0-alpha(SHA-256 afc55fe952fa10fbad2836993bacc2a01cf02e1d50d53a6db9595fb97b2a37aa),Byte Buddy 编译依赖版本与 SHA-256 见 writing-plans/opentelemetry-java/VERSIONS.md;扩展 JAR 不打包第二套 SDK,测试子进程的原有 SDK classpath 也不另行初始化全局 SDK。扩展 JAR 是 examples/opentelemetry-java/extension-lab/ 的构建结果,不与 library appender 混用。

从启动选项到方法 Advice

固定 Java instrumentation 源码 SHA:97d87f3f3b2bc61aa396d71a98387a1e857618af;Java SDK SHA:c25c0a0ee0da01ab2f74ba83052d1c249ed57020。以下行号在前者或注明的 SDK 快照中定位。

环节 固定源码 约束
扩展加载 ExtensionClassLoader.java L54–69、InstrumentationLoader.java L28–55 otel.javaagent.extensions 给出扩展 JAR;META-INF/services/io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule 指向模块类。未被加载的模块不会产生 Advice。
模块开关与注入 InstrumentationModuleInstaller.java L56–73、InstrumentationModuleInstaller.java L111–149 先检查该模块是否启用;经典注入路径选择类型、muzzle matcher、HelperInjector,然后应用类型转换。仅看到扩展 JAR 存在不足以证明方法被增强。
自定义匹配 TypeInstrumentation.java L21–60 类型匹配 CustomLibrary;方法仅匹配 execute(String)。签名变化时不应误增强其他重载。
Helper 生命周期 InstrumentationModule.java L130–160 getAdditionalHelperClassNames() 显式声明 Helper;Advice 引用它启动 Span,Helper 使用 Agent 已安装的 GlobalOpenTelemetry。全局实例为进程级共享入口,不需要在业务侧重新 set;SDK GlobalOpenTelemetry.java L64–87 是对应入口。

上游固定快照的 examples/extension/README.md 解释扩展参数;DemoServlet3InstrumentationModule.java L33–45 给出服务模块示例。上游 ExtensionClassLoaderTest.java L24–65 对路径解析作测试,HelperInjectionTest.groovy L25–49 检查类加载器注入;这些是静态阅读,没有在本机运行上游测试。

同一类名、两种签名

Lab25Test 通过完整 JDK 21 的 JavaCompiler 在隔离的测试临时目录生成两版 org.example.otel.compat.CustomLibrary。V1 暴露 execute(String),V2 改为 execute(int)。每个场景启动新的 JVM,并挂同一 Agent 和自定义扩展 JAR。扩展的 CustomLibraryInstrumentation 匹配类名与 execute(String);入口 Advice 调用 CustomLibraryHelper.start(),持有 Scope;退出 Advice 即使抛出业务异常,也先关闭 Scope,再记录异常、设置 ERROR、结束 Span。@Advice.OnMethodExit(onThrowable=Throwable.class) 不等于吞掉业务异常:探针在异常场景确实捕获到约定的 IllegalArgumentException。

工程目录执行:

1
JAVA_HOME=/tmp/otel-20260930/jdk-extract/usr/lib/jvm/java-21-openjdk-amd64 ./mvnw -q -pl extension-lab,sdk-labs -Dtest=Lab25Test -Dsurefire.failIfNoSpecifiedTests=false package

-pl extension-lab,sdk-labs 先打扩展 JAR;因为 -Dtest 指定了 SDK 模块中的测试,-Dsurefire.failIfNoSpecifiedTests=false 避免无此测试的扩展模块误报失败。退出码 0;examples/opentelemetry-java/evidence/25/RUN.md 记录原始 stdout:

1
2
3
4
5
6
LAB25 child=LAB25 mode=normal calls=2 exit=0
LAB25 child=LAB25 expectedLibraryException=true exit=0
LAB25 child=LAB25 mode=error calls=1 exit=0
LAB25 child=LAB25 mode=incompatible calls=1 exit=0
LAB25 child=LAB25 mode=disabled calls=1 exit=0
LAB25 compatible=2 error=1,event=1 incompatible=0 disabled=0

JUnit 解码父进程教学 OTLP/HTTP protobuf 接收端的 CustomLibrary.execute Span:V1 两次正常调用恰有两条、无重复;V1 异常调用恰有一条 ERROR Span 和一个异常 event;V2 方法签名不匹配,业务正常返回,未找到该自定义 Span;V1 在 OTEL_INSTRUMENTATION_LAB25_CUSTOM_LIBRARY_ENABLED=false 下也未找到该 Span。四个子 JVM 都在有期限的 waitFor(40s) 内正常退出;需要接收的两组还等 CountDownLatch.await(5s)。扩展的成功和失败路径没有另设应用级全局 SDK。

匹配失败不是兼容性承诺

V2 是测试临时编译的构造性不兼容版本。方法参数变化使 Advice 不再匹配,它不能替代对某个公开库的发布版本矩阵、muzzle 生成检查、重载/继承关系和真实依赖冲突的验证;本工程并未运行上游 Gradle 的 muzzle generation/check。Agent 层还有 disabled 分支、匹配分支与 Helper 注入的失败边界,不能从“业务返回正常”独自判断是哪一个在起作用。本地教学端点也不是 Collector;这里只证明 OTLP protobuf 到测试进程,并无持久化或后端检索。

  1. 修改 Lab25Test 的 V2 为同时包含 execute(int) 和 execute(String),再断言仅后者产生自定义 Span。继承子类版本也应单独对照类型 matcher;若父类仍被增强,要说明转换发生在何处。
  2. 为什么把 extension-lab 依赖改成运行时整包并把一套 SDK 打入扩展 JAR 会改变类加载与全局实例风险?根据 InstrumentationModuleInstaller 的开关、helper、muzzle 分支,给出至少两条“扩展 JAR 存在但没有 Span”的排查路径。

参考资料:Java instrumentation extension 示例(固定 SHA)、扩展 API 与 tooling(固定 SHA)、SDK 全局入口(固定 SHA)、examples/opentelemetry-java/evidence/25/RUN.md。

导航:23 Instrumenter · 24 Servlet 与 JDBC · 当前篇:25 自定义 Agent 扩展。