2. 核心概念:Span、Span Attributes、Resource、Resource Attributes、Instrumentation Scope

在 OpenTelemetry 的数据模型中,Span、Resource 和 Instrumentation Scope 是构成可观测性信号的三大支柱。理解它们各自的职责以及属性(Attributes)的注入位置,是掌握元数据管理的基础。本章将逐一拆解这些核心概念,并重点说明属性与资源元数据如何在链路中发挥作用。

2.1 Span:分布式追踪的基本单元

Span 代表一个工作单元,是分布式追踪中最小的可观测性记录。每个 Span 记录了一次操作(如 HTTP 请求、数据库查询、函数调用)的开始时间、结束时间、状态以及一组描述该操作的元数据。

  • Span 名称:描述操作的人类可读名称,例如 GET /api/users。
  • Span 上下文:包含 Trace ID、Span ID 和 Trace Flags,用于在分布式系统中关联父子 Span。
  • Span 属性:键值对形式的元数据,用于丰富 Span 的语义信息。
  • Span 事件:在 Span 生命周期内发生的带时间戳的日志事件。
  • Span 状态:表示操作成功或失败(Unset、Ok、Error)。
实践要点:Span 属性是注入业务元数据的主要入口。例如,在 HTTP 服务端 Span 中注入 http.method、http.status_code 和 user.id,可以快速定位特定用户的请求链路。

2.2 Span Attributes:细粒度的操作元数据

Span Attributes 是附加在 Span 上的键值对,用于描述该操作的具体细节。OpenTelemetry 定义了一套语义约定(Semantic Conventions),为常见场景(HTTP、gRPC、数据库、消息队列等)提供了标准化的属性键名。

属性键(Key) 值类型 示例值 说明
http.method string GET HTTP 请求方法
http.url string https://api.example.com/users 完整的请求 URL
http.status_code int 200 HTTP 响应状态码
db.system string postgresql 数据库类型
db.statement string SELECT * FROM users WHERE id = ? 数据库查询语句(建议脱敏)
messaging.destination string order.created 消息队列主题或队列名
避坑指南:避免在 Span 属性中注入高基数(High Cardinality)数据,如用户 ID、Session ID 或请求体全文。高基数属性会导致后端存储(如 Jaeger、Zipkin)索引膨胀,影响查询性能。建议将高基数数据作为 Span 事件或日志记录。

2.3 Resource:服务与环境的身份标识

Resource 代表产生可观测性信号的实体,通常是一个服务实例、一个容器、一台主机或一个 Kubernetes Pod。Resource 描述的是谁产生了数据,而不是做了什么。Resource 信息会附加到该实体产生的所有 Span、Metric 和 Log 上。

  • 服务名称:service.name 是最重要的 Resource 属性,用于在 UI 中区分不同服务。
  • 服务版本:service.version 用于关联代码版本与可观测性数据。
  • 部署环境:deployment.environment 用于区分生产、预发、测试环境。
  • 主机信息:host.name、host.id、os.type 等。
  • 容器/平台信息:container.id、k8s.pod.name、cloud.provider 等。
关键区别:Span Attributes 描述一次操作的特征(如 HTTP 状态码),而 Resource Attributes 描述数据来源的固定特征(如服务名称、主机名)。Resource 数据在服务启动时确定,通常不会在请求级别变化。

2.4 Resource Attributes:全局的上下文元数据

Resource Attributes 是附加在 Resource 上的键值对,它们会被自动合并到该 Resource 产生的所有 Span 和 Metric 中。这意味着你不需要在每个 Span 上重复注入环境信息,只需在 Resource 层面声明一次。

// Python 示例:在 SDK 初始化时设置 Resource
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider

resource = Resource.create({
    "service.name": "order-service",
    "service.version": "1.2.3",
    "deployment.environment": "production",
    "host.name": "ip-10-0-1-100.ec2.internal",
    "cloud.provider": "aws",
    "cloud.region": "us-east-1"
})

provider = TracerProvider(resource=resource)

2.5 Instrumentation Scope:库与模块的归属标记

