Files
PolyGateway/src/polygateway/telemetry/sqlite.py
T
iomgaa 7b9815f4bc feat: add gateway client with env-driven assembly
Includes config aggregation for multi-source env keys, from_env and
from_settings factories with explicit shared-backend injection,
gather_bounded, top-level exports, tightened import-linter layers with
the gate removed from the Makefile, and the finalized .env.example.
2026-07-20 07:47:05 -04:00

108 lines
3.3 KiB
Python

"""SQLite 遥测后端(默认): WAL + 单持久连接 + to_thread 桥接。
蓝本 VT `adapters/telemetry.py`: 构造期建连接与表,失败降级为 no-op
(记录基础设施不得拖垮业务调用);`INSERT OR IGNORE` 幂等(call_id 主键);
写入经 threading.Lock 串行化后由 `asyncio.to_thread` 执行,不阻塞事件循环。
"""
from __future__ import annotations
import asyncio
import sqlite3
import threading
from pathlib import Path
from loguru import logger
_DDL = """
CREATE TABLE IF NOT EXISTS llm_calls (
call_id TEXT PRIMARY KEY,
parent_call_id TEXT,
session_id TEXT,
model TEXT NOT NULL,
provider TEXT NOT NULL,
source_name TEXT NOT NULL,
messages TEXT NOT NULL,
response TEXT NOT NULL,
thinking TEXT NOT NULL DEFAULT '',
prompt_tokens INTEGER NOT NULL,
completion_tokens INTEGER NOT NULL,
usage_source TEXT NOT NULL,
latency_ms INTEGER NOT NULL,
ttft_ms REAL,
max_inter_token_ms REAL,
cache_hit INTEGER NOT NULL DEFAULT 0,
error TEXT,
cost REAL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
"""
_COLUMNS = (
"call_id",
"parent_call_id",
"session_id",
"model",
"provider",
"source_name",
"messages",
"response",
"thinking",
"prompt_tokens",
"completion_tokens",
"usage_source",
"latency_ms",
"ttft_ms",
"max_inter_token_ms",
"cache_hit",
"error",
"cost",
)
_INSERT = (
f"INSERT OR IGNORE INTO llm_calls ({', '.join(_COLUMNS)}) "
f"VALUES ({', '.join('?' for _ in _COLUMNS)})"
)
class SQLiteRecorder:
"""TelemetryRecorder 端口的 SQLite 实现;初始化/写入失败全降级 warning。"""
def __init__(self, db_path: Path | str) -> None:
self._lock = threading.Lock()
self._conn: sqlite3.Connection | None = None
try:
path = Path(db_path)
path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(path, check_same_thread=False, timeout=10.0)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA busy_timeout=5000")
conn.execute(_DDL)
conn.commit()
self._conn = conn
except (OSError, sqlite3.Error) as exc:
logger.warning("SQLite 遥测初始化失败,后续记录降级为 no-op: {}", exc)
async def record_llm_call(self, **fields: object) -> None:
"""写一行遥测;字段集合即 18 字段冻结签名(ports.TelemetryRecorder)。"""
if self._conn is None:
return
row = tuple(fields[col] for col in _COLUMNS)
try:
await asyncio.to_thread(self._write, row)
except (OSError, sqlite3.Error) as exc:
logger.warning("SQLite 遥测写入失败(降级不冒泡): {}", exc)
def _write(self, row: tuple) -> None:
assert self._conn is not None # 内部不变量: 调用方已判空
with self._lock:
self._conn.execute(_INSERT, row)
self._conn.commit()
def close(self) -> None:
"""幂等关闭持久连接。"""
conn, self._conn = self._conn, None
if conn is not None:
with self._lock:
conn.close()