docs: correct the plans against what the code actually does
The plan review caught three mistakes that would have gone red in the tests rather than in the implementation. Column counts: COLUMNS is the insert field list and excludes the database-filled created_at, so a stale table has 23 physical columns and a current one 25, not 22 and 24. Warning capture: the library logs through loguru, which never reaches caplog, so that assertion would have passed forever without seeing a single line. And the stale-table-under-least-privilege fixture is least_privilege_pre_tenant_dsn -- the other one builds a complete table and never reaches the missing-column path at all. Three more: make lint rewrites files, so verification uses make check; the recorder signature change now ships with its only call site instead of leaving a TypeError between two commits; and the backfill statements the library runs are not the ones it prints -- the library probes first to dodge the exclusive lock, while a script handed to a DBA has to carry IF NOT EXISTS or it cannot be run twice. On the cap side, all three clients build their emitter inside __init__, so a required parameter there would strand anyone constructing a client directly. The emitter stays required, the clients take a defaulted one.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
- **依据设计**: `research-wiki/designs/2026-08-19-issue12-telemetry-retention-design.md`(已人类审批 2026-08-19)。
|
||||
- **涉及技术**: Python 3.11+、argparse、sqlite3、asyncpg、pytest、PostgreSQL 分区与 RLS。
|
||||
- **保真校验**: **本计划不涉及参考实现迁移,保真校验不适用**。
|
||||
- **前置依赖**: **issue #13 的计划须先合并**。两条分支都会改 `config.py`(新增 settings 字段)与 `client.py`(装配透传),且本计划 Task 4 的分区模板依赖 #13 的 `telemetry_schema_sql()` 与无冲突目标的写入。本分支从 #13 合并后的 main 起。
|
||||
- **前置依赖**: **issue #13 的计划须先合并,本分支必须从合并后的 main 开出**(不可两条分支并行改再靠自动合并)。两者都动 `config.py:118-137` 的字段列表、`config.py:423-451` 的 `_load_pgw` 返回键与 `client.py:396-407` 的装配,字段顺序与返回键极易冲突且冲突后是静默的。两条分支都会改 `config.py`(新增 settings 字段)与 `client.py`(装配透传),且本计划 Task 4 的分区模板依赖 #13 的 `telemetry_schema_sql()` 与无冲突目标的写入。本分支从 #13 合并后的 main 起。
|
||||
|
||||
---
|
||||
|
||||
@@ -55,6 +55,16 @@ class TelemetryEmitter:
|
||||
) -> None: ...
|
||||
```
|
||||
|
||||
三个公共 Client 的 `__init__` 各增 keyword-only `text_cap`,**带默认值 `None`**(与既有全部可选参数同款,非破坏性):
|
||||
|
||||
```python
|
||||
class GatewayClient: # client.py:130 起的构造签名
|
||||
def __init__(self, *, ..., text_cap: int | None = None) -> None: ...
|
||||
# EmbeddingClient / OcrClient 同款
|
||||
```
|
||||
|
||||
**为什么 emitter 必填而 Client 带默认**: `TelemetryEmitter` 是库内部类,唯一构造者是这三个 Client,必填能保证没有一处漏传;而三个 Client 是**公共装配路**(下游可直接构造并注入自己的 recorder),给它们加必填参数会破坏既有调用点,且默认 `None` 恰好等于全局缺省行为(不截断)。少了这一层,直接构造的下游要么撞 `TypeError`,要么永远没法启用 cap。
|
||||
|
||||
`GatewaySettings` 新字段(无默认值),排在 `telemetry_auto_migrate` 之后:
|
||||
|
||||
```python
|
||||
@@ -80,7 +90,7 @@ telemetry_text_cap: int | None
|
||||
- [ ] **文件**: `src/polygateway/middleware/telemetry.py`、`src/polygateway/client.py`、`src/polygateway/embedding.py`、`src/polygateway/ocr.py`;`tests/unit/test_telemetry.py`、`tests/unit/test_cache.py`。
|
||||
- **行为**:
|
||||
- 按上文签名实现两个截断函数;`_record` 内在 `digest_messages(...)` 之后、`json.dumps(...)` 之前调用 `_cap_messages`,并对 `response_text`、`thinking` 调用 `_cap_text`。
|
||||
- `TelemetryEmitter` 增必填 `text_cap`;库内三个构造点(`client.py:149`、`embedding.py:131`、`ocr.py:130`)同步传参;测试内十余处构造点一并补齐。
|
||||
- `TelemetryEmitter` 增必填 `text_cap`;库内三个构造点(`client.py:149`、`embedding.py:131`、`ocr.py:130`)同步传参;**三个 Client 的 `__init__` 各增带默认值的 `text_cap` 参数**(见上,否则直接构造路要么 `TypeError` 要么永远用不上 cap);测试内十余处 emitter 构造点一并补齐。
|
||||
- **`digest_messages` 一个字节都不改**(它是缓存 key 与遥测共用的函数,`middleware/cache.py:31`)。
|
||||
- **`_cap_messages` 必须产出新对象,严禁就地修改**。这是本任务最容易踩的坑: `digest_messages` 对 content 不是 list 的消息是**原样 append 同一个 dict 对象**(`cache.py:43`),即遥测拿到的 dict 与调用方传入的、以及缓存 key 计算用的是**同一份**。就地改它会同时污染调用方的 `messages`、后续重试尝试的请求体与缓存写入的 key,且全程无任何报错。多模态 part 同理(`_digest_part` 对非 image_url 的 part 也是原样返回)。
|
||||
- `embedding.py:73` 与 `ocr.py:73` 各自的 200 字符上限**保留不动**,与新 cap 是"取更严者"的关系。
|
||||
@@ -124,8 +134,9 @@ telemetry_text_cap: int | None
|
||||
- `--older-than-days 0` 的边界(删到"此刻之前")行为明确且与文档一致。
|
||||
- 参数缺失/冲突(如 backend=sqlite 却给 `--dsn`)退出码 1。
|
||||
- `--vacuum` 不带 `--apply` 时退出码 1。
|
||||
- 先失败证据: 脚本不存在时 subprocess 返回非零且 stderr 含 `No such file`。
|
||||
- **验证**: `conda run -n PolyGateway pytest tests/unit/test_retention_tool.py -v` → PASS。
|
||||
- **PG 分支必须自带证据**(集成,真实 PG,临时 schema 隔离): ① 临时 schema 内建**分区表**,脚本探测到后打印改用 DETACH/DROP PARTITION 的提示并以退出码 **3** 结束、**一行都没删**; ② 临时 schema 内建普通表灌入跨日期的行,`--apply --batch-size 2` 后仅超期行被删且分多批提交; ③ 缺 `asyncpg` 时退出码 **2**——用一个只含 `raise ImportError` 的临时 `asyncpg.py` 目录挂进 `PYTHONPATH` 跑 subprocess 来构造该场景,不要靠 monkeypatch(脚本走的是子进程)。
|
||||
- 先失败证据: 脚本不存在时 subprocess 返回非零且 stderr 含 `No such file`;PG 三例在脚本只实现 SQLite 分支时分别以"未知 backend"或退出码 1 失败。
|
||||
- **验证**: `conda run -n PolyGateway pytest tests/unit/test_retention_tool.py tests/integration/test_retention_tool_pg.py -v` → PASS(PG 三例须在有 `PGW_TELEMETRY_PG_DSN` 的环境实跑,skip 不算通过)。
|
||||
- **提交**: `feat: add a retention script downstreams can schedule`
|
||||
|
||||
## Task 4: 生产部署模板与其机械化验收
|
||||
@@ -137,9 +148,11 @@ telemetry_text_cap: int | None
|
||||
- **分区**: `PARTITION BY RANGE (created_at)`、主键 `(call_id, created_at)`、`pg_partman` retention;并写明**分区部署下幂等键实际是 `(call_id, created_at)`**,`emit_cache_hit` 复用历史 `call_id`,故缓存命中行在普通表上第二次起会被吞掉、在分区表上每次都落一行——按 `cache_hit` 统计的下游必须知道。
|
||||
- **库需要的最小权限**: catalog SELECT(探测)+ INSERT +(可选)CREATE;auto 档另需 ALTER。
|
||||
- **合规下游推荐配置**: 一段可直接照抄的组合(`PGW_TELEMETRY_TEXT_CAP` + 分区 retention + 三角色),不把三件事散着让下游自己拼。
|
||||
- **截断覆盖面的诚实声明**(设计 §5.2,不得省): cap 作用于消息的 `content` 文本与多模态 part 中 `type == "text"` 的 `text`,与 `digest_messages` 的处理面一致;调用方放进 `tool_calls.function.arguments` 等其他字段的内容**不在覆盖范围内**。漏写这条,下游会以为开了 cap 就没有全文残留,合规判断直接出错。
|
||||
- **SQLite 侧的保留期**(设计 §6,不得省): 给按天/按实验轮转库文件的建议——这是 VT / CHSAnalyzer / dissect 三家现成的形态,比对本地文件跑 DELETE + VACUUM 更省事也更安全;`tools/telemetry_retention.py` 的 SQLite 分支是给"已经攒成一个大库"的存量场景兜底,不是推荐路径。
|
||||
- 每个代码块 ≤15 行(输出规范),超长的拆成相邻多块。
|
||||
- **验收**: 模板 SQL 在真实 PG 上逐条可执行;README 里的行为描述与实测一致。
|
||||
- **测试**(集成,真实 PG,沿用 `least_privilege_dsn` 同款临时 schema + 临时角色隔离,teardown 删净,**严禁碰共享的 `public.llm_calls`**): 新增一例,把 README 的模板 SQL 逐条执行后断言:
|
||||
- **测试**(集成,真实 PG,**新建自己的 fixture**,手法照搬 `least_privilege_dsn` 的临时 schema + 临时角色 + teardown 删净,**严禁碰共享的 `public.llm_calls`**): 新增一例,把 README 的模板 SQL 逐条执行后断言:
|
||||
- `app` 角色能 INSERT、**不能** DELETE(报权限错)。
|
||||
- `report` 角色能读、不能写。
|
||||
- 未设 `app.tenant_id` 时查询为**零行**(fail-closed),设了则只看到本租户的行。
|
||||
|
||||
Reference in New Issue
Block a user