feat(usage): record per-tenant usage events into the app DB
Deck #67 data-plane slice: tenant Pods record billable operations (embedding queries, pages/chunks embedded) into an app-DB usage_events table that the control plane later pulls read-only into the billing ledger and syncs to Stripe Meter Events. - migration 007: usage_events table (Postgres TIMESTAMPTZ/JSONB/UUID with portable SQLite fallbacks), indexed (occurred_at, metric) for the CP rollup's per-day range scan + GROUP BY metric. - UsageEventStore: best-effort, flag-gated writer reusing the shared RefreshTokenStorage engine; ON CONFLICT (event_id) DO NOTHING for idempotent retries; dialect-branched occurred_at bind. All work (incl. metadata JSON encode) is swallowed so a metering failure never surfaces to the user op. - USAGE_METERING_ENABLED flag (default off) wired through Settings + env map; off-path touches no storage, so OSS self-hosters get an empty table and zero write overhead. - two recording hooks: embeddings_queries (per nc_semantic_search, which nc_semantic_search_answer reuses) and pages_chunks (after dense embedding succeeds, covering both in-process and procrastinate paths). - storage.acquire()/.dialect public seams so the sibling store doesn't reach into the underscored internal. - tests parametrized over SQLite + Postgres: flag-off no-op, roundtrip, ON CONFLICT dedup, JSON/NULL metadata, and the best-effort swallow of both DB errors and unserializable metadata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
42505f8f87
commit
1c6b1a84ea
@@ -39,7 +39,7 @@ import os
|
||||
import socket
|
||||
import sqlite3
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -666,6 +666,21 @@ class RefreshTokenStorage:
|
||||
async with self.engine.connect() as conn:
|
||||
yield _DBConn(conn)
|
||||
|
||||
def acquire(self) -> AbstractAsyncContextManager["_DBConn"]:
|
||||
"""Public alias for :meth:`_db`: a backend-agnostic connection cm.
|
||||
|
||||
Lets sibling stores (e.g. :class:`UsageEventStore`) reuse this
|
||||
instance's engine, NullPool, and ``_DBConn`` shim without reaching
|
||||
into the underscored internal. Use as ``async with storage.acquire()
|
||||
as db:``.
|
||||
"""
|
||||
return self._db()
|
||||
|
||||
@property
|
||||
def dialect(self) -> str:
|
||||
"""Backend dialect name ("sqlite" / "postgresql"), or "unknown" pre-init."""
|
||||
return self._dialect
|
||||
|
||||
async def store_refresh_token(
|
||||
self,
|
||||
user_id: str,
|
||||
|
||||
Reference in New Issue
Block a user