一次请求在入口 Servlet 里查询数据库,再请求另一个 HTTP 端点。挂上 Agent 后看到几个 Span,如何判断它们真的是同一条链,而不是恰好同一批送出的独立事件?只检查 Span 数量不够:还要核 traceId、parentSpanId、SpanKind 和 SQL 属性,并且区分成功请求与错误请求。

本文实验采用完整 JDK 21.0.12、Java Agent 1.31.0、嵌入式 Tomcat 10.1.24 和 H2 2.2.224。Agent 的发行 JAR SHA-256 为 e866de5fa4e4c2c7d076072798fb9ca65b679d9c6be7792b37d4c94e2bc686f7;Tomcat、H2 发行物的 SHA-256 和取得方法见工程的 writing-plans/opentelemetry-java/VERSIONS.md。应用没有初始化另一套全局 SDK。子进程由 Agent 负责采集,JUnit 进程只负责启动请求并解码测试 HTTP 端点收到的 OTLP Trace。

入口到数据库的状态模型

1
2
3
4
外部请求 GET /lookup → Tomcat SERVER
├→ H2 prepared execute → JDBC CLIENT
└→ JDK HttpURLConnection → HTTP CLIENT
└→ Tomcat SERVER GET /backend

固定源码版本分别是 SDK c25c0a0ee0da01ab2f74ba83052d1c249ed57020 和 instrumentation 97d87f3f3b2bc61aa396d71a98387a1e857618af。下面的行号只针对这两份快照。

关口 实现依据 运行时含义
装载匹配 Tomcat10InstrumentationModule.java L21–37 需有 Jakarta Servlet 类;本次 Tomcat 10.1/JDK 21 的组合以实际测试确认,不外推所有版本。
SERVER 的 Scope Tomcat10ServerHandlerAdvice.java L21–49、TomcatHelper.java L31–59 读取当前 context、检查 shouldStart、开启 Scope;出口关闭 Scope,结束时还要考虑异步 Servlet 是否由其他出口处理。
Servlet 补充信息 JakartaServletServiceAdvice.java L56–81 复用已附着的 server context、更新路由信息;嵌套 servlet/filter 不一定新建 SERVER。
JDBC 初始化 DriverInstrumentation.java L49–62、ConnectionInstrumentation.java L35–52 从 Driver 的 URL 提取数据库信息;成功 prepare 后将 SQL 关联到 PreparedStatement,连接失败/空返回不附加信息。
JDBC 执行与失败 PreparedStatementInstrumentation.java L49–101 execute*() 入口检查 SQL 是否已附着、递归 call depth、shouldStart;退出时恢复 Scope,向 Instrumenter 传 Throwable。prepare 阶段失败不会经过 execute 的 advice。

DbRequest 中附着的原始 SQL 会进入 JdbcAttributesGetter.java L43–47;不能从“有 JDBC 插桩”推导任意 SQL 都安全。上游 JdbcInstrumentationTest.groovy L176–212 检查 H2 查询字段,TomcatHandlerTest.groovy L34–68 设定 Tomcat 服务端对照;这里是静态阅读,没有声称本机运行上游测试。

一次正常请求和一次故障请求

累计工程的 AgentServletProbe 让 /lookup 以 SELECT ? AS ITEM 执行 H2 查询;参数值为 private-customer-42,用于检查占位符是否把绑定值隔离在 db.statement 之外。随后 Servlet 用 JDK HTTP 客户端请求同一个 Tomcat 的 /backend。第二次请求加 ?fail=1,先执行同一条预编译查询,再 prepare 不存在的表,返回 HTTP 500。JUnit 父进程不挂 Agent,通过有期限的就绪信号、HTTP 超时、进程退出及接收闩锁收敛结果,不用任意 sleep。

在 examples/opentelemetry-java/ 执行(完整工具链和 Agent 的取得方法见工程 README):

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

本机退出码 0;原始 stdout(examples/opentelemetry-java/evidence/24/RUN.md)为:

1
LAB24 server=3 client=3 lookup=2 db=2 sql=SELECT_?_AS_ITEM dbParent=true backendParent=true boundValueAbsent=true errorHttp=500

断言检查两条数据库 CLIENT 的 db.system=h2、db.statement=SELECT ? AS ITEM,并与各自 /lookup SERVER 的 spanId/traceId 对上;/backend SERVER 也与上游 HTTP CLIENT 的 spanId/traceId 对上。两次成功执行查询共形成两条数据库 Span,而第二次的失败发生在 prepare,不应杜撰一个失败的 execute Span。500 对应的入口 SERVER 状态为 ERROR;额外内部 Span 不计入表中六条 SERVER/CLIENT。测试端点只证明接收到这些 protobuf 消息:不是 Collector、独立数据库服务或后端查询。

边界、误解与可修改的反例

占位符与绑定值缺席,是这条 SQL 的实测事实,不是自动日志脱敏承诺。把用户数据直接拼入 SQL,仍要检查映射与查询本身;URL 参数、其他日志 sink 和反常数据库驱动均不在本实验保证范围。500 也不代表 SQLException 必然有数据库错误 Span,失败阶段决定 Advice 是否运行;不同 Servlet 线程/异步派发还需单独检查 Scope 生命周期。Agent 记录、批处理、教学端点解析与真实 Collector 接收/转发/后端查询是四段不同的证据。

  1. 将 AgentServletProbe 的失败改为成功 prepare 后执行时报错(例如违反唯一键约束),在 Lab24Test 分别断言入口状态、数据库 ERROR 状态和父子关系;与本篇 prepare 失败时的数据库 Span 数作对照。
  2. 为什么不能用“六条 SERVER/CLIENT”的计数直接证明父子链?指出 parentSpanId 和 traceId 两列各自排除哪一种误判,并检查若移除 HTTP 客户端的传播增强,哪个断言先失败。

参考资料:Tomcat 10 instrumentation(固定 SHA)、JDBC instrumentation 与测试(固定 SHA)、Java SDK BatchSpanProcessor(固定 SHA)、examples/opentelemetry-java/evidence/24/RUN.md。

导航:21 Agent 启动 · 22 匹配与 Advice · 23 Instrumenter · 当前篇:24 Servlet 与 JDBC。