遥测:上游状态码被压平、三分之二失败行没有归因列、21 条错误详情为空 #19
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
一句话
上游过载回的 529 传到调用方手里变成了
bad_response_status_code,而三分之二的失败遥测行连是哪个模型都没记。2026-08-26 那一跑为了搞清「gemini 那条渠道到底怎么了」查了两个多小时,
最后是靠去中转后台翻日志才知道上游在回 529——这两条缺口各自贡献了一半的弯路。
口径
llm_calls表,窗口 2026-08-26 07:04 ~ 14:11 UTC,共 4372 行qwen3.7-plus/gpt-5.5/gemini-3.1-pro走中转newapi.iomgaa.online,monkey-ocr走内网一、上游的状态码在中转那一层被压平,PolyGateway 原样透传,于是丢了
gemini-3.1-pro110 次调用错 46 次,其中 35 条错误正文是同一句:上游实际回的是 529(过载)。这个数是去中转后台翻日志才拿到的,
从 PolyGateway 记的这一行里看不出来——429、500、503、529 到这边长得一模一样。
这两种的处理方式是相反的:配额打满该退避重排,上游过载该换渠道或者等。
分不开就只能一律当瞬时错误重试,而重试正是过载时最不该做的事。
压平这一步是中转做的,PolyGateway 只是原样收下。但PolyGateway 是唯一有机会
把「我实际收到的 HTTP 状态码」单独记一列的地方——它手里有响应对象,中转的 body
再怎么糊,status line 是它自己看见的。现在这个数只以字符串形式出现在
error的开头(
503 | {...}),要用得先 split。建议:
llm_calls加一列存实收的 HTTP 状态码,以及一列存上游 body 的原文(现在 body 是拼进
error的,但只有出错时才有,且和我们自己的前缀混在一起)。二、三分之二的失败行没有归因列
333 条失败行里,218 条的
model、provider、source_name三列全是空串。这批是 PolyGateway 自己的渠道级拒绝:
渠道名明明就在
error那个自由文本里,却没有进任何一个结构化列。后果是「哪条渠道熔断了几次」只能拿 LIKE 去凑,而
circuit_open那 182 条恰恰是判断一条渠道健康与否最要紧的信号——排查时我是先按
model分组,得到的结论是「gemini 只错了 46 次」,直到发现还有 218 条无归因行才知道
真实的失败规模是三倍。
熔断时具体用哪个 source 可能还没选出来,但渠道是知道的。
建议:给渠道名一个自己的列。source/model/provider 选不出来时留空可以接受,
渠道名留空不行。
三、21 条错误的详情是空的
冒号后面什么都没有。一条超时不说自己撞的是哪个上限、撞的时候等了多久;
一条网络错误不说是连接失败还是读到一半断了。
这一条不是理论问题。这一跑里判断「12:22 前后中转不可达三分钟」这件事,
靠的是恰好有几条
网络错误带上了 httpx 的原文:而另外 11 条同类的是空的。同一类异常,有的带原文有的不带,
说明是某条路径上
str(exc)拿到空串就直接拼进去了。建议:拿不到
str(exc)时退回到异常类名。超时那一类还应该把撞上的是哪个上限(连接超时 / 读超时 / 流活性 / 总时长)和实际等了多久记进去。
附:这次排查里被证伪的几条猜想
留个记录,免得下次重复查。这几条在
llm_calls现有的列上都查得出来,说明这张表本身的骨架是对的,缺的是上面那三处。
circuit_open的时延中位 452 ms,179 条
nursing_extract_primary里 176 条在 10 秒内。怀疑过「先在并发闸上等满槽位、再被熔断拒掉」,不成立。
失败的均值 0.50、成功的 0.41,最大都是 1。
messages平均 5820 字符,成功的 6447,失败的反而更小,区间完全重叠。
monkey-ocr全程 1343 次只错 4 次(0.3%)。本 issue 由 1.3.5 关闭(main
ab00aa44576bd4cd47d0680d107d5191679dc1ee,tagv1.3.5,已上传 registry)。归因缺失的三条本体已修:上游状态码不再被压平(中转把 529 改写成 503 就记 503,不猜回 529);失败行补齐
http_status_code/error_type/cause_type/error_body四个归因列(成功行五列全 NULL,不统一填 200);空str()的 Connect/Read/Write/PoolTimeout 现在把 httpx 类名落进cause_type,error退回类名,不再留空。另新增scope/operation两维,operation由调用点给定(与exc.operation是两个语义),并修正了embed()非 200 历史误标为chat的问题。下游必须做的迁移(不做会静默出错)
TelemetryRecorder补 10 个 keyword-only 参数(26 → 36),或改成**fieldsValueError,不是运行期静默丢行。旧签名 recorder 升级后会在TelemetryEmitter构造时被拒telemetry_schema_sql("postgres")自取;不补则这 10 维按现有列裁剪后不落库(库发一条点名警告)WHERE event_kind = 'terminal_failure'error IS NOT NULL会跨尝试行与终态行,升级后计数变大(400 密集负载下错误行翻倍,是已批准的可见变化)AVG(latency_ms)按event_kind分组latency_ms是整次逻辑调用总耗时,与单次尝试不同量纲口径澄清(避免误读)
terminal_failure行是常规遥测口径变更,不是"异常字段":它们对每次逻辑调用恒定产生,下游按新口径统计即可,不应当成偶发脏数据过滤掉。cost IS NULL、usage_source='unavailable'、token 为 0,不参与SUM(cost),不会双计。http_status_code不可当成败判据:MonkeyOCR 的success != true就是 200 下的失败;终态行该列为 NULL(不冒充某一次尝试的状态码)。验证范围声明
已跑:合并后
make lint(import-linter 1 kept / 0 broken)、全套件make test(1605 passed / 23 skipped / 108 deselected / 95%)、真实网关 e2e 冒烟 4 节点、Redis 时间语义 slow 18 项、真实 Postgres 列兼容 30 项,以及从 registry 独立安装后的离线真链路复核(3 条 attempt + 恰 1 条 terminal_failure、诊断列记真实 503、终态cost为 NULL)。未做全模型矩阵与全渠道验证:本版未触碰推理路径,沿用 1.3.4 的型号级结论及其 FAIL/UNKNOWN/不可达例外,不代表所有模型或所有上游渠道都已验证可用。