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:
Chris Coutinho
2026-06-07 15:13:14 +02:00
co-authored by Claude Opus 4.8
parent 42505f8f87
commit 1c6b1a84ea
9 changed files with 491 additions and 1 deletions
+12
View File
@@ -157,6 +157,18 @@ class TestGetSettings:
assert settings.vector_sync_processor_workers == 5
assert settings.vector_sync_queue_max_size == 5000
@patch.dict(os.environ, {}, clear=True)
def test_usage_metering_disabled_by_default(self):
"""USAGE_METERING_ENABLED defaults to False (OSS doesn't self-monitor)."""
_reload_config()
assert get_settings().usage_metering_enabled is False
@patch.dict(os.environ, {"USAGE_METERING_ENABLED": "true"}, clear=True)
def test_usage_metering_enabled_via_env(self):
"""USAGE_METERING_ENABLED=true maps to settings.usage_metering_enabled."""
_reload_config()
assert get_settings().usage_metering_enabled is True
class TestChunkConfigValidation:
"""Test document chunking configuration validation."""