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
@@ -244,6 +244,11 @@ _DEFAULTS: dict[str, Any] = {
|
||||
# this before a real ACL backfill would silently drop legacy results.
|
||||
# verify-on-read remains the correctness backstop regardless.
|
||||
"acl_prefilter_enabled": False,
|
||||
# Usage metering (Deck #67, control-plane usage-metering.md). OFF by
|
||||
# default so OSS self-hosters don't accrue a metering table or write
|
||||
# overhead; Astrolabe Cloud provisioning sets it true. When on, billable
|
||||
# ops record rows into the app-DB usage_events table (best-effort).
|
||||
"usage_metering_enabled": False,
|
||||
}
|
||||
|
||||
|
||||
@@ -829,6 +834,10 @@ class Settings:
|
||||
embedding_gateway_scope: str | None = None
|
||||
tenant_id: str | None = None # per-tenant identity (UUID form)
|
||||
acl_prefilter_enabled: bool = False # query-side ACL pre-filter (§11); OFF
|
||||
# Usage metering (Deck #67); OFF by default. When true, billable ops
|
||||
# record best-effort rows into the app-DB usage_events table for the
|
||||
# control plane to pull. See nextcloud_mcp_server/usage/store.py.
|
||||
usage_metering_enabled: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate configuration and set defaults."""
|
||||
@@ -1441,6 +1450,7 @@ def get_settings() -> Settings:
|
||||
"embedding_gateway_scope": "EMBEDDING_GATEWAY_SCOPE",
|
||||
"tenant_id": "TENANT_ID",
|
||||
"acl_prefilter_enabled": "ACL_PREFILTER_ENABLED",
|
||||
"usage_metering_enabled": "USAGE_METERING_ENABLED",
|
||||
}
|
||||
|
||||
# Only pass values that dynaconf actually has; omit unset keys so
|
||||
|
||||
Reference in New Issue
Block a user