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()
}
注意:根 Span 的 end() 调用必须在所有子 Span 结束之后。如果根 Span 先于子 Span 结束,子 Span 将变成“孤儿 Span”,无法被正确关联到追踪树中。

4.3 创建子 Span:中间件调用链的边界

子 Span 代表事务内部的细分步骤,例如数据库查询、远程服务调用、消息发送等。子 Span 通过继承当前上下文的父 Span 自动建立父子关系。

下面的 SVG 图展示了根 Span 与子 Span 在中间件调用链中的层级关系:

Span 层级结构:根 Span 与子 Span 根 Span: processOrder 子 Span: validateInventory 子 Span: processPayment 子 Span: sendNotification 时间 → 说明:子 Span 通过 makeCurrent() 继承父 Span 的上下文,形成完整的追踪树。 每个子 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 与事件的时间分布 T0 T1 根 Span: processOrder (580ms) validateInventory (200ms) processPayment (250ms) notify (60ms) transaction.start inventory.check.end payment.processed transaction.end 事务边界记录要点: 1. 在根 Span 开始后立即记录 transaction.start 事件 2. 在每个子 Span 的关键节点记录业务事件(如库存校验完成、支付处理) 3. 在所有子 Span 结束后,根 Span 结束前记录 transaction.end 事件

完整的事务边界记录代码示例:

// 事务开始
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 并精确设置属性与事件,开发者能够完全掌控中间件调用链中的事务边界,为后续的分布式追踪分析提供高质量的数据基础。