OpenTelemetry 的 Baggage 是与上下文关联的一组字符串键值,可以让请求入口已经知道的信息继续到达下游,例如部署区域或发布分组。它与跨度属性不同,主要用途是传播;它也与链路上下文不同,承载的是应用选择的信息,而不是连接跨度的身份标识和标志。这些机制可以一起出现在一个请求里,但不能因此把它们理解为同一份数据,也不能假定它们具有相同的存储和信任语义。

跨进程传播必须落实到具体边界。发送客户端把上下文注入 HTTP 请求头等载体,接收端再从载体中提取成本地上下文。配置正确的插桩库可以自动执行这些动作,手工插桩时则要明确调用。仅在一个进程中创建行李字段,并不会把信息自动送到另一个进程、任意后台任务或外部服务。判断传播是否成功,应检查真实客户端与接收端,而不是只检查入口是否调用过设置函数。

决定哪些信息可以传播、哪些需要记录

行李字段不会自动成为跨度、日志或指标的属性。下游必须显式读取并复制,或者使用配置好的处理器执行这一映射策略。团队如果只打开传播,就期待在查询后台找到对应属性,通常会遇到数据看似消失的问题。反过来,把所有入站字段无差别复制进遥测,也可能造成语义不清、数据过大或指标基数增长。传播和记录是两个独立决定,应当分别明确目的与范围。

上下文操作返回新的上下文,不会把所有既有引用同时修改。调用者需要保留返回值,并在预期的位置传递或附加它。为某个出站请求删除不合适的行李字段时,也应保留该请求仍然需要的链路上下文。出站字段允许列表和遥测属性允许列表都应当是明确策略:某个接收者需要的值,不一定适合随后经过的全部目的地,更不一定适合永久写入所有观测记录。

坏掉的样子

行李上下文本身没有内置机制,能够证明某个值确实由可信组件签发。调用方完全可以提交请求头,自称属于昂贵账户等级或拥有特权角色。解析成功只能说明数据表示可被读取,不能说明其声明具有授权效力。认证与权限判断必须依赖各自可信的输入。即使字段很短、内容看起来合理,也不能跳过信任来源判断;本地解析器接受字符串,不等于业务系统应该接受其中的身份声明。

不受限制的行李字段会增加请求头大小,也可能把敏感信息传播给原本不该接收它的对象,包括自动插桩所覆盖的第三方接口。需要限制键名与值的范围,检查实际传播目的地,并由传播器负责线上编码。两个相同 SDK 实例之间往返成功,也不等于所有语言实现和代理组合都能够互操作。真正跨服务使用时,应验证实际途经的边界,而不是把一个进程内的演示扩大成对整个部署的保证。

快速验证

建立 Python 虚拟环境,安装 opentelemetry-sdk==1.35.0,把脚本保存为 baggage.py,并使用该环境的解释器运行。本次实际使用 Python 3.14.4。实验调用真实的 W3C 链路上下文传播器和行李传播器,并把跨度导出到内存;没有启动 HTTP 服务,字典代表客户端接下来将发送的请求头载体。因此结果直接证明编解码与上下文行为,不把尚未执行的跨服务通信写成已完成验证。

import json

from opentelemetry import baggage, trace
from opentelemetry.context import Context
from opentelemetry.baggage.propagation import W3CBaggagePropagator
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator

base = Context()
raw = baggage.set_baggage("region", "eu-west", context=base)
raw = baggage.set_baggage("release", "canary/2", context=raw)
raw = baggage.set_baggage("internal_note", "synthetic-only", context=raw)
assert not baggage.get_all(base)
propagator = CompositePropagator([TraceContextTextMapPropagator(), W3CBaggagePropagator()])
provider = TracerProvider()
exporter = InMemorySpanExporter()
provider.add_span_processor(SimpleSpanProcessor(exporter))
tracer = provider.get_tracer("baggage-lab")
with tracer.start_as_current_span("parent", context=Context()) as parent:
    outgoing = trace.set_span_in_context(parent, baggage.clear(context=raw))
    for key in ("region", "release"):
        outgoing = baggage.set_baggage(key, baggage.get_baggage(key, raw), context=outgoing)
    carrier = {}
    propagator.inject(carrier, context=outgoing)
    received = propagator.extract(carrier, context=Context())
    assert set(baggage.get_all(received)) == {"region", "release"}
    assert baggage.get_baggage("region", received) == "eu-west"
    assert baggage.get_baggage("internal_note", raw) == "synthetic-only"
    assert trace.get_current_span(received).get_span_context().trace_id == parent.get_span_context().trace_id
    with tracer.start_as_current_span("inherited", context=received):
        pass
    with tracer.start_as_current_span("copied", context=received) as span:
        span.set_attribute("deployment.region", baggage.get_baggage("region", received))
assert provider.force_flush()
spans = {span.name: span for span in exporter.get_finished_spans()}
assert not spans["inherited"].attributes
assert spans["copied"].attributes == {"deployment.region": "eu-west"}
independent = propagator.extract({"traceparent": "invalid", "baggage": "region=eu-west"},
                                context=Context())
assert not trace.get_current_span(independent).get_span_context().is_valid
assert baggage.get_baggage("region", independent) == "eu-west"
untrusted = propagator.extract({"baggage": "tier=gold"}, context=Context())
assert baggage.get_baggage("tier", untrusted) == "gold"
print(json.dumps({"wire_fields": sorted(carrier),
                  "wire_baggage": carrier["baggage"],
                  "automatic_span_attributes": dict(spans["inherited"].attributes),
                  "explicit_span_attributes": dict(spans["copied"].attributes),
                  "baggage_survives_invalid_traceparent": True,
                  "caller_supplied_tier_was_parsed": True}, indent=2))
provider.shutdown()

原始上下文包含区域、发布分组和一条完全虚构的内部备注。出站上下文只保留前两个字段,同时携带原父跨度。接收后区域应为 eu-west,发布值 canary/2 经过请求头编码后还原一致。断言还检查了筛选过程没有删除原上下文中的备注,也没有改变最初的空上下文。这说明清理出站载体与修改其他引用不是同一件事,后续代码必须使用正确的上下文对象。

两个子跨度进一步展示记录边界:inherited 收到行李上下文,但属性仍然为空;copied 只有显式设置的部署区域属性。另一个输入故意带有无效 traceparent,此时链路上下文无效,但独立编码的行李仍然可以解析。最后,调用方自称 tier=gold 的字符串也会被正常读取,过程中没有身份校验。把这套策略用于真实服务之前,还应检查客户端插桩、接收端提取、出站筛选和目标后台的属性映射,逐一确认实际行为与团队约定一致。

来源

  • OpenTelemetry baggage,行李上下文独立于遥测属性,且没有内置完整性保证,已核验 2026-09-13。
  • W3C Baggage,HTTP 表示、值编码与传播限制,已核验 2026-09-13。
  • W3C Trace Context,链路身份与应用行李上下文分别传播,已核验 2026-09-13。