跨度是有的,却复现不出来
目标
从一个失败请求的一行跨度出发,填入足够的属性和事件,使得只凭这个跨度就能再次触发同样的失败。完整走一遍:把不能放的值换掉再留下,统计唯一值预算,把团队规约写成文件并用检查器强制执行。
为什么重要
自动埋点填上的是 HTTP 的外壳。路径、状态码和长度都会有,但凌晨打开那个跨度,问一句“那么放进什么才能让它再现”,却没有答案。把复现输入、当时的状态和我们做过的事作为属性留下,调查才能开始。反过来,如果把请求体整个放进去,联系方式和令牌就会原样堆积在可观测性后端——所以不放原文,而是换成哈希、类别、长度留下。带有时间点的事实不是属性而是事件,不是父子关系的关系就是链接。最后,如果不去数每个键有多少种唯一值,一条异常消息就会把键撑成数千种值,让搜索界面崩溃。
步骤
- 读一读
/opt/app/tracelab/tp_attrs/failed.jsonl中的一行。这是失败请求的跨度,但缺少复现所需的值。在/root/tp-attrs/01-missing.txt中写五行——每行是<열쇠이름>=<왜 필요한가>(占位符依次为键名、为什么需要),键依次为shop.cart.item_count、shop.request.body_bytes、shop.cache.hit、shop.queue.depth、shop.payment.retry_count。每个键的理由至少 25 个字,用自己的话写出“没有这个值就无法判断什么”。 - 创建
/root/tp-attrs/02_attrs.py。用tracelab.tp_attrs.orders处理订单ord-1010,同时创建一个名为POST /checkout的SERVER跨度,在第 1 步的五个键上再加订单号shop.order.id,共六个属性。支付调用charge(주문번호, 시도번호)(占位符依次为订单号、尝试序号),按 1、2、3 最多尝试三次,shop.payment.retry_count为(尝试次数 − 1)。转储默认路径为/root/tp-attrs/02-attrs.jsonl,如果设置了环境变量TRACELAB_OUT,就用它指定的路径。 - 创建
/root/tp-attrs/03_redact.py。在第 2 步的基础上再加三个属性——shop.customer.email_hash是邮箱 SHA-256 十六进制字符串的前 16 位,shop.payment.card_brand是卡的 brand 值,shop.auth.token_len是认证令牌的长度(整数)。邮箱、卡号(pan)、令牌、邮政编码的原文不能留在任何属性中。转储默认路径为/root/tp-attrs/03-redact.jsonl。 - 创建
/root/tp-attrs/04_events.py。在第 3 步的基础上,(1)如果缓存未命中,留下一个cache.miss事件;(2)每次支付尝试失败时,各留下一个payment.attempt.failed事件——事件属性是attempt(整数的尝试序号)和reason(PaymentError的kind);(3)如果三次都失败,把异常消息原样放入shop.error.message属性,并把跨度状态改为ERROR。转储默认路径为/root/tp-attrs/04-events.jsonl。这一步的shop.error.message会在第 5 步再次出现。 - 创建
/root/tp-attrs/05_bulk.py,用同样的埋点把orders.ORDER_IDS中的 200 个订单各处理一次(每个请求一个跨度)。转储默认路径为/root/tp-attrs/05-bulk.jsonl。然后在/root/tp-attrs/05-cardinality.tsv中,为转储文件里出现的每个属性键写一行<열쇠><탭><고유값수><탭><판정>(占位符依次为键、制表符、唯一值数量、制表符、判定),并按键名升序排列。判定规则:shop.order.id和shop.customer.email_hash为free,其余键的唯一值在 20 以下为low,超过 20 为leak。 - 创建
/root/tp-attrs/convention.tsv。每行是一个键,用制表符分成四列<열쇠><탭><타입><탭><허용값><탭><고유값성격>(占位符依次为键、制表符、类型、制表符、允许值、制表符、唯一值性质)。类型是string、int、bool、deny之一,允许值是*(不限制)或用|连接的列表,唯一值性质是free或low。第 5 步出现的十个键中,shop.error.message用deny禁止,改为新放入shop.error.kind,允许值为declined|timeout。第 8 步要用的shop.refund.amount(int、free)也要提前放进去,原文禁止键shop.customer.email、shop.payment.card_pan、shop.auth.token也写成deny。deny行的允许值和性质两列写-。 - 创建
/root/tp-attrs/lint_spans.py。用python3 lint_spans.py <규약파일> <덤프>(占位符依次为规约文件、转储文件)调用时,每个违反规约的键输出一次VIOLATION <열쇠> <이유>(占位符依次为键、原因),按键名升序,每个键只输出一次,并以退出码 1 退出;没有违反时,输出一行以OK开头的内容,并以退出码 0 退出。要检查四件事——规约中没有的键、被deny禁止的键、类型不同的值、不在允许值列表中的值。此外,声明为low的键,如果它在转储文件中的唯一值超过 20,也算违反。写好之后,分别在/root/tp-attrs/05-bulk.jsonl和/opt/app/tracelab/tp_attrs/noisy.jsonl上运行,并在/root/tp-attrs/07-violations.tsv中按这个顺序写两行<덤프파일이름><탭><깨진 열쇠들을 쉼표로 이은 것>(占位符依次为转储文件名、制表符、被违反的键用逗号连接的结果)。 - 创建
/root/tp-attrs/08_refund.py,为处理由队列触发的退款ord-1027的POST /refund创建一个SERVER跨度。属性是shop.order.id、shop.refund.amount、shop.queue.depth、shop.customer.email_hash、shop.payment.card_brand、shop.auth.token_len、shop.payment.retry_count,如果以失败告终,就再加上shop.error.kind,并把状态设为ERROR(不要放入原文消息)。每次失败的尝试都留下payment.attempt.failed事件,并把orders.job_context(주문번호)(占位符为订单号)给出的跟踪坐标作为链接加上(不要挂成父级)。转储默认路径为/root/tp-attrs/08-refund.jsonl,用第 7 步的检查器在这个转储文件上运行,必须输出OK。
参考
- 工作目录是
/root/tp-attrs。如果不存在,先创建。 - 带埋点的程序必须用
/opt/otel-lab/bin/python运行。读取并统计转储文件的脚本,用系统python3就够了。 - 公共接线是
/opt/app/tracelab/dump.py(provider、flush),材料是/opt/app/tracelab/tp_attrs/orders.py(订单处理所知道的事实)、/opt/app/tracelab/tp_attrs/failed.jsonl(第 1 步要看的失败跨度)和/opt/app/tracelab/tp_attrs/noisy.jsonl(第 7 步要用检查器去跑的别人的转储文件)。两个转储文件由/opt/app/tracelab/tp_attrs/make_fixtures.py生成。 - 常见错误:不删除转储文件就重新运行程序。转储是追加写入,跨度会累积。
- 常见错误:把关系写成属性字符串(
parent_trace_id=...)。工具无法把它们连起来,只能由人用眼睛去找。关系应该用链接。 - 这个 Pod 里没有 Collector。实际运行中,Collector 的 redaction processor 还会再过滤一遍,但这里只能看到应用程序自己过滤后的结果。后端的索引成本也无法测量,所以用唯一值的数量来代替。
- OpenTelemetry — Traces · 处理敏感数据 · 属性命名规约 · Python API 参考(add_event、Link) · Collector 配置最佳实践(redaction)
针对失败的跨度,写下缺失的值
读一读 /opt/app/tracelab/tp_attrs/failed.jsonl 中的一行。这是失败请求的跨度,但缺少复现所需的值。在 /root/tp-attrs/01-missing.txt 中写五行——每行是 <열쇠이름>=<왜 필요한가>(占位符依次为键名、为什么需要),键依次为 shop.cart.item_count、shop.request.body_bytes、shop.cache.hit、shop.queue.depth、shop.payment.retry_count。每个键的理由至少 25 个字,用自己的话写出“没有这个值就无法判断什么”。
想把转储文件展开得好看,用 python3 -m json.tool /opt/app/tracelab/tp_attrs/failed.jsonl 会很方便。跨度上现在有的只是 HTTP 的外壳——路径、状态码、长度。自己问一问,仅凭这些能不能再次触发同样的失败。评分器还会确认这五个键是不是真的不在那个跨度里。
把复现所需的值作为属性加上
创建 /root/tp-attrs/02_attrs.py。用 tracelab.tp_attrs.orders 处理订单 ord-1010,同时创建一个名为 POST /checkout 的 SERVER 跨度,在第 1 步的五个键上再加订单号 shop.order.id,共六个属性。支付调用 charge(주문번호, 시도번호)(占位符依次为订单号、尝试序号),按 1、2、3 最多尝试三次,shop.payment.retry_count 为(尝试次数 − 1)。转储默认路径为 /root/tp-attrs/02-attrs.jsonl,如果设置了环境变量 TRACELAB_OUT,就用它指定的路径。
orders.payload 返回请求体,orders.body_bytes 返回它的大小,orders.cache_lookup 和 orders.queue_depth 返回当时的状态。支付失败是 orders.PaymentError。带埋点的程序用 /opt/otel-lab/bin/python 运行——系统 python3 中没有 OpenTelemetry。
不能放的值,换成哈希、类别、长度
创建 /root/tp-attrs/03_redact.py。在第 2 步的基础上再加三个属性——shop.customer.email_hash 是邮箱 SHA-256 十六进制字符串的前 16 位,shop.payment.card_brand 是卡的 brand 值,shop.auth.token_len 是认证令牌的长度(整数)。邮箱、卡号(pan)、令牌、邮政编码的原文不能留在任何属性中。转储默认路径为 /root/tp-attrs/03-redact.jsonl。
不是把原文整个丢掉,而是换个形式留下,是因为这样仍有能回答的问题——哈希回答“是不是反复发生在同一个用户身上”,类别回答“是不是只在特定发卡机构出现”,长度回答“令牌是不是被截断后传进来的”。哈希用 hashlib.sha256(문자열.encode("utf-8")).hexdigest()(占位符为要计算哈希的字符串)生成。
带有时间点的事实移到事件里
创建 /root/tp-attrs/04_events.py。在第 3 步的基础上,(1)如果缓存未命中,留下一个 cache.miss 事件;(2)每次支付尝试失败时,各留下一个 payment.attempt.failed 事件——事件属性是 attempt(整数的尝试序号)和 reason(PaymentError 的 kind);(3)如果三次都失败,把异常消息原样放入 shop.error.message 属性,并把跨度状态改为 ERROR。转储默认路径为 /root/tp-attrs/04-events.jsonl。这一步的 shop.error.message 会在第 5 步再次出现。
重试了两次是一个数字,所以是属性;第一次重试是什么时候、为什么发生的,是带有时刻的记录,所以是事件。两者不是竞争关系——计数用属性,发生的瞬间用事件。状态用 span.set_status(Status(StatusCode.ERROR, "...")) 来修改。
数出每个键的唯一值,做出预算表
创建 /root/tp-attrs/05_bulk.py,用同样的埋点把 orders.ORDER_IDS 中的 200 个订单各处理一次(每个请求一个跨度)。转储默认路径为 /root/tp-attrs/05-bulk.jsonl。然后在 /root/tp-attrs/05-cardinality.tsv 中,为转储文件里出现的每个属性键写一行 <열쇠><탭><고유값수><탭><판정>(占位符依次为键、制表符、唯一值数量、制表符、判定),并按键名升序排列。判定规则:shop.order.id 和 shop.customer.email_hash 为 free,其余键的唯一值在 20 以下为 low,超过 20 为 leak。
标识符每个请求都不同才正常,类别型的键只应该有几种值才正常。本该是类别的位置混进原文,一个键就会有数千种值——这个转储文件里会出现一个 leak,它就是在第 4 步特意放进去的那个属性。
把团队规约写成文件
创建 /root/tp-attrs/convention.tsv。每行是一个键,用制表符分成四列 <열쇠><탭><타입><탭><허용값><탭><고유값성격>(占位符依次为键、制表符、类型、制表符、允许值、制表符、唯一值性质)。类型是 string、int、bool、deny 之一,允许值是 *(不限制)或用 | 连接的列表,唯一值性质是 free 或 low。第 5 步出现的十个键中,shop.error.message 用 deny 禁止,改为新放入 shop.error.kind,允许值为 declined|timeout。第 8 步要用的 shop.refund.amount(int、free)也要提前放进去,原文禁止键 shop.customer.email、shop.payment.card_pan、shop.auth.token 也写成 deny。deny 行的允许值和性质两列写 -。
规约只留成文字,下一个人不会去读。做成机器能读的表格,才能配上检查器。把禁止键也一并写下的原因是,“不要放”这种共识如果只在代码评审中存活,最终还是会泄漏。行数是十五。
编写能抓出违反规约的跨度的检查器
创建 /root/tp-attrs/lint_spans.py。用 python3 lint_spans.py <규약파일> <덤프>(占位符依次为规约文件、转储文件)调用时,每个违反规约的键输出一次 VIOLATION <열쇠> <이유>(占位符依次为键、原因),按键名升序,每个键只输出一次,并以退出码 1 退出;没有违反时,输出一行以 OK 开头的内容,并以退出码 0 退出。要检查四件事——规约中没有的键、被 deny 禁止的键、类型不同的值、不在允许值列表中的值。此外,声明为 low 的键,如果它在转储文件中的唯一值超过 20,也算违反。写好之后,分别在 /root/tp-attrs/05-bulk.jsonl 和 /opt/app/tracelab/tp_attrs/noisy.jsonl 上运行,并在 /root/tp-attrs/07-violations.tsv 中按这个顺序写两行 <덤프파일이름><탭><깨진 열쇠들을 쉼표로 이은 것>(占位符依次为转储文件名、制表符、被违反的键用逗号连接的结果)。
如果每个跨度都输出一次违规,会有 200 行——要按键为单位汇总后只输出一次。noisy.jsonl 是别的团队的转储文件,不能改。这一步要做的是“让机器说出哪里不符”,修改则在第 8 步用自己的代码来做。
按规约给第二个 handler 埋点,并用链接连起来
创建 /root/tp-attrs/08_refund.py,为处理由队列触发的退款 ord-1027 的 POST /refund 创建一个 SERVER 跨度。属性是 shop.order.id、shop.refund.amount、shop.queue.depth、shop.customer.email_hash、shop.payment.card_brand、shop.auth.token_len、shop.payment.retry_count,如果以失败告终,就再加上 shop.error.kind,并把状态设为 ERROR(不要放入原文消息)。每次失败的尝试都留下 payment.attempt.failed 事件,并把 orders.job_context(주문번호)(占位符为订单号)给出的跟踪坐标作为链接加上(不要挂成父级)。转储默认路径为 /root/tp-attrs/08-refund.jsonl,用第 7 步的检查器在这个转储文件上运行,必须输出 OK。
链接是先创建 Link(SpanContext(trace_id=int(16진문자열, 16), span_id=int(16진문자열, 16), is_remote=True, trace_flags=TraceFlags(0x01)))(占位符均为十六进制字符串),再传给 start_as_current_span(..., links=[link])。如果把队列任务挂成父级,就会出现一个几小时前开始、现在才结束的奇怪的父级——有关系但不是父子关系的位置,正是链接。检查器给出 VIOLATION 时,要改埋点,不要改规约。