深入 OpenTelemetry 03 - Span 生命周期与异常状态
抛出了异常,Trace 却没有红色错误状态;或者方法提前返回,Span 始终没有进入导出器。这两种故障涉及不同的动作:记录异常、设置状态、结束 Span 并非同一个 API 的副作用。沿订单查询的三条控制流逐一验证。
三个动作不要合并
1 | |
recordException(exception) 生成名为 exception 的事件;setStatus(ERROR) 修改状态;end() 记录结束时间并触发 processor 的 onEnd。三者彼此独立。正常结束的 Span 状态并非必须显式设置 OK;本篇实验中没有设置时为 UNSET,这不等于业务错误。
上游从哪里做了这些事
公开 tracer.spanBuilder(...).startSpan() 进入内部 SdkSpanBuilder.startSpan,采样后才可能构造记录用的 SdkSpan。SdkSpan.startSpan 调用处理器 onStart,调用者是本线程内的 builder;end 也在调用它的线程触发 onEnd。本篇用 Simple processor,尚未出现 Batch 工作线程。采样 DROP 时 startSpan 不创建记录用 Span;这不是一次有事件而被 exporter 漏掉的数据。
SdkSpan.recordException 添加事件,但不调用 setStatus;状态由 SdkSpan.setStatus 单独修改。endInternal 在锁内检查是否已经结束,更新 endEpochNanos 与 hasEnded,随后在锁外调用 onEnd;再次调用 end 走已结束分支,不重新交付。由此可知“已抛出异常”和“Span 被标记 ERROR”之间必须存在一条明确的语义处理路径。上游 SdkSpanTest 包含异常事件测试,结束后的不可变数据测试见该文件 第 270 行附近。
正常、异常、提前返回
教学实验 Lab03Test 使用 try / catch / finally:正常路径直接返回;异常路径在 catch 中先 recordException 再 setStatus(ERROR);提前返回路径从 try 中返回;三条路径一律由 finally 调用 span.end()。从工程目录执行:
1 | |
测试先得到三个已结束 Span:正常与提前返回的状态均为 UNSET,异常分支的事件名为 exception 且状态为 ERROR。另有独立反例 exception-only:只调用 recordException、不设置状态,验证事件存在而状态仍为 UNSET。实际输出如下:
1 | |
这个实验只覆盖手动 Span 的内存数据,不推断 HTTP 插桩遇到某个响应码时如何设置状态;框架和语义约定的映射需要到相应章节另行查证。end() 是否调用应在代码里保证,而不是由 exporter 替应用补上。
控制流与边界
finally 处理提前返回,也处理 catch 后返回。若只在正常返回的最后一行调用 end,异常和早退会留下一条仍在记录、尚未交付的 Span;若只记录异常,后端也未必会显示期望的 ERROR 状态。这是资源式结束与错误语义解耦的通用模式:异常如何标记,由业务/插桩语义决定;不漏结束是另一条独立约束。
练习一。 在 Lab03Test.run 中暂时移走 finally 内的 span.end(),观察已结束列表数量变化;再恢复,说明业务方法返回和 Span 交付之间的区别。
练习二。 删除 catch 中的 setStatus(ERROR),保留 recordException;预测异常事件和状态的断言哪一条失败。这是验证“记录异常自动标错”说法的反例。
系列导航与资料
00 导读与第一条 Trace · 01 初始化 · 02 字段归属 · 当前篇:03 Span 生命周期。下一篇:04 Context 与 Scope(待写)。
参考资料:OpenTelemetry Java API、上文固定 SHA 的实现与测试(Apache-2.0);教学代码 Lab03Test 与原始输出见工程 evidence/03/。
