Baggage 通过明确的边界传播上下文
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 经过请求头编码后还原一致。断言还检查了筛选过程没有删除原上下文中的备注,也没有改变最初的空上下文。这说明清理出站载体与修改其他引用不是同一件事,后续代码必须使用正确的上下文对象。 ...