4. 手动创建 Span:创建根 Span 与子 Span,设置 Span 属性与事件,记录事务的开始与结束
在 OpenTelemetry 中,Span 是分布式追踪的基本工作单元。当自动插桩无法覆盖特定业务逻辑或中间件调用边界时,手动创建 Span 成为精确控制事务边界的核心手段。本章将系统讲解如何通过 OpenTelemetry SDK 手动创建根 Span 与子 Span,并通过属性(Attributes)和事件(Events)丰富追踪上下文,最终实现事务开始与结束的精确记录。
4.1 Span 的生命周期与手动创建流程
一个 Span 的生命周期包含四个关键阶段:开始、设置上下文、添加元数据、结束。手动创建 Span 时,开发者需要显式调用这些阶段的 API。
| 阶段 | API 方法 | 说明 |
|---|---|---|
| 开始 | tracer.startSpan(name) |
创建一个新的 Span 实例,此时 Span 处于“进行中”状态 |
| 设置上下文 | with span.makeCurrent() |
将 Span 设置为当前激活的 Span,使子 Span 能自动继承父 Span |
| 添加元数据 | span.setAttribute() / span.addEvent() |
附加业务属性与事件时间戳 |
| 结束 | span.end() |
标记 Span 完成,记录结束时间戳,并发送到导出器 |
try-with-resources(Java)或 with 语句(Python)确保 Span 在异常路径下也能正确结束,避免内存泄漏和追踪数据丢失。
4.2 创建根 Span:事务的起点
根 Span 是整个追踪链的起点,它没有父 Span。在中间件调用链中,根 Span 通常对应一个外部请求的入口,例如 HTTP 请求的接收、消息队列的消息消费开始。
以下示例展示如何在 Java 中创建一个根 Span,并记录事务的开始:
// 获取 Tracer 实例
Tracer tracer = openTelemetry.getTracer("order-service", "1.0.0");
// 创建根 Span
Span rootSpan = tracer.spanBuilder("processOrder")
.setSpanKind(SpanKind.SERVER)
.startSpan();
// 将根 Span 设置为当前上下文
try (Scope scope = rootSpan.makeCurrent()) {
// 记录事务开始事件
rootSpan.addEvent("transaction.start",
Attributes.of(AttributeKey.stringKey("order.id"), "ORD-2024-001"));
// 执行业务逻辑...
processPayment();
// 记录事务结束事件
rootSpan.addEvent("transaction.end",
Attributes.of(AttributeKey.stringKey("status"), "success"));
} catch (Exception e) {
rootSpan.recordException(e);
rootSpan.setStatus(StatusCode.ERROR);
} finally {
rootSpan.end(); // 必须调用 end()
}
end() 调用必须在所有子 Span 结束之后。如果根 Span 先于子 Span 结束,子 Span 将变成“孤儿 Span”,无法被正确关联到追踪树中。
4.3 创建子 Span:中间件调用链的边界
子 Span 代表事务内部的细分步骤,例如数据库查询、远程服务调用、消息发送等。子 Span 通过继承当前上下文的父 Span 自动建立父子关系。
下面的 SVG 图展示了根 Span 与子 Span 在中间件调用链中的层级关系:
创建子 Span 的典型代码模式如下:
// 在根 Span 的上下文中创建子 Span
Span childSpan = tracer.spanBuilder("validateInventory")
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
try (Scope childScope = childSpan.makeCurrent()) {
// 设置子 Span 属性
childSpan.setAttribute("inventory.item", "SKU-12345");
childSpan.setAttribute("inventory.quantity", 10);
// 记录子 Span 事件
childSpan.addEvent("inventory.check.start");
// 执行库存校验逻辑...
boolean available = checkStock("SKU-12345", 10);
childSpan.addEvent("inventory.check.end",
Attributes.of(AttributeKey.booleanKey("available"), available));
if (!available) {
childSpan.setStatus(StatusCode.ERROR, "Insufficient stock");
}
} catch (Exception e) {
childSpan.recordException(e);
childSpan.setStatus(StatusCode.ERROR);
} finally {
childSpan.end();
}
4.4 设置 Span 属性与事件
Span 属性(Attributes) 是键值对形式的元数据,用于描述 Span 的静态特征,如请求 ID、用户 ID、服务名称等。Span 事件(Events) 是带时间戳的日志记录,用于标记 Span 生命周期内的关键时间点。
| 特性 | Span 属性 | Span 事件 |
|---|---|---|
| 用途 | 描述 Span 的静态上下文 | 记录时间点上的动态信息 |
| 数据结构 | 键值对(Key-Value) | 名称 + 时间戳 + 可选属性 |
| 可变性 | 可在 Span 生命周期内多次设置 | 每次调用 addEvent 生成一条新记录 |
| 典型场景 | 请求路径、用户 ID、服务版本 | 缓存命中/未命中、异常发生、事务提交 |
| 查询能力 | 支持基于属性的过滤和聚合 | 支持基于事件名称的时间序列分析 |
以下代码展示了如何为 Span 设置丰富的属性与事件:
// 设置多个属性
span.setAttribute("http.method", "POST");
span.setAttribute("http.url", "/api/orders");
span.setAttribute("user.id", "user-789");
span.setAttribute("request.size", 2048);
// 使用 Attributes 构建器批量设置
span.setAllAttributes(Attributes.builder()
.put("db.system", "postgresql")
.put("db.statement", "SELECT * FROM orders WHERE id = ?")
.put("db.connection.count", 5)
.build());
// 记录事件
span.addEvent("cache.lookup");
span.addEvent("cache.hit",
Attributes.of(AttributeKey.stringKey("cache.key"), "order:ORD-2024-001"));
span.addEvent("db.query.start");
// ... 执行数据库查询 ...
span.addEvent("db.query.end",
Attributes.of(AttributeKey.longKey("rows.returned"), 1));
4.5 记录事务的开始与结束
在中间件调用链中,事务边界通常对应一个完整的业务操作,例如“创建订单”或“处理支付”。通过手动创建 Span 并添加开始/结束事件,可以精确测量事务的耗时和状态。
下面的 SVG 图展示了事务边界在时间轴上的 Span 与事件分布:
完整的事务边界记录代码示例:
// 事务开始
Span transactionSpan = tracer.spanBuilder("createOrderTransaction")
.setSpanKind(SpanKind.SERVER)
.startSpan();
try (Scope scope = transactionSpan.makeCurrent()) {
// 记录事务开始事件
transactionSpan.addEvent("transaction.start",
Attributes.of(
AttributeKey.stringKey("transaction.id"), generateTransactionId(),
AttributeKey.stringKey("timestamp"), Instant.now().toString()
));
// 步骤1:校验库存(子 Span)
Span inventorySpan = tracer.spanBuilder("validateInventory").startSpan();
try (Scope invScope = inventorySpan.makeCurrent()) {
inventorySpan.setAttribute("product.id", productId);
inventorySpan.addEvent("inventory.check");
// ... 校验逻辑 ...
inventorySpan.end();
}
// 步骤2:处理支付(子 Span)
Span paymentSpan = tracer.spanBuilder("processPayment").startSpan();
try (Scope payScope = paymentSpan.makeCurrent()) {
paymentSpan.setAttribute("payment.method", "credit_card");
paymentSpan.addEvent("payment.authorization");
// ... 支付逻辑 ...
paymentSpan.end();
}
// 记录事务结束事件
transactionSpan.addEvent("transaction.end",
Attributes.of(
AttributeKey.stringKey("status"), "success",
AttributeKey.longKey("duration.ms"),
ChronoUnit.MILLIS.between(startTime, Instant.now())
));
transactionSpan.setStatus(StatusCode.OK);
} catch (Exception e) {
transactionSpan.addEvent("transaction.error",
Attributes.of(
AttributeKey.stringKey("error.message"), e.getMessage()
));
transactionSpan.setStatus(StatusCode.ERROR, e.getMessage());
transactionSpan.recordException(e);
} finally {
transactionSpan.end();
}
finally 块中确保 transaction.end 事件被记录。
4.6 常见错误与避坑指南
- 忘记调用
end():未结束的 Span 会导致内存泄漏,且追踪数据不完整。始终使用 try-finally 或 try-with-resources 确保结束。 - 根 Span 先于子 Span 结束:子 Span 会变成孤儿,无法在追踪树中正确显示。确保所有子 Span 结束后再结束根 Span。
- 属性值类型不匹配:OpenTelemetry 要求属性值类型必须与 AttributeKey 声明的类型一致。例如
AttributeKey.longKey("count")只能接受long类型值。 - 事件名称过于泛化:使用
"start"、"end"等通用名称会导致后续分析困难。建议采用"module.action"的命名规范,如"inventory.check.start"。 - 忽略异常路径:在 catch 块中必须调用
recordException()和setStatus(StatusCode.ERROR),否则异常 Span 会被误判为成功。
通过手动创建 Span 并精确设置属性与事件,开发者能够完全掌控中间件调用链中的事务边界,为后续的分布式追踪分析提供高质量的数据基础。