一、 先看一段代码
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Scope;
public class Main {
public static void parentMethod() {
Span span = OpenTelemetrySupport.getTracer().spanBuilder("parent span").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("good", "job");
childMethod();
} catch (Throwable t) {
span.setStatus(StatusCode.ERROR, "handle parent span error");
} finally {
span.end();
}
}
public static void childMethod() {
Span span = OpenTelemetrySupport.getTracer().spanBuilder("child span").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("hello", "world");
} catch (Throwable t) {
span.setStatus(StatusCode.ERROR, "handle child span error");
} finally {
span.end();
}
}
public static void main(String[] args) {
parentMethod();
}
}
这段代码演示了如何使用 OpenTelemetry API 进行手动追踪(Tracing),包括 Span 的创建、属性设置、错误处理和上下文传播。以下是详细讲解:
1. 代码结构
Main
类:包含两个方法parentMethod()
和childMethod()
,以及main()
入口。OpenTelemetrySupport
(未展示):假设是一个工具类,提供Tracer
实例(实际项目中可能由 OpenTelemetry SDK 自动配置或手动初始化)。- 核心逻辑:
- 在
parentMethod()
中创建父 Span,并调用childMethod()
。 - 在
childMethod()
中创建子 Span。 - 通过
try-with-resources
和Scope
确保 Span 的生命周期管理。
- 在
2. 关键代码解析
(1) 创建 Span
Span span = OpenTelemetrySupport.getTracer().spanBuilder("parent span").startSpan();
getTracer()
:获取一个Tracer
实例(用于创建 Span)。spanBuilder("span name")
:定义 Span 的名称(在追踪系统中显示)。startSpan()
:启动 Span,开始记录追踪数据。
(2) 绑定 Span 到当前上下文
try (Scope scope = span.makeCurrent()) {
// 业务逻辑
}
span.makeCurrent()
:将 Span 绑定到当前线程的上下文(Context),使其成为“当前 Span”。try-with-resources
:确保Scope
在退出时自动关闭,避免上下文泄漏。- 作用:
- 确保子 Span(如
childMethod()
中的 Span)能正确关联到父 Span。 - 如果不绑定,子 Span 可能无法形成层级关系(变成独立的根 Span)。
- 确保子 Span(如
(3) 设置 Span 属性
span.setAttribute("good", "job");
setAttribute(key, value)
:为 Span 添加键值对属性,可用于:- 在追踪系统(如 Jaeger、Zipkin)中过滤或搜索。
- 记录业务数据(如用户 ID、请求参数)。
(4) 错误处理
} catch (Throwable t) {
span.setStatus(StatusCode.ERROR, "handle parent span error");
}
setStatus(StatusCode.ERROR, "description")
:- 标记 Span 为错误状态。
- 可附加错误描述(可选)。
- 注意:
- 即使发生异常,仍需在
finally
中调用span.end()
结束 Span。
- 即使发生异常,仍需在
(5) 结束 Span
finally {
span.end();
}
span.end()
:结束 Span,记录其持续时间。- 必须调用:否则 Span 不会上报,导致追踪数据不完整。
3. 追踪数据流
main()
调用parentMethod()
:- 创建
parent span
并设为当前上下文。
- 创建
parentMethod()
调用childMethod()
:- 由于
parent span
是当前上下文,childMethod()
中创建的child span
会自动成为其子 Span。
- 由于
- Span 层级关系:
parent span └── child span
- 上报数据:
- 最终 Span 数据会被 OpenTelemetry SDK 的 Exporter(如 OTLP、Logging)发送到后端(Jaeger/Zipkin)。
总结
- 核心功能:演示 OpenTelemetry 手动埋点,包括 Span 创建、上下文绑定、属性和错误处理。
- 关键点:
- 使用
makeCurrent()
确保上下文传播。 - 通过
try-finally
保证 Span 正确结束。 - 可扩展性:需结合 SDK 和 Exporter 上报数据。
- 使用
- 适用场景:需要精细控制追踪逻辑时(如自定义中间件、异步任务)。
二、关于Span、Scope
在 OpenTelemetry 中,Scope scope = span.makeCurrent()
的作用是将当前 Span
绑定到线程的上下文(Context)中,并返回一个 Scope
对象用于管理该上下文的生命周期。以下是详细解释:
1. 为什么需要 Scope
?
(1) 上下文传播(Context Propagation)
- 在分布式追踪中,Span 之间需要形成父子关系(例如
parentMethod()
调用childMethod()
时,子 Span 应关联到父 Span)。 - OpenTelemetry 通过
Context
(上下文)机制实现这一点:- 当一个 Span 被设为“当前 Span”时,后续在该线程中创建的 Span 会自动成为其子 Span。
- 如果没有正确绑定上下文,子 Span 可能无法关联到父 Span,导致追踪数据断裂。
(2) 避免上下文泄漏
- 如果直接调用
span.makeCurrent()
而不使用Scope
,可能会导致:- 忘记手动清除当前 Span(例如异常时未恢复之前的上下文)。
- 后续代码意外使用错误的 Span 作为父级。
Scope
通过try-with-resources
机制确保上下文在退出代码块时自动恢复,避免泄漏。
2. Scope
的作用
(1) 绑定 Span 到当前上下文
try (Scope scope = span.makeCurrent()) {
// 此时 `span` 是当前线程的活跃 Span
childMethod(); // 子 Span 会自动关联到父 Span
}
// 退出 try 块后,之前的上下文会自动恢复
span.makeCurrent()
:- 将
span
设置为当前线程的活跃 Span。 - 返回一个
Scope
对象,用于管理上下文的生命周期。
- 将
try-with-resources
:- 在
try
块结束时,Scope.close()
会被自动调用,恢复之前的上下文(即解除当前 Span 的绑定)。
- 在
(2) 恢复之前的上下文
- 如果在
try
块之前已有其他 Span 绑定到上下文(例如嵌套调用),Scope.close()
会将其恢复为之前的 Span。 - 示例:
Span parentSpan = tracer.spanBuilder("parent").startSpan(); try (Scope parentScope = parentSpan.makeCurrent()) { Span childSpan = tracer.spanBuilder("child").startSpan(); try (Scope childScope = childSpan.makeCurrent()) { // 此时活跃 Span 是 childSpan } // 退出后恢复为 parentSpan // 此时活跃 Span 是 parentSpan } // 退出后恢复为无活跃 Span
3. 对比:不使用 Scope
的问题
错误示例
// 错误:未使用 Scope,可能导致上下文混乱
Span parentSpan = tracer.spanBuilder("parent").startSpan();
parentSpan.makeCurrent(); // 手动绑定,但未恢复
Span childSpan = tracer.spanBuilder("child").startSpan();
childSpan.makeCurrent(); // 覆盖了父 Span 的绑定
// 问题:parentSpan 的上下文未恢复,可能导致后续代码误用
正确做法
// 正确:使用 Scope 自动管理上下文
try (Scope parentScope = parentSpan.makeCurrent()) {
try (Scope childScope = childSpan.makeCurrent()) {
// 逻辑...
}
}
4. Scope
的底层实现
Scope
是一个AutoCloseable
对象:- 它的
close()
方法会恢复调用makeCurrent()
之前的上下文。
- 它的
- 类似设计模式:
- 类似于数据库事务(
Transaction
)或锁(Lock
)的管理,通过try-with-resources
确保资源释放。
- 类似于数据库事务(
5. 总结
关键点 | 说明 |
---|---|
span.makeCurrent() | 将 Span 绑定到当前线程的上下文。 |
Scope | 管理上下文生命周期,确保退出代码块后自动恢复之前的上下文。 |
try-with-resources | 避免忘记恢复上下文,防止上下文泄漏。 |
父子 Span 关联 | 只有正确绑定上下文,子 Span 才能自动关联到父 Span。 |
最佳实践
- 始终使用
try (Scope scope = span.makeCurrent())
确保上下文正确管理。 - 避免手动调用
makeCurrent()
而不恢复,否则会导致追踪数据错误。
通过这种方式,OpenTelemetry 可以准确地构建 Span 之间的层级关系,生成完整的追踪链路。