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创建与导出完全在进程内完成,不依赖任何外部网络或存储服务。这使得测试可以做到零依赖、可重复、毫秒级执行。
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的示例代码片段:
# 创建父子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