我们直接相信了外部传来的 baggage
目标
亲手放入、取出 baggage 和 tracestate,用数字确认 baggage 不会自动变成跨度属性、以及这个请求头会被带到所有下游请求,再给从外部传入的上下文做一个白名单过滤器,并让第二个服务也按同样的规则处理。
为什么重要
即使跟踪连起来了,往下六跳的跨度上也没有“这是哪个租户的请求”。baggage 就是为了把这个值运送到整个请求而设的位置,但只放进去的话,它哪里也不会出现——每个服务必须把它取出来,抄写到自己的跨度上,才能成为可查询的数据。反过来,如果把值放得很宽松,这些字节就会附在发往下游的所有请求上,只有调用多的路径延迟变长,还会触到代理的请求头上限。而且在公开 API 前面收到的 baggage 是别人写的字符串,原样抄成属性的话,指标的基数就由别人来决定。tracestate 虽然附在同一个请求上,但不是放业务值的位置,而是跟踪工具记录自己位置的位置,规范对条目数量和长度定有规则。
步骤
- 创建
/root/tp-baggage/carry.py。转储路径读取环境变量TRACELAB_OUT,没有则使用/root/tp-baggage/carry.jsonl。服务名称是shop-edge,跨度名称是checkout。在该跨度内,往 baggage 中放入tenant=acme和checkout.tier=gold,然后向一个空 dict 中同时注入traceparent和baggage,再把这个 dict 以 JSON 写入与转储文件相同的目录中的carry.json。最后把程序运行一次。 - 创建
/root/tp-baggage/attrs.py。默认转储路径是/root/tp-baggage/attrs.jsonl。从/opt/app/tracelab/tp_baggage/requests.json中取出name为ok的请求的请求头,提取上下文,并在该上下文之下创建两个 SERVER 跨度。第一个price.raw不加任何属性,第二个price.tagged把从 baggage 中取出的值抄写成属性tenant和checkout.tier。服务名称是shop-pricing。运行程序之后,用眼睛比较两个跨度的attributes。 - 创建
/root/tp-baggage/budget.py,把/opt/app/tracelab/tp_baggage/candidates.json中的四组候选分别做成 baggage 请求头并测量成本。默认转储路径是/root/tp-baggage/budget.jsonl,在与转储文件相同目录的budget.tsv中,不要表头,用制表符分隔写四行——<id>、<넣은 항목 수>、<헤더에 실제로 실린 항목 수>、<헤더 바이트 수>、<keep|drop>(占位符依次为 id、放入的条目数、请求头中实际带上的条目数、请求头字节数、keep 或 drop)。最后一列,在 W3C Baggage 规范保证传递的范围(条目数和字节数)之内为keep,超出为drop。每个候选留下一个名为budget的跨度,并加上属性baggage.id、baggage.entries、baggage.bytes。 - 创建
/root/tp-baggage/sanitize.py。默认转储路径是/root/tp-baggage/sanitize.jsonl。从/opt/app/tracelab/tp_baggage/requests.json的hostile请求中提取 baggage 后,只保留键是tenant、checkout.tier、region之一、且值符合正则^[a-z0-9][a-z0-9._-]{0,31}$的条目。用只装着留下内容的新上下文,作为 baggage 请求头注入,并写入与转储文件相同目录的kept.json。在该请求的父级之下创建 SERVER 跨度POST /checkout,在属性baggage.in、baggage.kept、baggage.dropped中放入个数,并在事件baggage.dropped的属性keys中,把被丢弃的键按字典序用逗号连起来写入。服务名称是shop-edge。 - 创建
/root/tp-baggage/tsread.py,对/opt/app/tracelab/tp_baggage/tracestates.json中的六个请求头做出判定。默认转储路径是/root/tp-baggage/tsread.jsonl,在与转储文件相同目录的tsreport.tsv中,用制表符分隔写六行<id>、<항목 수>、<헤더 글자 수>、<ok|bad>(占位符依次为 id、条目数、请求头字符数、ok 或 bad)。ok的条件是:条目数不超过规范的上限,键以小写字母或数字开头,且只使用小写字母、数字以及_、-、*、/,值不超过规范的字符数上限,并且不含逗号或等号。另外在/root/tp-baggage/05-limits.txt中写三行max_members=、max_value_chars=、propagate_min_chars=,数字取自规范。每一项各留下一个tracestate.read跨度,并加上属性ts.id、ts.members、ts.ok。 - 创建
/root/tp-baggage/tsmutate.py,把同样的六个请求头改成发出的请求头。规则有四条——(1)我们的条目是labhub=r1,始终放在最左边;(2)如果我们的键已经存在,先删掉再新放到前面(不能出现两次);(3)其余条目的顺序保持不变;(4)条目数超过规范的上限时,先从后往前删除超过 128 个字符的条目,如果仍然超出,就从末尾开始删除。默认转储路径是/root/tp-baggage/tsmutate.jsonl,在与转储文件相同目录的tsout.tsv中,用制表符分隔写六行<id>和<나가는 헤더>(占位符依次为 id、发出的请求头)。每一项各留下一个tracestate.out跨度,并加上属性ts.id、ts.out。 - 把前面几步中手工放入的值集中到
/root/tp-baggage/policy.json这一个位置。在baggage之下放allow(允许的键数组)、value_pattern(值的正则)、max_entries、max_bytes,在tracestate之下放key、value、max_members、drop_over_chars、position。max_entries和max_bytes必须在规范保证的范围之内,max_members和drop_over_chars必须与规范的数字相同,position是left。allow里不要放用于识别个人的键。 - 创建
/root/tp-baggage/gateway.py,读取/root/tp-baggage/policy.json,处理/opt/app/tracelab/tp_baggage/requests.json中全部四个请求。对每个请求,按规则过滤 baggage,按第 6 步的规则改写 tracestate,然后在该请求的父级之下各留下一个 SERVER 跨度gateway,并加上属性req.name、baggage.kept、baggage.dropped、tracestate.members。默认转储路径是/root/tp-baggage/gateway.jsonl,在与转储文件相同目录的gateway.tsv中,用制表符分隔写四行<name>、<남긴 수>、<버린 수>、<나가는 tracestate 항목 수>(占位符依次为名称、保留数、丢弃数、发出的 tracestate 条目数)。服务名称是shop-gateway。
参考
- 工作目录是
/root/tp-baggage。如果不存在,先创建。 - 带埋点的程序必须用
/opt/otel-lab/bin/python运行。系统python3中没有 OpenTelemetry。 - 转储路径始终先读取环境变量
TRACELAB_OUT,没有时才使用题目中写的默认路径。附带产物(如carry.json、budget.tsv)也要写在与转储文件相同的目录里。因为评分器会在自己的临时目录里把同一个程序再运行一次来比对。 - 转储文件是追加写入,所以程序运行多次,跨度会累积。开始时用
open(OUT, "w").close()清空它。 - 材料在
/opt/app/tracelab/tp_baggage/中——requests.json(传入的四个请求)、candidates.json(四组 baggage 候选)、tracestates.json(六个 tracestate 请求头)。这些文件不要修改。 - 想把转储文件看得易读,用
python3 /opt/lab/checks/_tplib.py summary <덤프>(占位符为转储文件)。 - 常见错误:放入 baggage 之后,没有使用
set_baggage返回的上下文,而是注入当前上下文。那样发出去的请求头是空的。 - 常见错误:提取传入的 baggage 时,没有以空的
Context()为基准,导致我们这边的值混进来。 - W3C Baggage · W3C Trace Context — tracestate · OpenTelemetry — Baggage 概念 · OpenTelemetry Python — Propagation · OpenTelemetry — Context propagation
放入 baggage 并传给下一个服务
创建 /root/tp-baggage/carry.py。转储路径读取环境变量 TRACELAB_OUT,没有则使用 /root/tp-baggage/carry.jsonl。服务名称是 shop-edge,跨度名称是 checkout。在该跨度内,往 baggage 中放入 tenant=acme 和 checkout.tier=gold,然后向一个空 dict 中同时注入 traceparent 和 baggage,再把这个 dict 以 JSON 写入与转储文件相同的目录中的 carry.json。最后把程序运行一次。
opentelemetry.baggage.set_baggage(key, value, context=...) 会返回加上了值的新上下文。把这个上下文同时传给 W3CBaggagePropagator().inject(carrier, context=...) 和 TraceContextTextMapPropagator().inject(...),一个 dict 里就会放入两个请求头。带埋点的程序用 /opt/otel-lab/bin/python 运行。
baggage 不会自动变成跨度属性
创建 /root/tp-baggage/attrs.py。默认转储路径是 /root/tp-baggage/attrs.jsonl。从 /opt/app/tracelab/tp_baggage/requests.json 中取出 name 为 ok 的请求的请求头,提取上下文,并在该上下文之下创建两个 SERVER 跨度。第一个 price.raw 不加任何属性,第二个 price.tagged 把从 baggage 中取出的值抄写成属性 tenant 和 checkout.tier。服务名称是 shop-pricing。运行程序之后,用眼睛比较两个跨度的 attributes。
从请求头中取出上下文的工作由两个提取器分担——traceparent 用 TraceContextTextMapPropagator,baggage 用 W3CBaggagePropagator。要把前一个的结果传给后一个的 context=,两者才会汇集到同一个上下文里。取出的 baggage 全部内容用 baggage.get_all(ctx) 查看。创建跨度用 start_as_current_span(..., context=ctx, kind=SpanKind.SERVER)。
baggage 这一行会带在发出的每个请求上
创建 /root/tp-baggage/budget.py,把 /opt/app/tracelab/tp_baggage/candidates.json 中的四组候选分别做成 baggage 请求头并测量成本。默认转储路径是 /root/tp-baggage/budget.jsonl,在与转储文件相同目录的 budget.tsv 中,不要表头,用制表符分隔写四行——<id>、<넣은 항목 수>、<헤더에 실제로 실린 항목 수>、<헤더 바이트 수>、<keep|drop>(占位符依次为 id、放入的条目数、请求头中实际带上的条目数、请求头字节数、keep 或 drop)。最后一列,在 W3C Baggage 规范保证传递的范围(条目数和字节数)之内为 keep,超出为 drop。每个候选留下一个名为 budget 的跨度,并加上属性 baggage.id、baggage.entries、baggage.bytes。
空上下文用 opentelemetry.context.Context() 创建。注入的 dict 里 baggage 的值就是请求头字符串,条目数是按逗号拆分后的个数,字节数是按 UTF-8 编码后的长度。保证范围的两个数字写在规范的 Limits 一节里。四个候选中有一个,放入的条目数和实际带上的条目数不同——为什么会这样,直接看请求头。
给从外部传入的 baggage 加上白名单
创建 /root/tp-baggage/sanitize.py。默认转储路径是 /root/tp-baggage/sanitize.jsonl。从 /opt/app/tracelab/tp_baggage/requests.json 的 hostile 请求中提取 baggage 后,只保留键是 tenant、checkout.tier、region 之一、且值符合正则 ^[a-z0-9][a-z0-9._-]{0,31}$ 的条目。用只装着留下内容的新上下文,作为 baggage 请求头注入,并写入与转储文件相同目录的 kept.json。在该请求的父级之下创建 SERVER 跨度 POST /checkout,在属性 baggage.in、baggage.kept、baggage.dropped 中放入个数,并在事件 baggage.dropped 的属性 keys 中,把被丢弃的键按字典序用逗号连起来写入。服务名称是 shop-edge。
想只看传入的 baggage,提取时要以空的 Context() 而不是当前上下文为基准。只过滤键是不够的——这个请求里混有“允许的键里放了奇怪的值”的情况。事件用 span.add_event(이름, {속성})(占位符依次为事件名称、属性)留下。
从规范中读取 tracestate 的规则并应用
创建 /root/tp-baggage/tsread.py,对 /opt/app/tracelab/tp_baggage/tracestates.json 中的六个请求头做出判定。默认转储路径是 /root/tp-baggage/tsread.jsonl,在与转储文件相同目录的 tsreport.tsv 中,用制表符分隔写六行 <id>、<항목 수>、<헤더 글자 수>、<ok|bad>(占位符依次为 id、条目数、请求头字符数、ok 或 bad)。ok 的条件是:条目数不超过规范的上限,键以小写字母或数字开头,且只使用小写字母、数字以及 _、-、*、/,值不超过规范的字符数上限,并且不含逗号或等号。另外在 /root/tp-baggage/05-limits.txt 中写三行 max_members=、max_value_chars=、propagate_min_chars=,数字取自规范。每一项各留下一个 tracestate.read 跨度,并加上属性 ts.id、ts.members、ts.ok。
三个数字就原样写在 Trace Context 规范的 tracestate Limits 一节和 Key、Value 两节里。propagate_min_chars 不是上限,而是厂商至少必须传递的长度。空请求头不是错误——规范写明了要接受。
保留传入的 tracestate,并把我们的条目放到前面
创建 /root/tp-baggage/tsmutate.py,把同样的六个请求头改成发出的请求头。规则有四条——(1)我们的条目是 labhub=r1,始终放在最左边;(2)如果我们的键已经存在,先删掉再新放到前面(不能出现两次);(3)其余条目的顺序保持不变;(4)条目数超过规范的上限时,先从后往前删除超过 128 个字符的条目,如果仍然超出,就从末尾开始删除。默认转储路径是 /root/tp-baggage/tsmutate.jsonl,在与转储文件相同目录的 tsout.tsv 中,用制表符分隔写六行 <id> 和 <나가는 헤더>(占位符依次为 id、发出的请求头)。每一项各留下一个 tracestate.out 跨度,并加上属性 ts.id、ts.out。
规范写着“被修改的键要移到左边,没有动过的条目要保持顺序”。所以先把我们的条目删掉、再重新加到最前面的顺序很重要。截断时必须整条丢弃,规范点名要先丢弃的条目是什么,写在 Limits 一节里。六个输入中有两个因为不同的原因被截断。
把规则固化成机器可读的文件
把前面几步中手工放入的值集中到 /root/tp-baggage/policy.json 这一个位置。在 baggage 之下放 allow(允许的键数组)、value_pattern(值的正则)、max_entries、max_bytes,在 tracestate 之下放 key、value、max_members、drop_over_chars、position。max_entries 和 max_bytes 必须在规范保证的范围之内,max_members 和 drop_over_chars 必须与规范的数字相同,position 是 left。allow 里不要放用于识别个人的键。
这个文件由下一步的程序读取。第 4 步写的白名单和正则、第 6 步写的我们的键和值,原样搬到这里就行。max_entries 和 max_bytes 取得比规范上限小也可以——上限的意思是“到这里为止都会被传递”,而不是“要填到这里”。
把规则原样应用到第二个服务
创建 /root/tp-baggage/gateway.py,读取 /root/tp-baggage/policy.json,处理 /opt/app/tracelab/tp_baggage/requests.json 中全部四个请求。对每个请求,按规则过滤 baggage,按第 6 步的规则改写 tracestate,然后在该请求的父级之下各留下一个 SERVER 跨度 gateway,并加上属性 req.name、baggage.kept、baggage.dropped、tracestate.members。默认转储路径是 /root/tp-baggage/gateway.jsonl,在与转储文件相同目录的 gateway.tsv 中,用制表符分隔写四行 <name>、<남긴 수>、<버린 수>、<나가는 tracestate 항목 수>(占位符依次为名称、保留数、丢弃数、发出的 tracestate 条目数)。服务名称是 shop-gateway。
不要把规则再写成常量,要从 policy.json 中读取——这一步的要点就在这里。四个请求中,一个既没有 baggage 也没有 tracestate,一个的 tracestate 已经满了。两者都必须不出错地通过。