39fcf2631d
Postgres requires a partitioned table's unique constraints to cover the partition key, so ranging on created_at forces the primary key to (call_id, created_at) -- and ON CONFLICT (call_id) DO NOTHING then matches no constraint at all. The retention design claimed INSERT stays transparent under partitioning; that holds for the routing, not for the conflict target, and telemetry would have failed outright on any partitioned deployment. The write drops its conflict target, which is byte-equivalent on a plain table and legal on both. The cap design gains the three emitter construction sites it has to touch and the relationship to the 200-char caps embed and OCR already carry: they stay, and the new cap is the stricter of the two. Covering all three call paths is deliberate -- their rows land in one table, and issue #11 settled that argument already.
4.1 KiB
4.1 KiB
type, node_id, title, date
| type | node_id | title | date |
|---|---|---|---|
| design | design:issue12-telemetry-retention | issue #12: 遥测表的正文体量、保留期与访问控制 | 2026-08-19 |
issue #12: 遥测表的正文体量、保留期与访问控制
正文: 2026-08-19-issue12-telemetry-retention-design.md。状态: 待人类审批。同批交付 design:issue13-schema-mode。
- 选定方案: 三个子问题分层落点——(a) 正文体量: 新增
PGW_TELEMETRY_TEXT_CAP,缺省 None 即不截断,截断只发生在TelemetryEmitter._record; (b) 保留期: README 分区 +pg_partmanretention 模板 +tools/telemetry_retention.py独立脚本(默认 dry-run),库本体不持有 DELETE/DROP 权限; (c) 访问控制: 纯文档,三角色划分 +REVOKE UPDATE, DELETE+ 不可变性说明。 - 只有 (a) 改库本体代码,且它是唯一预防性手段: 没写进去的数据不需要删。
- 缺省不截断的理由(人类决策): 截断后遥测不再是审计证据、也无法复现重放,而这是既有下游正在依赖的行为,默认改动即破坏。代价是 issue 那句"无限期保留全部租户全文不应是默认状态"只解决一半——默认仍是全文,但下游第一次有了不写全文的手段。
- 按每条文本切而不是切整串 JSON: 后者产出非法 JSON,让此后一切按 JSON 解析该列的分析全废(SQLite 的
messages是 TEXT 列,不做任何 JSON 校验,坏数据静默存进去)。 - 不复用
_http_errors.summarize_body: 它折叠空白 + 保头保尾,是为错误 JSON 设计的——折叠空白会破坏正文里的代码块与缩进,保头保尾服务的是诊断而非"不想存全文"。视觉标记口径一致,实现各自独立。 - 红线:
digest_messages一个字节都不能碰(缓存 key 与遥测共用,middleware/cache.py:31),动它 = 全量缓存 miss + key 口径分叉。已设机械化验收: 同一组 messages 在 cap 开关两态下build_cache_key输出逐字节相同。 - 权限张力: 既要
REVOKE DELETE又要清理,就只能走DROP PARTITION(owner 操作)而非DELETE(应用角色)。这是分区方案不可替代的理由,不是性能偏好。 - 文档必须进 README 而非 wiki: sdist 只打包
src/与 README(无 MANIFEST.in),wiki 里的模板下游pip install后读不到——56f3805 的教训。README 的模板 SQL 另设真实 PG 集成测试逐条执行,因为下游照抄错 SQL 就中招。 - 被否决备选: 缺省即截断(所有现有下游遥测正文被静默削短);库内建 TTL/清理(库需 DELETE 权限,与 (c) 的 REVOKE 建议直接冲突,且"纯 asyncio 中立、无全局状态"铁律排斥库内定时任务);给
TelemetryRecorder加purge_before(ts)(冻结签名的端口扩展 + 同样的权限冲突);只写文档不改代码(下游唯一手段是不用遥测)。 - 共同边界(建议入 ARCHITECTURE D15): 库对下游库只做 SELECT/INSERT(加可选建表),一切改结构与删数据的操作交给下游,库的义务是把需要执行的 SQL 明明白白告诉下游。本设计与 design:issue13-schema-mode 各实现它的一面。
- 审查留痕(Codex,2026-08-19): 报 3 项,采纳 1 项、部分采纳 1 项、不采纳 1 项。① 阻断级的分区表与幂等冲突已采纳,修法归 design:issue13-schema-mode §4.6,本设计 §6.1 承接分区部署下的语义差异(缓存命中行复用历史
call_id,分区表上不再被幂等吞掉)。②text_cap漏列 emitter 构造点——缺口成立(client.py:149/embedding.py:131/ocr.py:130三处不改即TypeError),已补;但其"覆盖 embed/OCR 属语义扩散"的价值判断不采纳: 三条链路的行落同一张表,只覆盖一条会让同表内一半受控一半不受控(issue #11 同款判断),且核实后 embed 与 OCR 各已有 200 字符自有上限,新 cap 与之是"取更严者",实际影响远小于顾虑。③ "缺省不截断只解决一半"是人类已定的 E-a 决策而非疏漏,不改;作为补偿,README 须给一段可直接照抄的合规下游推荐配置(cap + 分区 retention + 三角色),不把三件事散着让下游自己拼。