4、第一个Mock Span:使用OTelMock创建并导出第一个模拟Span

在掌握了OTelMock的安装与启动之后,本章将带领你完成第一个实战操作:创建一个模拟Span并将其导出。这是理解OTelMock工作流的起点,也是后续所有隔离测试的基础。

4.1 核心概念回顾:Span的生命周期

在OpenTelemetry中,Span代表一个操作单元,具有明确的开始和结束时间。OTelMock模拟了这一完整生命周期,但所有数据仅在内存中流转,不会发送到真实后端。

阶段 OTelMock行为 真实OTel Collector行为
创建Span 在内存中分配Span对象 分配Span对象并序列化
设置属性 直接写入内存结构 写入并准备导出
结束Span 标记完成时间,存入缓冲区 标记完成,加入导出队列
导出 通过Mock Exporter暴露给测试代码 发送到配置的后端(如Jaeger)

4.2 操作流程概览

创建并导出第一个Mock Span只需四个步骤。下面的SVG图展示了完整的调用链路与数据流向。

关键原则:OTelMock的Span创建与导出完全在进程内完成,不依赖任何外部网络或存储服务。这使得测试可以做到零依赖、可重复、毫秒级执行。
第一个Mock Span创建与导出流程 测试代码 (Python / Java / Go) TracerProvider (Mock实现) Span对象 (内存中) MockExporter (内存缓冲区) 测试断言 (验证Span数据) 说明: 整个流程在单个进程内完成,不涉及网络I/O。MockExporter将Span数据保存在内存缓冲区中, 测试代码可以直接读取并断言,无需等待异步导出或轮询后端。

4.3 实战:Python示例

以下代码演示了如何使用OTelMock的Python SDK创建并导出第一个模拟Span。请确保已按照上一章完成OTelMock的安装与启动。

# test_first_span.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from otelmock.exporter import MockExporter

# 1. 创建Mock Exporter
mock_exporter = MockExporter()

# 2. 创建TracerProvider并注册Exporter
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(mock_exporter))
trace.set_tracer_provider(provider)

# 3. 获取Tracer并创建Span
tracer = trace.get_tracer("my-first-tracer")
with tracer.start_as_current_span("hello-span") as span:
    span.set_attribute("service.name", "demo-service")
    span.set_attribute("span.type", "manual")
    # 模拟一些业务逻辑
    span.add_event("processing started")
    # ... 业务代码 ...
    span.add_event("processing completed")

# 4. 从MockExporter获取导出的Span
exported_spans = mock_exporter.get_exported_spans()
assert len(exported_spans) == 1

first_span = exported_spans[0]
print(f"Span Name: {first_span.name}")
print(f"Span Attributes: {dict(first_span.attributes)}")
print(f"Span Events: {[e.name for e in first_span.events]}")

# 5. 清理
mock_exporter.reset()

4.4 关键代码解析

上述代码中的每个步骤都有明确的职责。下表列出了核心组件及其作用:

组件 作用 注意事项
MockExporter 将Span数据保存在内存列表中,提供get_exported_spans()方法供测试读取 每次测试前应调用reset()清空缓冲区
SimpleSpanProcessor Span结束时立即导出,无需等待批处理 测试场景推荐使用Simple模式,避免异步延迟
tracer.start_as_current_span 创建Span并自动管理上下文,退出with块时自动结束 也可使用start_span手动管理
set_attribute 为Span添加键值对元数据 属性值类型应为字符串、数字或布尔值
add_event 记录Span生命周期中的时间点事件 事件名称应具有可读性

4.5 验证输出

运行上述代码后,控制台应输出类似以下内容:

Span Name: hello-span
Span Attributes: {'service.name': 'demo-service', 'span.type': 'manual'}
Span Events: ['processing started', 'processing completed']
验证要点:
  • Span名称与创建时传入的字符串一致
  • 所有通过set_attribute设置的属性均出现在输出中
  • 事件按添加顺序排列,且时间戳为模拟时间
  • get_exported_spans()返回的列表长度等于已结束的Span数量

4.6 常见问题与避坑

  • Span未导出:检查是否在with块之外调用了get_exported_spans()。Span只有在结束后才会被导出。
  • 属性丢失:确保在Span结束之前设置属性。结束后的属性修改不会被导出。
  • Exporter未注册:确认add_span_processor已正确调用,且传入的是MockExporter实例。
  • 多次运行数据污染:每次测试前调用mock_exporter.reset(),避免上一次测试的Span干扰当前断言。
重要:OTelMock的MockExporter与真实OTel Exporter的接口完全兼容。这意味着你可以在不修改业务代码的前提下,通过替换Exporter实现从Mock到真实后端的切换。这是隔离测试的核心价值所在。

4.7 扩展:验证Span的层级结构

实际应用中Span往往存在父子关系。下面的SVG图展示了如何创建带有嵌套结构的Span树,并验证其层级关系。

嵌套Span的父子关系验证 parent-span trace_id: abc123 child-span-1 parent: parent-span child-span-2 parent: parent-span 验证方式:通过 span.parent_span_id 和 span.trace_id 断言父子关系。 所有子Span的trace_id必须与父Span一致,且parent_span_id指向父Span的span_id。

验证嵌套Span的示例代码片段:

# 创建父子Span
with tracer.start_as_current_span("parent-span") as parent:
    with tracer.start_as_current_span("child-span-1") as child1:
        child1.set_attribute("action", "query")
    with tracer.start_as_current_span("child-span-2") as child2:
        child2.set_attribute("action", "write")

spans = mock_exporter.get_exported_spans()
parent_span = [s for s in spans if s.name == "parent-span"][0]
child1_span = [s for s in spans if s.name == "child-span-1"][0]

# 断言父子关系
assert child1_span.parent_span_id == parent_span.span_id
assert child1_span.trace_id == parent_span.trace_id