|
| 1 | +-- Migration 140: OTLP metric usage rows on the canonical usage_events store |
| 2 | +-- Date: 2026-07-05 |
| 3 | +-- Purpose: Land Claude Code's native OTLP *metrics* export (token / cost / |
| 4 | +-- session / lines-of-code / tool-decision counters, POST /v1/metrics) |
| 5 | +-- as canonical usage rows in the SAME usage_events table every other |
| 6 | +-- metering plane writes to — not a parallel/satellite store. The |
| 7 | +-- existing columns are shaped for one API/LLM call each; an OTLP |
| 8 | +-- metric datapoint is a named counter increment keyed on |
| 9 | +-- session_id / user_email, so it needs its own descriptive columns. |
| 10 | +-- |
| 11 | +-- Rows written by the metrics ingest use event_type = 'claude_code_metric'. |
| 12 | +-- Because the usage rollups (usage_hourly / usage_daily, migration 081) group |
| 13 | +-- BY event_type, these rows aggregate into their own buckets and never blend |
| 14 | +-- into 'api_call' / 'llm_request' counts. Token deltas are ALSO mirrored into |
| 15 | +-- the existing prompt_tokens / completion_tokens / total_tokens columns (and |
| 16 | +-- cost into estimated_cost_cents) so the existing token/cost aggregates carry |
| 17 | +-- Claude Code usage with no rollup change. |
| 18 | +-- |
| 19 | +-- metric_value is the NORMALIZED DELTA for the datapoint (cumulative OTLP |
| 20 | +-- streams are converted to deltas at ingest using metric_series_key + |
| 21 | +-- metric_raw_value + metric_start_time; see platform/common/usage). Summing |
| 22 | +-- metric_value per metric_name is therefore always correct, regardless of the |
| 23 | +-- exporter's aggregation temporality. |
| 24 | +-- |
| 25 | +-- All columns are NULLABLE with no default: every existing writer is untouched |
| 26 | +-- and the migration is purely additive. session_id / user_email are ASSERTED |
| 27 | +-- attribution labels from the telemetry, not an authentication boundary — the |
| 28 | +-- org_id scope on every row still comes from the authenticated license. |
| 29 | + |
| 30 | +ALTER TABLE usage_events |
| 31 | + ADD COLUMN IF NOT EXISTS session_id VARCHAR(255), |
| 32 | + ADD COLUMN IF NOT EXISTS user_email VARCHAR(320), |
| 33 | + ADD COLUMN IF NOT EXISTS metric_name VARCHAR(128), |
| 34 | + ADD COLUMN IF NOT EXISTS metric_value DOUBLE PRECISION, |
| 35 | + ADD COLUMN IF NOT EXISTS metric_raw_value DOUBLE PRECISION, |
| 36 | + ADD COLUMN IF NOT EXISTS metric_temporality VARCHAR(16), |
| 37 | + ADD COLUMN IF NOT EXISTS metric_series_key VARCHAR(64), |
| 38 | + ADD COLUMN IF NOT EXISTS metric_attributes JSONB, |
| 39 | + ADD COLUMN IF NOT EXISTS metric_time TIMESTAMP WITH TIME ZONE, |
| 40 | + ADD COLUMN IF NOT EXISTS metric_start_time TIMESTAMP WITH TIME ZONE; |
| 41 | + |
| 42 | +-- OPERATIONAL NOTE: the three CREATE INDEX below are plain (non-CONCURRENT — |
| 43 | +-- the migration runner is transactional) and take a SHARE lock on usage_events |
| 44 | +-- for the build scan, briefly blocking concurrent usage writes on deployments |
| 45 | +-- with a large usage_events table. All three are PARTIAL on metric columns |
| 46 | +-- that are NULL for every pre-existing row, so the scan is the only cost — |
| 47 | +-- no rewrite. Schedule the upgrade like any other migration window. |
| 48 | + |
| 49 | +-- Cumulative→delta normalization reads the latest prior datapoint of the same |
| 50 | +-- series (org-scoped under RLS). Partial: only metric rows carry a series key, |
| 51 | +-- so api_call / llm_request rows add no index weight. |
| 52 | +CREATE INDEX IF NOT EXISTS idx_usage_events_metric_series |
| 53 | + ON usage_events(metric_series_key, id DESC) |
| 54 | + WHERE metric_series_key IS NOT NULL; |
| 55 | + |
| 56 | +-- Exact-duplicate datapoint dedup: an OTLP series emits at most one datapoint |
| 57 | +-- per timestamp, so a second row with the same (series, time) is a client |
| 58 | +-- RETRY of an export the server already committed (the client saw a timeout). |
| 59 | +-- The ingest INSERTs with ON CONFLICT DO NOTHING against this index, so the |
| 60 | +-- retry lands zero rows instead of double-counting usage. |
| 61 | +CREATE UNIQUE INDEX IF NOT EXISTS ux_usage_events_metric_point |
| 62 | + ON usage_events(metric_series_key, metric_time) |
| 63 | + WHERE metric_series_key IS NOT NULL AND metric_time IS NOT NULL; |
| 64 | + |
| 65 | +-- Session/user-keyed usage reporting ("what did this Claude Code session / |
| 66 | +-- developer consume") — the read this plane exists to serve. |
| 67 | +CREATE INDEX IF NOT EXISTS idx_usage_events_session |
| 68 | + ON usage_events(session_id, created_at DESC) |
| 69 | + WHERE session_id IS NOT NULL; |
| 70 | + |
| 71 | +COMMENT ON COLUMN usage_events.session_id IS 'AI-tool session id (OTLP session.id attribute); asserted attribution label, not an auth boundary (#2832)'; |
| 72 | +COMMENT ON COLUMN usage_events.user_email IS 'Developer email (OTLP user.email attribute); asserted attribution label, not an auth boundary (#2832)'; |
| 73 | +COMMENT ON COLUMN usage_events.metric_name IS 'OTLP metric name, e.g. claude_code.token.usage (#2832)'; |
| 74 | +COMMENT ON COLUMN usage_events.metric_value IS 'Normalized DELTA value for this datapoint — always safe to SUM per metric_name (#2832)'; |
| 75 | +COMMENT ON COLUMN usage_events.metric_raw_value IS 'Datapoint value exactly as exported (delta or cumulative per metric_temporality) (#2832)'; |
| 76 | +COMMENT ON COLUMN usage_events.metric_temporality IS 'OTLP aggregation temporality of the source stream: delta | cumulative (#2832)'; |
| 77 | +COMMENT ON COLUMN usage_events.metric_series_key IS 'SHA-256 over org + metric name + full attribute set; identifies one OTLP series for cumulative→delta normalization (#2832)'; |
| 78 | +COMMENT ON COLUMN usage_events.metric_attributes IS 'Allowlisted OTLP datapoint/resource attributes (structural identifiers only; unknown keys are dropped at ingest, never stored) (#2832)'; |
| 79 | +COMMENT ON COLUMN usage_events.metric_time IS 'Datapoint TimeUnixNano (event time); created_at stays ingest time for rollup semantics (#2832)'; |
| 80 | +COMMENT ON COLUMN usage_events.metric_start_time IS 'Datapoint StartTimeUnixNano; a changed start time marks a counter reset for delta normalization (#2832)'; |
0 commit comments