Instrumentation Scope 用于标识产生 Span 或 Metric 的库、模块或组件。它解决了“这段可观测性数据是由哪个库生成的”问题。例如,一个 HTTP 请求的 Span 可能由 opentelemetry-instrumentation-flask 库创建,而一个数据库查询 Span 则由 opentelemetry-instrumentation-psycopg2 创建。

  • Scope Name:通常是库的包名或模块名,例如 io.opentelemetry.flask。
  • Scope Version:库的版本号,用于追踪 instrumentation 库的变更。
  • 作用:在 UI 中按 instrumentation 库过滤数据,方便排查特定库版本引入的问题。

2.6 三者的关系与数据流

下图展示了 Span、Resource 和 Instrumentation Scope 在 OpenTelemetry 数据模型中的层级关系。每个 Span 都归属于一个 Instrumentation Scope,并且所有 Span 都继承自同一个 Resource 的属性。

OpenTelemetry 数据模型层级关系 Resource(资源) service.name, service.version, host.name, deployment.environment 描述“谁”产生了数据(服务、主机、容器) Instrumentation Scope(检测范围) scope.name: io.opentelemetry.flask, scope.version: 0.45b0 描述“哪个库”产生了数据 Span(跨度) 描述“做了什么”(一次操作) Span Attributes http.method: GET http.status_code: 200 user.id: 12345 Span 元数据 Trace ID: abc...def Span ID: 123...789 Start / End Timestamp

2.7 属性注入的优先级与合并规则

当同一个属性键在多个层级出现时,OpenTelemetry 遵循明确的合并规则。理解这些规则可以避免元数据被意外覆盖。

层级 优先级 示例
Span Attributes 最高 Span 上显式设置的 http.method 会覆盖 Resource 中的同名属性
Instrumentation Scope Attributes 中 Scope 级别的属性会覆盖 Resource 属性,但被 Span 属性覆盖
Resource Attributes 最低(基础) 作为全局默认值,被所有下级继承
最佳实践:将不变的环境信息(服务名、版本、区域)放在 Resource 中;将库相关的上下文(如 instrumentation 库版本)放在 Scope 中;将请求级别的动态信息(HTTP 方法、状态码、业务 ID)放在 Span 属性中。这样既能避免重复,又能保证数据覆盖的正确性。

2.8 实战:在 Python 中注入 Span 与 Resource 属性

以下代码演示了如何在 Python 中同时设置 Resource 属性和 Span 属性,并观察它们的合并效果。

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

# 1. 创建 Resource,注入全局属性
resource = Resource.create({
    "service.name": "payment-service",
    "service.version": "2.0.0",
    "deployment.environment": "staging"
})

# 2. 配置 TracerProvider
provider = TracerProvider(resource=resource)
processor = SimpleSpanProcessor(OTLPSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

# 3. 获取 Tracer
tracer = trace.get_tracer(__name__, "1.0.0")

# 4. 创建 Span 并注入 Span 属性
with tracer.start_as_current_span("process_payment") as span:
    span.set_attribute("payment.amount", 99.99)
    span.set_attribute("payment.currency", "USD")
    span.set_attribute("payment.method", "credit_card")
    # Resource 属性(service.name, version, environment)会自动附加到此 Span
    print("Span 已创建,Resource 属性自动继承")

在这个例子中,payment-service、2.0.0 和 staging 三个 Resource 属性会自动出现在 process_payment Span 中,无需在每个 Span 上重复设置。而 payment.amount 等 Span 属性则描述了本次支付操作的具体细节。

2.9 概念对比总结

概念 作用域 生命周期 典型用途 是否可被下级覆盖
Resource 全局(所有信号) 进程级别,启动时确定 服务名称、版本、主机、环境 是(被 Scope 和 Span 覆盖)
Instrumentation Scope 库/模块级别 库加载时确定 库名称、库版本 是(被 Span 覆盖)
Span 单次操作 请求级别,动态创建 HTTP 方法、状态码、业务 ID 最高优先级,不可被覆盖