feat: record the observability fields end to end through telemetry

This commit is contained in:
2026-07-31 08:03:43 -04:00
parent 0ed9dc107c
commit c2fcd5b1f8
7 changed files with 307 additions and 6 deletions
+15 -2
View File
@@ -6,7 +6,7 @@
① 结构性失败(建池/建表)→ warning 一次后永久降级(池置 None 短路);
② 运行时单条写失败 → 逐条 warning 丢弃,不降级不重试(连接抖动由
asyncpg 池自恢复;避免浸泡开头一次抖动导致后续全程失遥测)。
构造不连库(lazy),18 列 schema 与 SQLite 版同名同序。
构造不连库(lazy),20 列 schema 与 SQLite 版同名同序。
"""
from __future__ import annotations
@@ -39,10 +39,18 @@ CREATE TABLE IF NOT EXISTS llm_calls (
cache_hit BOOLEAN NOT NULL DEFAULT FALSE,
error TEXT,
cost DOUBLE PRECISION,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
cached_prompt_tokens INTEGER,
model_reported TEXT
);
"""
# 新列排在 created_at 之后: 与旧表 ALTER 追加的位置一致(见 sqlite.py 同款注释)
_BACKFILL = (
"ALTER TABLE llm_calls ADD COLUMN IF NOT EXISTS cached_prompt_tokens INTEGER",
"ALTER TABLE llm_calls ADD COLUMN IF NOT EXISTS model_reported TEXT",
)
_COLUMNS = (
"call_id",
"parent_call_id",
@@ -62,6 +70,8 @@ _COLUMNS = (
"cache_hit",
"error",
"cost",
"cached_prompt_tokens",
"model_reported",
)
_INSERT = (
@@ -104,6 +114,9 @@ class PostgresRecorder:
self._pool = await asyncpg.create_pool(self._dsn, timeout=10)
async with self._pool.acquire() as conn:
await conn.execute(_DDL)
for statement in _BACKFILL:
# 已存在的旧表补新列(issue #3);ADD COLUMN IF NOT EXISTS 原生幂等
await conn.execute(statement)
self._schema_ready = True
return self._pool
except asyncio.CancelledError:
+33 -2
View File
@@ -34,10 +34,16 @@ CREATE TABLE IF NOT EXISTS llm_calls (
cache_hit INTEGER NOT NULL DEFAULT 0,
error TEXT,
cost REAL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
created_at TEXT NOT NULL DEFAULT (datetime('now')),
cached_prompt_tokens INTEGER,
model_reported TEXT
);
"""
# 新列必须排在 created_at 之后: 旧表只能经 ALTER 追加到末尾,新建库若把它们
# 插在前面,两条路径的物理列序会分叉(列序断言测试无合规修法)。
_BACKFILL_COLUMNS = (("cached_prompt_tokens", "INTEGER"), ("model_reported", "TEXT"))
_COLUMNS = (
"call_id",
"parent_call_id",
@@ -57,6 +63,8 @@ _COLUMNS = (
"cache_hit",
"error",
"cost",
"cached_prompt_tokens",
"model_reported",
)
_INSERT = (
@@ -82,9 +90,32 @@ class SQLiteRecorder:
self._conn = conn
except (OSError, sqlite3.Error) as exc:
logger.warning("SQLite 遥测初始化失败,后续记录降级为 no-op: {}", exc)
self._backfill_columns()
def _backfill_columns(self) -> None:
"""给已存在的旧表补新列(issue #3);独立 try,失败只降级为逐行丢弃。
必须放在 `self._conn` 赋值**之后**并先判空: 初始化失败时连接为 None,
无守卫的补列会抛 AttributeError 逃出 `__init__`,把"静默降级"变成崩溃。
补列失败也绝不清空 `self._conn`——那会让整个 recorder 永久 no-op,
比逐行丢弃严重得多。
"""
if self._conn is None:
return
try:
existing = {row[1] for row in self._conn.execute("PRAGMA table_info(llm_calls)")}
for column, decl in _BACKFILL_COLUMNS:
if column in existing:
continue
self._conn.execute(f"ALTER TABLE llm_calls ADD COLUMN {column} {decl}")
self._conn.commit()
except sqlite3.Error as exc:
# duplicate column: 多进程共库时后到者必然撞上,属预期竞态,视为成功
if "duplicate column" not in str(exc).lower():
logger.warning("SQLite 遥测补列失败(写入将逐行降级): {}", exc)
async def record_llm_call(self, **fields: object) -> None:
"""写一行遥测;字段集合即 18 字段冻结签名(ports.TelemetryRecorder)。"""
"""写一行遥测;字段集合即 20 字段冻结签名(ports.TelemetryRecorder)。"""
if self._conn is None:
return
row = tuple(fields[col] for col in _COLUMNS)