From 69e4ddc97d0a76ca7f19ab5c006be52572504a0d Mon Sep 17 00:00:00 2001 From: chronark Date: Thu, 6 Nov 2025 18:10:25 +0100 Subject: [PATCH 01/10] chore: switch clickhouse reads to new tables --- ...1_analytics_getVerifications.happy.test.ts | 2 +- .../routes/v1_analytics_getVerifications.ts | 6 +- apps/docs/analytics/getting-started.mdx | 14 +-- .../docs/architecture/services/analytics.mdx | 10 +- .../docs/architecture/services/clickhouse.mdx | 4 +- .../docs/contributing/pull-request-checks.mdx | 22 ++-- .../multi_node_ratelimiting/run.go | 4 +- .../multi_node_usagelimiting/run.go | 4 +- .../api/routes/v2_ratelimit_limit/200_test.go | 6 +- go/pkg/clickhouse/billable_ratelimits.go | 2 +- go/pkg/clickhouse/billable_verifications.go | 2 +- internal/clickhouse/schema/000_README.md | 72 ------------- .../schema/001_create_databases.sql | 17 --- ...002_create_metrics_raw_api_requests_v1.sql | 43 -------- ...verifications_raw_key_verifications_v1.sql | 35 ------ ...ications_key_verifications_per_hour_v1.sql | 18 ---- ...fications_key_verifications_per_day_v1.sql | 19 ---- ...cations_key_verifications_per_month_v1.sql | 17 --- ...tions_key_verifications_per_hour_mv_v1.sql | 25 ----- ...ations_key_verifications_per_day_mv_v1.sql | 25 ----- ...ions_key_verifications_per_month_mv_v1.sql | 25 ----- ...create_ratelimits_raw_ratelimits_table.sql | 19 ---- .../011_create_telemetry_raw_sdks_v1.sql | 24 ----- ...ng_billable_verifications_per_month_v1.sql | 15 --- ...billable_verifications_per_month_mv_v1.sql | 21 ---- ...te_ratelimits_ratelimits_per_minute_v1.sql | 19 ---- ...eate_ratelimits_ratelimits_per_hour_v1.sql | 18 ---- ...reate_ratelimits_ratelimits_per_day_v1.sql | 19 ---- ...ate_ratelimits_ratelimits_per_month_v1.sql | 19 ---- ...ratelimits_ratelimits_per_minute_mv_v1.sql | 23 ---- ...e_ratelimits_ratelimits_per_hour_mv_v1.sql | 22 ---- ...te_ratelimits_ratelimits_per_day_mv_v1.sql | 23 ---- ..._ratelimits_ratelimits_per_month_mv_v1.sql | 22 ---- ...usiness_active_workspaces_per_month_v1.sql | 13 --- ...ness_active_workspaces_per_month_mv_v1.sql | 21 ---- .../024_create_ratelimits_last_used_mv_v1.sql | 34 ------ .../025_create_billable_verifications_v2.sql | 30 ------ .../026_create_billable_ratelimits_v1.sql | 30 ------ ...verifications.raw_key_verifications_v1.sql | 11 -- ...ications.key_verifications_per_hour_v2.sql | 19 ---- ...tions.key_verifications_per_hour_mv_v2.sql | 27 ----- ...fications.key_verifications_per_day_v2.sql | 19 ---- ...ations.key_verifications_per_day_mv_v2.sql | 27 ----- ...cations.key_verifications_per_month_v2.sql | 19 ---- ...ions.key_verifications_per_month_mv_v2.sql | 27 ----- ...cations.key_verifications_per_month_v2.sql | 33 ------ ...cations.key_verifications_per_month_v2.sql | 16 --- ...ications.key_verifications_per_hour_v3.sql | 19 ---- ...tions.key_verifications_per_hour_mv_v3.sql | 49 --------- ...fications.key_verifications_per_day_v3.sql | 19 ---- ...ations.key_verifications_per_day_mv_v3.sql | 49 --------- ...cations.key_verifications_per_month_v3.sql | 19 ---- ...ions.key_verifications_per_month_mv_v3.sql | 49 --------- .../042_create_api_requests_per_hour_v1.sql | 24 ----- ...043_create_api_requests_per_hour_mv_v1.sql | 22 ---- .../044_create_api_requests_per_minute_v1.sql | 24 ----- ...5_create_api_requests_per_minute_mv_v1.sql | 22 ---- .../046_create_api_requests_per_day_v1.sql | 24 ----- .../047_create_api_requests_per_day_mv_v1.sql | 22 ---- .../048_raw_ratelimits_metrics_indexes_v1.sql | 13 --- ...9_raw_api_metrics_ratelimit_indexes_v1.sql | 13 --- ...ations.key_verifications_per_minute_v1.sql | 19 ---- ...ons.key_verifications_per_minute_mv_v1.sql | 26 ----- internal/clickhouse/src/billing.ts | 2 +- .../clickhouse/src/logs-timeseries.test.ts | 2 +- ...ation_outcomes_propagate_correctly.test.ts | 6 +- tools/migrate/ch_logs.ts | 2 +- tools/migrate/ch_migrate_v2_simple.ts | 101 ------------------ tools/migrate/v1_deprecation.ts | 6 +- 69 files changed, 48 insertions(+), 1426 deletions(-) delete mode 100644 internal/clickhouse/schema/000_README.md delete mode 100644 internal/clickhouse/schema/001_create_databases.sql delete mode 100644 internal/clickhouse/schema/002_create_metrics_raw_api_requests_v1.sql delete mode 100644 internal/clickhouse/schema/003_create_verifications_raw_key_verifications_v1.sql delete mode 100644 internal/clickhouse/schema/004_create_verifications_key_verifications_per_hour_v1.sql delete mode 100644 internal/clickhouse/schema/005_create_verifications_key_verifications_per_day_v1.sql delete mode 100644 internal/clickhouse/schema/006_create_verifications_key_verifications_per_month_v1.sql delete mode 100644 internal/clickhouse/schema/007_create_verifications_key_verifications_per_hour_mv_v1.sql delete mode 100644 internal/clickhouse/schema/008_create_verifications_key_verifications_per_day_mv_v1.sql delete mode 100644 internal/clickhouse/schema/009_create_verifications_key_verifications_per_month_mv_v1.sql delete mode 100644 internal/clickhouse/schema/010_create_ratelimits_raw_ratelimits_table.sql delete mode 100644 internal/clickhouse/schema/011_create_telemetry_raw_sdks_v1.sql delete mode 100644 internal/clickhouse/schema/012_create_billing_billable_verifications_per_month_v1.sql delete mode 100644 internal/clickhouse/schema/013_create_billing_billable_verifications_per_month_mv_v1.sql delete mode 100644 internal/clickhouse/schema/014_create_ratelimits_ratelimits_per_minute_v1.sql delete mode 100644 internal/clickhouse/schema/015_create_ratelimits_ratelimits_per_hour_v1.sql delete mode 100644 internal/clickhouse/schema/016_create_ratelimits_ratelimits_per_day_v1.sql delete mode 100644 internal/clickhouse/schema/017_create_ratelimits_ratelimits_per_month_v1.sql delete mode 100644 internal/clickhouse/schema/018_create_ratelimits_ratelimits_per_minute_mv_v1.sql delete mode 100644 internal/clickhouse/schema/019_create_ratelimits_ratelimits_per_hour_mv_v1.sql delete mode 100644 internal/clickhouse/schema/020_create_ratelimits_ratelimits_per_day_mv_v1.sql delete mode 100644 internal/clickhouse/schema/021_create_ratelimits_ratelimits_per_month_mv_v1.sql delete mode 100644 internal/clickhouse/schema/022_create_business_active_workspaces_per_month_v1.sql delete mode 100644 internal/clickhouse/schema/023_create_business_active_workspaces_per_month_mv_v1.sql delete mode 100644 internal/clickhouse/schema/024_create_ratelimits_last_used_mv_v1.sql delete mode 100644 internal/clickhouse/schema/025_create_billable_verifications_v2.sql delete mode 100644 internal/clickhouse/schema/026_create_billable_ratelimits_v1.sql delete mode 100644 internal/clickhouse/schema/027_add_tags_to_verifications.raw_key_verifications_v1.sql delete mode 100644 internal/clickhouse/schema/028_create_verifications.key_verifications_per_hour_v2.sql delete mode 100644 internal/clickhouse/schema/029_create_verifications.key_verifications_per_hour_mv_v2.sql delete mode 100644 internal/clickhouse/schema/030_create_verifications.key_verifications_per_day_v2.sql delete mode 100644 internal/clickhouse/schema/031_create_verifications.key_verifications_per_day_mv_v2.sql delete mode 100644 internal/clickhouse/schema/032_create_verifications.key_verifications_per_month_v2.sql delete mode 100644 internal/clickhouse/schema/033_create_verifications.key_verifications_per_month_mv_v2.sql delete mode 100644 internal/clickhouse/schema/034_billing_read_from_verifications.key_verifications_per_month_v2.sql delete mode 100644 internal/clickhouse/schema/035_business_update_active_workspaces_keys_per_month_mv_v1_read_from_verifications.key_verifications_per_month_v2.sql delete mode 100644 internal/clickhouse/schema/036_create_verifications.key_verifications_per_hour_v3.sql delete mode 100644 internal/clickhouse/schema/037_create_verifications.key_verifications_per_hour_mv_v3.sql delete mode 100644 internal/clickhouse/schema/038_create_verifications.key_verifications_per_day_v3.sql delete mode 100644 internal/clickhouse/schema/039_create_verifications.key_verifications_per_day_mv_v3.sql delete mode 100644 internal/clickhouse/schema/040_create_verifications.key_verifications_per_month_v3.sql delete mode 100644 internal/clickhouse/schema/041_create_verifications.key_verifications_per_month_mv_v3.sql delete mode 100644 internal/clickhouse/schema/042_create_api_requests_per_hour_v1.sql delete mode 100644 internal/clickhouse/schema/043_create_api_requests_per_hour_mv_v1.sql delete mode 100644 internal/clickhouse/schema/044_create_api_requests_per_minute_v1.sql delete mode 100644 internal/clickhouse/schema/045_create_api_requests_per_minute_mv_v1.sql delete mode 100644 internal/clickhouse/schema/046_create_api_requests_per_day_v1.sql delete mode 100644 internal/clickhouse/schema/047_create_api_requests_per_day_mv_v1.sql delete mode 100644 internal/clickhouse/schema/048_raw_ratelimits_metrics_indexes_v1.sql delete mode 100644 internal/clickhouse/schema/049_raw_api_metrics_ratelimit_indexes_v1.sql delete mode 100644 internal/clickhouse/schema/050_create_verifications.key_verifications_per_minute_v1.sql delete mode 100644 internal/clickhouse/schema/051_create_verifications.key_verifications_per_minute_mv_v1.sql delete mode 100644 tools/migrate/ch_migrate_v2_simple.ts diff --git a/apps/api/src/routes/v1_analytics_getVerifications.happy.test.ts b/apps/api/src/routes/v1_analytics_getVerifications.happy.test.ts index e1f5a6d16a..7af72d65fe 100644 --- a/apps/api/src/routes/v1_analytics_getVerifications.happy.test.ts +++ b/apps/api/src/routes/v1_analytics_getVerifications.happy.test.ts @@ -73,7 +73,7 @@ describe.each([ const inserted = await h.ch.querier.query({ query: - "SELECT COUNT(*) AS count from verifications.raw_key_verifications_v1 WHERE workspace_id={workspaceId:String}", + "SELECT COUNT(*) AS count from default.key_verifications_raw_v2 WHERE workspace_id={workspaceId:String}", params: z.object({ workspaceId: z.string() }), schema: z.object({ count: z.number() }), })({ diff --git a/apps/api/src/routes/v1_analytics_getVerifications.ts b/apps/api/src/routes/v1_analytics_getVerifications.ts index f166d9f6e9..38485ae8d2 100644 --- a/apps/api/src/routes/v1_analytics_getVerifications.ts +++ b/apps/api/src/routes/v1_analytics_getVerifications.ts @@ -222,21 +222,21 @@ export const registerV1AnalyticsGetVerifications = (app: App) => const tables = { hour: { - name: "verifications.key_verifications_per_hour_v3", + name: "default.key_verifications_per_hour_v2", fill: `WITH FILL FROM toStartOfHour(fromUnixTimestamp64Milli({ start: Int64 })) TO toStartOfHour(fromUnixTimestamp64Milli({ end: Int64 })) STEP INTERVAL 1 HOUR`, }, day: { - name: "verifications.key_verifications_per_day_v3", + name: "default.key_verifications_per_day_v2", fill: `WITH FILL FROM toStartOfDay(fromUnixTimestamp64Milli({ start: Int64 })) TO toStartOfDay(fromUnixTimestamp64Milli({ end: Int64 })) STEP INTERVAL 1 DAY`, }, month: { - name: "verifications.key_verifications_per_month_v3", + name: "default.key_verifications_per_month_v2", fill: `WITH FILL FROM toDateTime(toStartOfMonth(fromUnixTimestamp64Milli({ start: Int64 }))) TO toDateTime(toStartOfMonth(fromUnixTimestamp64Milli({ end: Int64 }))) diff --git a/apps/docs/analytics/getting-started.mdx b/apps/docs/analytics/getting-started.mdx index 1ea887bceb..031464310c 100644 --- a/apps/docs/analytics/getting-started.mdx +++ b/apps/docs/analytics/getting-started.mdx @@ -44,7 +44,7 @@ Count the total number of key verifications in the last 7 days across all your A ```sql SQL SELECT SUM(count) as total -FROM key_verifications_per_day_v1 +FROM key_verifications_per_day_v2 WHERE time >= now() - INTERVAL 7 DAY ``` @@ -53,7 +53,7 @@ curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ - "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY" + "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v2 WHERE time >= now() - INTERVAL 7 DAY" }' ``` @@ -94,7 +94,7 @@ Identify your most active users by counting their total verifications over the l SELECT external_id, SUM(count) as verifications -FROM key_verifications_per_day_v1 +FROM key_verifications_per_day_v2 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id ORDER BY verifications DESC @@ -114,10 +114,10 @@ curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ **Performance tip:** For longer time ranges, use pre-aggregated tables instead of the raw table: - - `key_verifications_per_minute_v1` - For queries spanning hours - - `key_verifications_per_hour_v1` - For queries spanning days - - `key_verifications_per_day_v1` - For queries spanning weeks/months - - `key_verifications_per_month_v1` - For queries spanning years + - `key_verifications_per_minute_v2` - For queries spanning hours + - `key_verifications_per_hour_v2` - For queries spanning days + - `key_verifications_per_day_v2` - For queries spanning weeks/months + - `key_verifications_per_month_v2` - For queries spanning years Use `SUM(count)` instead of `COUNT(*)` with aggregated tables. They scan far fewer rows and are much faster. diff --git a/apps/engineering/content/docs/architecture/services/analytics.mdx b/apps/engineering/content/docs/architecture/services/analytics.mdx index c991edf7e9..19625ac427 100644 --- a/apps/engineering/content/docs/architecture/services/analytics.mdx +++ b/apps/engineering/content/docs/architecture/services/analytics.mdx @@ -64,11 +64,11 @@ Users query against friendly table names that map to actual ClickHouse tables: ```go TableAliases: map[string]string{ - "key_verifications": "default.key_verifications_raw_v2", - "key_verifications_per_minute": "default.key_verifications_per_minute_v2", - "key_verifications_per_hour": "default.key_verifications_per_hour_v2", - "key_verifications_per_day": "default.key_verifications_per_day_v2", - "key_verifications_per_month": "default.key_verifications_per_month_v2", + "key_verifications_v1": "default.key_verifications_raw_v2", + "key_verifications_per_minute_v1": "default.key_verifications_per_minute_v2", + "key_verifications_per_hour_v1": "default.key_verifications_per_hour_v2", + "key_verifications_per_day_v1": "default.key_verifications_per_day_v2", + "key_verifications_per_month_v1": "default.key_verifications_per_month_v2", } ``` diff --git a/apps/engineering/content/docs/architecture/services/clickhouse.mdx b/apps/engineering/content/docs/architecture/services/clickhouse.mdx index 17a48888fa..a05268bc13 100644 --- a/apps/engineering/content/docs/architecture/services/clickhouse.mdx +++ b/apps/engineering/content/docs/architecture/services/clickhouse.mdx @@ -170,7 +170,7 @@ If you need to understand or optimize a query: 1. Use the client's query method directly for custom queries: ```typescript const result = await ch.querier.query({ - query: `SELECT count() FROM verifications.raw_key_verifications_v1 WHERE workspace_id = {workspaceId: String}`, + query: `SELECT count() FROM raw_key_verifications_raw_v2 WHERE workspace_id = {workspaceId: String}`, params: z.object({ workspaceId: z.string() }), schema: z.object({ count: z.number() }) })({ workspaceId: "ws_123" }); @@ -179,7 +179,7 @@ If you need to understand or optimize a query: 2. Check performance with `EXPLAIN`: ```typescript const explain = await ch.querier.query({ - query: `EXPLAIN SELECT * FROM verifications.raw_key_verifications_v1 WHERE workspace_id = {workspaceId: String}`, + query: `EXPLAIN SELECT * FROM raw_key_verifications_raw_v2 WHERE workspace_id = {workspaceId: String}`, params: z.object({ workspaceId: z.string() }), schema: z.object({ explain: z.string() }) })({ workspaceId: "ws_123" }); diff --git a/apps/engineering/content/docs/contributing/pull-request-checks.mdx b/apps/engineering/content/docs/contributing/pull-request-checks.mdx index 868f21280c..286dd8ee01 100644 --- a/apps/engineering/content/docs/contributing/pull-request-checks.mdx +++ b/apps/engineering/content/docs/contributing/pull-request-checks.mdx @@ -144,7 +144,7 @@ This ensures all values are properly escaped by ClickHouse's query engine. ### SQL Performance - ClickHouse -When querying ClickHouse, we have to be careful about what we are joining or querying since that particular data might be huge. For example, not handling queries to metrics.raw_api_requests_v1 properly can be quite problematic. +When querying ClickHouse, we have to be careful about what we are joining or querying since that particular data might be huge. For example, not handling queries to api_requests_raw_v2 properly can be quite problematic. To improve query performance: - Filter data as early as possible in the query @@ -163,8 +163,8 @@ To improve query performance: r.identifier, r.passed, m.host, - FROM ratelimits.raw_ratelimits_v1 r - LEFT JOIN metrics.raw_api_requests_v1 m ON + FROM ratelimits_raw_v2 r + LEFT JOIN api_requests_raw_v2 m ON r.request_id = m.request_id WHERE r.workspace_id = {workspaceId: String} AND r.namespace_id = {namespaceId: String} @@ -177,8 +177,8 @@ To improve query performance: LIMIT {limit: Int} ```` -Imagine this query: we are trying to get ratelimit details by joining with raw_api_requests_v1. -Since raw_api_requests_v1 might be huge, the query can take a long time to execute or consume too much memory. +Imagine this query: we are trying to get ratelimit details by joining with api_requests_raw_v2. +Since api_requests_raw_v2 might be huge, the query can take a long time to execute or consume too much memory. To optimize this, we should first filter as much as possible, with time and workspace_id being the most important factors. We can make the query much faster by eliminating unrelated workspaceId's and irrelevant time ranges. @@ -192,7 +192,7 @@ WITH filtered_ratelimits AS ( namespace_id, identifier, toUInt8(passed) as status - FROM ratelimits.raw_ratelimits_v1 r + FROM ratelimits_raw_v2 r WHERE workspace_id = {workspaceId: String} AND namespace_id = {namespaceId: String} AND time BETWEEN {startTime: UInt64} AND {endTime: UInt64} @@ -212,7 +212,7 @@ SELECT m.host, FROM filtered_ratelimits fr LEFT JOIN ( - SELECT * FROM metrics.raw_api_requests_v1 + SELECT * FROM api_requests_raw_v2 -- Those two filters are doing the heavy lifting now WHERE workspace_id = {workspaceId: String} AND time BETWEEN {startTime: UInt64} AND {endTime: UInt64} @@ -228,19 +228,19 @@ Finally, don't forget about adding indexes for our filters. While this step is o -- Composite index for workspace + time filtering -- Most effective when filtering workspace_id first, then time -- MINMAX type creates a sparse index with min/max values -ALTER TABLE ratelimits.raw_ratelimits_v1 +ALTER TABLE ratelimits_raw_v2 ADD INDEX idx_workspace_time (workspace_id, time) TYPE minmax GRANULARITY 1; -- Single-column index for JOIN operations -- Speeds up request_id matching between tables -- GRANULARITY 1 means finest possible index precision -ALTER TABLE ratelimits.raw_ratelimits_v1 +ALTER TABLE ratelimits_raw_v2 ADD INDEX idx_request_id (request_id) TYPE minmax GRANULARITY 1; -- Same indexes on metrics table to optimize both sides of JOIN -ALTER TABLE metrics.raw_api_requests_v1 +ALTER TABLE api_requests_raw_v2 ADD INDEX idx_workspace_time (workspace_id, time) TYPE minmax GRANULARITY 1; -ALTER TABLE metrics.raw_api_requests_v1 +ALTER TABLE api_requests_raw_v2 ADD INDEX idx_request_id (request_id) TYPE minmax GRANULARITY 1; ``` diff --git a/go/apps/api/integration/multi_node_ratelimiting/run.go b/go/apps/api/integration/multi_node_ratelimiting/run.go index 6bf0609240..832b053319 100644 --- a/go/apps/api/integration/multi_node_ratelimiting/run.go +++ b/go/apps/api/integration/multi_node_ratelimiting/run.go @@ -179,7 +179,7 @@ func RunRateLimitTest( data, selectErr := clickhouse.Select[aggregatedCounts]( ctx, h.CH.Conn(), - `SELECT count(*) as total_requests, countIf(passed > 0) as success_count, countIf(passed = 0) as failure_count FROM ratelimits.raw_ratelimits_v1 WHERE workspace_id = {workspace_id:String} AND namespace_id = {namespace_id:String}`, + `SELECT count(*) as total_requests, countIf(passed > 0) as success_count, countIf(passed = 0) as failure_count FROM default.ratelimits_raw_v2 WHERE workspace_id = {workspace_id:String} AND namespace_id = {namespace_id:String}`, map[string]string{ "workspace_id": h.Resources().UserWorkspace.ID, "namespace_id": namespaceID, @@ -205,7 +205,7 @@ func RunRateLimitTest( metricsCount := uint64(0) uniqueCount := uint64(0) - row := h.CH.Conn().QueryRow(ctx, fmt.Sprintf(`SELECT count(*) as total_requests, count(DISTINCT request_id) as unique_requests FROM metrics.raw_api_requests_v1 WHERE workspace_id = '%s';`, h.Resources().UserWorkspace.ID)) + row := h.CH.Conn().QueryRow(ctx, fmt.Sprintf(`SELECT count(*) as total_requests, count(DISTINCT request_id) as unique_requests FROM default.api_requests_raw_v2 WHERE workspace_id = '%s';`, h.Resources().UserWorkspace.ID)) err = row.Scan(&metricsCount, &uniqueCount) diff --git a/go/apps/api/integration/multi_node_usagelimiting/run.go b/go/apps/api/integration/multi_node_usagelimiting/run.go index fe079ac88b..bd79f56aa4 100644 --- a/go/apps/api/integration/multi_node_usagelimiting/run.go +++ b/go/apps/api/integration/multi_node_usagelimiting/run.go @@ -176,7 +176,7 @@ func RunUsageLimitTest( data, selectErr := clickhouse.Select[aggregatedCounts]( ctx, h.CH.Conn(), - `SELECT count(*) as total_requests, countIf(outcome = 'VALID') as success_count, countIf(outcome = 'USAGE_EXCEEDED') as failure_count FROM verifications.raw_key_verifications_v1 WHERE workspace_id = {workspace_id:String} AND key_id = {key_id:String}`, + `SELECT count(*) as total_requests, countIf(outcome = 'VALID') as success_count, countIf(outcome = 'USAGE_EXCEEDED') as failure_count FROM default.key_verifications_raw_v2 WHERE workspace_id = {workspace_id:String} AND key_id = {key_id:String}`, map[string]string{ "workspace_id": h.Resources().UserWorkspace.ID, "key_id": keyResponse.KeyID, @@ -203,7 +203,7 @@ func RunUsageLimitTest( require.Eventually(t, func() bool { metricsCount := uint64(0) uniqueCount := uint64(0) - row := h.CH.Conn().QueryRow(ctx, fmt.Sprintf(`SELECT count(*) as total_requests, count(DISTINCT request_id) as unique_requests FROM metrics.raw_api_requests_v1 WHERE workspace_id = '%s';`, h.Resources().UserWorkspace.ID)) + row := h.CH.Conn().QueryRow(ctx, fmt.Sprintf(`SELECT count(*) as total_requests, count(DISTINCT request_id) as unique_requests FROM default.api_requests_raw_v2 WHERE workspace_id = '%s';`, h.Resources().UserWorkspace.ID)) err := row.Scan(&metricsCount, &uniqueCount) require.NoError(t, err) diff --git a/go/apps/api/routes/v2_ratelimit_limit/200_test.go b/go/apps/api/routes/v2_ratelimit_limit/200_test.go index 0a03784859..41dd6298fa 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/200_test.go @@ -127,13 +127,13 @@ func TestLimitSuccessfully(t *testing.T) { res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) require.Equal(t, 200, res.Status, "expected 200, received: %v", res.Body) - row := schema.RatelimitRequestV1{} + row := schema.RatelimitV2{} require.Eventually(t, func() bool { - data, err := clickhouse.Select[schema.RatelimitRequestV1]( + data, err := clickhouse.Select[schema.RatelimitV2]( ctx, h.ClickHouse.Conn(), - "SELECT * FROM ratelimits.raw_ratelimits_v1 WHERE workspace_id = {workspace_id:String} AND namespace_id = {namespace_id:String}", + "SELECT * FROM default.ratelimits_raw_v2 WHERE workspace_id = {workspace_id:String} AND namespace_id = {namespace_id:String}", map[string]string{ "workspace_id": h.Resources().UserWorkspace.ID, "namespace_id": namespaceID, diff --git a/go/pkg/clickhouse/billable_ratelimits.go b/go/pkg/clickhouse/billable_ratelimits.go index f27c6e5d82..2ef8c0f093 100644 --- a/go/pkg/clickhouse/billable_ratelimits.go +++ b/go/pkg/clickhouse/billable_ratelimits.go @@ -22,7 +22,7 @@ func (c *clickhouse) GetBillableRatelimits(ctx context.Context, workspaceID stri query := ` SELECT sum(count) as count - FROM billing.billable_ratelimits_per_month_v1 + FROM billing.billable_ratelimits_per_month_v2 WHERE workspace_id = ? AND year = ? AND month = ? diff --git a/go/pkg/clickhouse/billable_verifications.go b/go/pkg/clickhouse/billable_verifications.go index 6c6b85986f..7713c12956 100644 --- a/go/pkg/clickhouse/billable_verifications.go +++ b/go/pkg/clickhouse/billable_verifications.go @@ -22,7 +22,7 @@ func (c *clickhouse) GetBillableVerifications(ctx context.Context, workspaceID s query := ` SELECT sum(count) as count - FROM billing.billable_verifications_per_month_v2 + FROM billable_verifications_per_month_v2 WHERE workspace_id = ? AND year = ? AND month = ? diff --git a/internal/clickhouse/schema/000_README.md b/internal/clickhouse/schema/000_README.md deleted file mode 100644 index b1d380abf9..0000000000 --- a/internal/clickhouse/schema/000_README.md +++ /dev/null @@ -1,72 +0,0 @@ - -# ClickHouse Table Naming Conventions - -This document outlines the naming conventions for tables and materialized views in our ClickHouse setup. Adhering to these conventions ensures consistency, clarity, and ease of management across our data infrastructure. - -## General Rules - -1. Use lowercase letters and separate words with underscores. -2. Avoid ClickHouse reserved words and special characters in names. -3. Be descriptive but concise. - -## Table Naming Convention - -Format: `[prefix]_[domain]_[description]_[version]` - -### Prefixes - -- `raw_`: Input data tables -- `tmp_{yourname}_`: Temporary tables for experiments, add your name, so it's easy to identify ownership. - -### Domain/Category - -Include the domain or category of the data when applicable. - -Examples: -- `keys` -- `audit` -- `user` -- `gateway` - -### Versioning - -- Version numbers: `_v1`, `_v2`, etc. - -### Aggregation Suffixes - -For aggregated or summary tables, use suffixes like: -- `_per_day` -- `_per_month` -- `_summary` - -## Materialized View Naming Convention - -Format: `[description]_[aggregation]_mv_[version]` - -- Always suffix with `mv_[version]` -- Include a description of the view's purpose -- Add aggregation level if applicable - -## Examples - -1. Raw Data Table: - `raw_sales_transactions_v1` - -2. Materialized View: - `active_users_per_day_mv_v2` - -3. Temporary Table: - `tmp_andreas_user_analysis_v1` - -4. Aggregated Table: - `sales_summary_per_hour_mv_v1` - -## Consistency Across Related Objects - -Maintain consistent naming across related tables, views, and other objects: - -- `raw_user_activity_v1` -- `user_activity_per_day_v1` -- `user_activity_per_day_mv_v1` - -By following these conventions, we ensure a clear, consistent, and scalable naming structure for our ClickHouse setup. diff --git a/internal/clickhouse/schema/001_create_databases.sql b/internal/clickhouse/schema/001_create_databases.sql deleted file mode 100644 index c8b10934e7..0000000000 --- a/internal/clickhouse/schema/001_create_databases.sql +++ /dev/null @@ -1,17 +0,0 @@ --- +goose up - -CREATE DATABASE verifications; -CREATE DATABASE telemetry; -CREATE DATABASE metrics; -CREATE DATABASE ratelimits; -CREATE DATABASE business; -CREATE DATABASE billing; - - --- +goose down -DROP DATABASE verifications; -DROP DATABASE telemetry; -DROP DATABASE metrics; -DROP DATABASE ratelimits; -DROP DATABASE business; -DROP DATABASE billing; diff --git a/internal/clickhouse/schema/002_create_metrics_raw_api_requests_v1.sql b/internal/clickhouse/schema/002_create_metrics_raw_api_requests_v1.sql deleted file mode 100644 index 829f5f26e2..0000000000 --- a/internal/clickhouse/schema/002_create_metrics_raw_api_requests_v1.sql +++ /dev/null @@ -1,43 +0,0 @@ --- +goose up -CREATE TABLE metrics.raw_api_requests_v1( - request_id String, - -- unix milli - time Int64, - - workspace_id String, - - host String, - - -- Upper case HTTP method - -- Examples: "GET", "POST", "PUT", "DELETE" - method LowCardinality(String), - path String, - -- "Key: Value" pairs - request_headers Array(String), - request_body String, - - response_status Int, - -- "Key: Value" pairs - response_headers Array(String), - response_body String, - -- internal err.Error() string, empty if no error - error String, - - -- milliseconds - service_latency Int64, - - user_agent String, - ip_address String, - country String, - city String, - colo String, - continent String, - - -) -ENGINE = MergeTree() -ORDER BY (workspace_id, time, request_id) -; - --- +goose down -DROP TABLE metrics.raw_api_requests_v1; diff --git a/internal/clickhouse/schema/003_create_verifications_raw_key_verifications_v1.sql b/internal/clickhouse/schema/003_create_verifications_raw_key_verifications_v1.sql deleted file mode 100644 index 9edb56bd49..0000000000 --- a/internal/clickhouse/schema/003_create_verifications_raw_key_verifications_v1.sql +++ /dev/null @@ -1,35 +0,0 @@ --- +goose up -CREATE TABLE verifications.raw_key_verifications_v1( - -- the api request id, so we can correlate the verification with traces and logs - request_id String, - - -- unix milli - time Int64, - - workspace_id String, - key_space_id String, - key_id String, - - -- Right now this is a 3 character airport code, but when we move to aws, - -- this will be the region code such as `us-east-1` - region LowCardinality(String), - - -- Examples: - -- - "VALID" - -- - "RATE_LIMITED" - -- - "EXPIRED" - -- - "DISABLED - outcome LowCardinality(String), - - -- Empty string if the key has no identity - identity_id String, - - - -) -ENGINE = MergeTree() -ORDER BY (workspace_id, key_space_id, key_id, time) -; - --- +goose down -DROP TABLE verifications.raw_key_verifications_v1; diff --git a/internal/clickhouse/schema/004_create_verifications_key_verifications_per_hour_v1.sql b/internal/clickhouse/schema/004_create_verifications_key_verifications_per_hour_v1.sql deleted file mode 100644 index bfdf27092a..0000000000 --- a/internal/clickhouse/schema/004_create_verifications_key_verifications_per_hour_v1.sql +++ /dev/null @@ -1,18 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_hour_v1 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, time, identity_id, key_id) -; - - --- +goose down -DROP TABLE verifications.key_verifications_per_hour_v1; diff --git a/internal/clickhouse/schema/005_create_verifications_key_verifications_per_day_v1.sql b/internal/clickhouse/schema/005_create_verifications_key_verifications_per_day_v1.sql deleted file mode 100644 index d2d8ceb2e6..0000000000 --- a/internal/clickhouse/schema/005_create_verifications_key_verifications_per_day_v1.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_day_v1 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, time, identity_id, key_id) -; - - --- +goose down -DROP TABLE verifications.key_verifications_per_day_v1; - diff --git a/internal/clickhouse/schema/006_create_verifications_key_verifications_per_month_v1.sql b/internal/clickhouse/schema/006_create_verifications_key_verifications_per_month_v1.sql deleted file mode 100644 index f849f136ed..0000000000 --- a/internal/clickhouse/schema/006_create_verifications_key_verifications_per_month_v1.sql +++ /dev/null @@ -1,17 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_month_v1 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, time, identity_id, key_id) -; - --- +goose down -DROP TABLE verifications.key_verifications_per_month_v1; diff --git a/internal/clickhouse/schema/007_create_verifications_key_verifications_per_hour_mv_v1.sql b/internal/clickhouse/schema/007_create_verifications_key_verifications_per_hour_mv_v1.sql deleted file mode 100644 index 4219ab2404..0000000000 --- a/internal/clickhouse/schema/007_create_verifications_key_verifications_per_hour_mv_v1.sql +++ /dev/null @@ -1,25 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW verifications.key_verifications_per_hour_mv_v1 -TO verifications.key_verifications_per_hour_v1 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfHour(fromUnixTimestamp64Milli(time)) AS time -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time -; - - --- +goose down -DROP VIEW verifications.key_verifications_per_hour_mv_v1; diff --git a/internal/clickhouse/schema/008_create_verifications_key_verifications_per_day_mv_v1.sql b/internal/clickhouse/schema/008_create_verifications_key_verifications_per_day_mv_v1.sql deleted file mode 100644 index 76a050afd2..0000000000 --- a/internal/clickhouse/schema/008_create_verifications_key_verifications_per_day_mv_v1.sql +++ /dev/null @@ -1,25 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW verifications.key_verifications_per_day_mv_v1 -TO verifications.key_verifications_per_day_v1 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfDay(fromUnixTimestamp64Milli(time)) AS time -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time -; - - --- +goose down -DROP VIEW verifications.key_verifications_per_day_mv_v1; diff --git a/internal/clickhouse/schema/009_create_verifications_key_verifications_per_month_mv_v1.sql b/internal/clickhouse/schema/009_create_verifications_key_verifications_per_month_mv_v1.sql deleted file mode 100644 index e37e73bf4f..0000000000 --- a/internal/clickhouse/schema/009_create_verifications_key_verifications_per_month_mv_v1.sql +++ /dev/null @@ -1,25 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW verifications.key_verifications_per_month_mv_v1 -TO verifications.key_verifications_per_month_v1 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfMonth(fromUnixTimestamp64Milli(time)) AS time -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time -; - - --- +goose down -DROP VIEW verifications.key_verifications_per_month_mv_v1; diff --git a/internal/clickhouse/schema/010_create_ratelimits_raw_ratelimits_table.sql b/internal/clickhouse/schema/010_create_ratelimits_raw_ratelimits_table.sql deleted file mode 100644 index 755dd1d95d..0000000000 --- a/internal/clickhouse/schema/010_create_ratelimits_raw_ratelimits_table.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE ratelimits.raw_ratelimits_v1( - request_id String, - -- unix milli - time Int64, - workspace_id String, - namespace_id String, - identifier String, - passed Bool - -) -ENGINE = MergeTree() -ORDER BY (workspace_id, namespace_id, time, identifier) -; - - - --- +goose down -DROP TABLE ratelimits.raw_ratelimits_v1; diff --git a/internal/clickhouse/schema/011_create_telemetry_raw_sdks_v1.sql b/internal/clickhouse/schema/011_create_telemetry_raw_sdks_v1.sql deleted file mode 100644 index 3b7a6ad67c..0000000000 --- a/internal/clickhouse/schema/011_create_telemetry_raw_sdks_v1.sql +++ /dev/null @@ -1,24 +0,0 @@ --- +goose up -CREATE TABLE telemetry.raw_sdks_v1( - -- the api request id, so we can correlate the telemetry with traces and logs - request_id String, - - -- unix milli - time Int64, - - -- ie: node@20 - runtime String, - -- ie: vercel - platform String, - - -- ie: [ "@unkey/api@1.2.3", "@unkey/ratelimit@4.5.6" ] - versions Array(String) -) -ENGINE = MergeTree() -ORDER BY (request_id, time) -; - - - --- +goose down -DROP TABLE telemetry.raw_sdks_v1; diff --git a/internal/clickhouse/schema/012_create_billing_billable_verifications_per_month_v1.sql b/internal/clickhouse/schema/012_create_billing_billable_verifications_per_month_v1.sql deleted file mode 100644 index 31dfaf6fe4..0000000000 --- a/internal/clickhouse/schema/012_create_billing_billable_verifications_per_month_v1.sql +++ /dev/null @@ -1,15 +0,0 @@ --- +goose up -CREATE TABLE billing.billable_verifications_per_month_v1 -( - year Int, - month Int, - workspace_id String, - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, year, month) -; - - --- +goose down -DROP TABLE billing.billable_verifications_per_month_v1; diff --git a/internal/clickhouse/schema/013_create_billing_billable_verifications_per_month_mv_v1.sql b/internal/clickhouse/schema/013_create_billing_billable_verifications_per_month_mv_v1.sql deleted file mode 100644 index 00e8005f1d..0000000000 --- a/internal/clickhouse/schema/013_create_billing_billable_verifications_per_month_mv_v1.sql +++ /dev/null @@ -1,21 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW billing.billable_verifications_per_month_mv_v1 -TO billing.billable_verifications_per_month_v1 -AS -SELECT - workspace_id, - count(*) AS count, - toYear(time) AS year, - toMonth(time) AS month -FROM verifications.key_verifications_per_month_v1 -WHERE outcome = 'VALID' -GROUP BY - workspace_id, - year, - month -; - - - --- +goose down -DROP VIEW billing.billable_verifications_per_month_mv_v1; diff --git a/internal/clickhouse/schema/014_create_ratelimits_ratelimits_per_minute_v1.sql b/internal/clickhouse/schema/014_create_ratelimits_ratelimits_per_minute_v1.sql deleted file mode 100644 index cfc1b4b6a9..0000000000 --- a/internal/clickhouse/schema/014_create_ratelimits_ratelimits_per_minute_v1.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE ratelimits.ratelimits_per_minute_v1 -( - time DateTime, - workspace_id String, - namespace_id String, - identifier String, - - passed Int64, - total Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, namespace_id, time, identifier) -; - - - --- +goose down -DROP TABLE ratelimits.ratelimits_per_minute_v1; diff --git a/internal/clickhouse/schema/015_create_ratelimits_ratelimits_per_hour_v1.sql b/internal/clickhouse/schema/015_create_ratelimits_ratelimits_per_hour_v1.sql deleted file mode 100644 index 1cc5f6aca2..0000000000 --- a/internal/clickhouse/schema/015_create_ratelimits_ratelimits_per_hour_v1.sql +++ /dev/null @@ -1,18 +0,0 @@ --- +goose up -CREATE TABLE ratelimits.ratelimits_per_hour_v1 -( - time DateTime, - workspace_id String, - namespace_id String, - identifier String, - - passed Int64, - total Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, namespace_id, time, identifier) -; - - --- +goose down -DROP TABLE ratelimits.ratelimits_per_hour_v1; diff --git a/internal/clickhouse/schema/016_create_ratelimits_ratelimits_per_day_v1.sql b/internal/clickhouse/schema/016_create_ratelimits_ratelimits_per_day_v1.sql deleted file mode 100644 index 57b7b9febf..0000000000 --- a/internal/clickhouse/schema/016_create_ratelimits_ratelimits_per_day_v1.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE ratelimits.ratelimits_per_day_v1 -( - time DateTime, - workspace_id String, - namespace_id String, - identifier String, - - passed Int64, - total Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, namespace_id, time, identifier) -; - - - --- +goose down -DROP TABLE ratelimits.ratelimits_per_day_v1; diff --git a/internal/clickhouse/schema/017_create_ratelimits_ratelimits_per_month_v1.sql b/internal/clickhouse/schema/017_create_ratelimits_ratelimits_per_month_v1.sql deleted file mode 100644 index ffd91a5f63..0000000000 --- a/internal/clickhouse/schema/017_create_ratelimits_ratelimits_per_month_v1.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE ratelimits.ratelimits_per_month_v1 -( - time DateTime, - workspace_id String, - namespace_id String, - identifier String, - - passed Int64, - total Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, namespace_id, time, identifier) -; - - - --- +goose down -DROP TABLE ratelimits.ratelimits_per_month_v1; diff --git a/internal/clickhouse/schema/018_create_ratelimits_ratelimits_per_minute_mv_v1.sql b/internal/clickhouse/schema/018_create_ratelimits_ratelimits_per_minute_mv_v1.sql deleted file mode 100644 index 93487cd52e..0000000000 --- a/internal/clickhouse/schema/018_create_ratelimits_ratelimits_per_minute_mv_v1.sql +++ /dev/null @@ -1,23 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW ratelimits.ratelimits_per_minute_mv_v1 -TO ratelimits.ratelimits_per_minute_v1 -AS -SELECT - workspace_id, - namespace_id, - identifier, - countIf(passed > 0) as passed, - count(*) as total, - toStartOfMinute(fromUnixTimestamp64Milli(time)) AS time -FROM ratelimits.raw_ratelimits_v1 -GROUP BY - workspace_id, - namespace_id, - identifier, - time -; - - - --- +goose down -DROP VIEW ratelimits.ratelimits_per_minute_mv_v1; diff --git a/internal/clickhouse/schema/019_create_ratelimits_ratelimits_per_hour_mv_v1.sql b/internal/clickhouse/schema/019_create_ratelimits_ratelimits_per_hour_mv_v1.sql deleted file mode 100644 index fb423d933e..0000000000 --- a/internal/clickhouse/schema/019_create_ratelimits_ratelimits_per_hour_mv_v1.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW ratelimits.ratelimits_per_hour_mv_v1 -TO ratelimits.ratelimits_per_hour_v1 -AS -SELECT - workspace_id, - namespace_id, - identifier, - countIf(passed > 0) as passed, - count(*) as total, - toStartOfHour(fromUnixTimestamp64Milli(time)) AS time -FROM ratelimits.raw_ratelimits_v1 -GROUP BY - workspace_id, - namespace_id, - identifier, - time -; - - --- +goose down -DROP VIEW ratelimits.ratelimits_per_hour_mv_v1; diff --git a/internal/clickhouse/schema/020_create_ratelimits_ratelimits_per_day_mv_v1.sql b/internal/clickhouse/schema/020_create_ratelimits_ratelimits_per_day_mv_v1.sql deleted file mode 100644 index 8e76285e3b..0000000000 --- a/internal/clickhouse/schema/020_create_ratelimits_ratelimits_per_day_mv_v1.sql +++ /dev/null @@ -1,23 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW ratelimits.ratelimits_per_day_mv_v1 -TO ratelimits.ratelimits_per_day_v1 -AS -SELECT - workspace_id, - namespace_id, - identifier, - count(*) as total, - countIf(passed > 0) as passed, - toStartOfDay(fromUnixTimestamp64Milli(time)) AS time -FROM ratelimits.raw_ratelimits_v1 -GROUP BY - workspace_id, - namespace_id, - identifier, - time -; - - - --- +goose down -DROP VIEW ratelimits.ratelimits_per_day_mv_v1; diff --git a/internal/clickhouse/schema/021_create_ratelimits_ratelimits_per_month_mv_v1.sql b/internal/clickhouse/schema/021_create_ratelimits_ratelimits_per_month_mv_v1.sql deleted file mode 100644 index a97c911186..0000000000 --- a/internal/clickhouse/schema/021_create_ratelimits_ratelimits_per_month_mv_v1.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW ratelimits.ratelimits_per_month_mv_v1 -TO ratelimits.ratelimits_per_month_v1 -AS -SELECT - workspace_id, - namespace_id, - identifier, - countIf(passed > 0) as passed, - count(*) as total, - toStartOfMonth(fromUnixTimestamp64Milli(time)) AS time -FROM ratelimits.raw_ratelimits_v1 -GROUP BY - workspace_id, - namespace_id, - identifier, - time -; - - --- +goose down -DROP VIEW ratelimits.ratelimits_per_month_mv_v1; diff --git a/internal/clickhouse/schema/022_create_business_active_workspaces_per_month_v1.sql b/internal/clickhouse/schema/022_create_business_active_workspaces_per_month_v1.sql deleted file mode 100644 index 29be93dd73..0000000000 --- a/internal/clickhouse/schema/022_create_business_active_workspaces_per_month_v1.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose up -CREATE TABLE business.active_workspaces_per_month_v1 -( - time Date, - workspace_id String -) -ENGINE = MergeTree() -ORDER BY (time) -; - - --- +goose down -DROP TABLE business.active_workspaces_per_month_v1; diff --git a/internal/clickhouse/schema/023_create_business_active_workspaces_per_month_mv_v1.sql b/internal/clickhouse/schema/023_create_business_active_workspaces_per_month_mv_v1.sql deleted file mode 100644 index a22401696b..0000000000 --- a/internal/clickhouse/schema/023_create_business_active_workspaces_per_month_mv_v1.sql +++ /dev/null @@ -1,21 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW IF NOT EXISTS business.active_workspaces_keys_per_month_mv_v1 -TO business.active_workspaces_per_month_v1 -AS -SELECT - workspace_id, toDate(time) as time -FROM verifications.key_verifications_per_month_v1 -; - -CREATE MATERIALIZED VIEW IF NOT EXISTS business.active_workspaces_ratelimits_per_month_mv_v1 -TO business.active_workspaces_per_month_v1 -AS -SELECT - workspace_id, toDate(time) as time -FROM ratelimits.ratelimits_per_month_v1 -; - - --- +goose down -DROP VIEW business.active_workspaces_keys_per_month_mv_v1; -DROP VIEW business.active_workspaces_ratelimits_per_month_mv_v1; diff --git a/internal/clickhouse/schema/024_create_ratelimits_last_used_mv_v1.sql b/internal/clickhouse/schema/024_create_ratelimits_last_used_mv_v1.sql deleted file mode 100644 index 56c2f26e38..0000000000 --- a/internal/clickhouse/schema/024_create_ratelimits_last_used_mv_v1.sql +++ /dev/null @@ -1,34 +0,0 @@ --- +goose up -CREATE TABLE ratelimits.ratelimits_last_used_v1 -( - time Int64, - workspace_id String, - namespace_id String, - identifier String, - -) -ENGINE = AggregatingMergeTree() -ORDER BY (workspace_id, namespace_id, time, identifier) -; - - - -CREATE MATERIALIZED VIEW ratelimits.ratelimits_last_used_mv_v1 -TO ratelimits.ratelimits_last_used_v1 -AS -SELECT - workspace_id, - namespace_id, - identifier, - maxSimpleState(time) as time -FROM ratelimits.raw_ratelimits_v1 -GROUP BY - workspace_id, - namespace_id, - identifier -; - - --- +goose down -DROP VIEW IF EXISTS ratelimits.ratelimits_last_used_mv_v1; -DROP TABLE IF EXISTS ratelimits.ratelimits_last_used_v1; diff --git a/internal/clickhouse/schema/025_create_billable_verifications_v2.sql b/internal/clickhouse/schema/025_create_billable_verifications_v2.sql deleted file mode 100644 index d5e89dda4a..0000000000 --- a/internal/clickhouse/schema/025_create_billable_verifications_v2.sql +++ /dev/null @@ -1,30 +0,0 @@ --- +goose up -CREATE TABLE billing.billable_verifications_per_month_v2 -( - year Int, - month Int, - workspace_id String, - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, year, month) -; - -CREATE MATERIALIZED VIEW billing.billable_verifications_per_month_mv_v2 -TO billing.billable_verifications_per_month_v2 -AS SELECT - workspace_id, - sum(count) AS count, - toYear(time) AS year, - toMonth(time) AS month -FROM verifications.key_verifications_per_month_v1 -WHERE outcome = 'VALID' -GROUP BY - workspace_id, - year, - month -; --- +goose down - -DROP VIEW billing.billable_verifications_per_month_mv_v2; -DROP TABLE billing.billable_verifications_per_month_v2; diff --git a/internal/clickhouse/schema/026_create_billable_ratelimits_v1.sql b/internal/clickhouse/schema/026_create_billable_ratelimits_v1.sql deleted file mode 100644 index 0b0b46d954..0000000000 --- a/internal/clickhouse/schema/026_create_billable_ratelimits_v1.sql +++ /dev/null @@ -1,30 +0,0 @@ --- +goose up - -CREATE TABLE billing.billable_ratelimits_per_month_v1 -( - year Int, - month Int, - workspace_id String, - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, year, month) -; - -CREATE MATERIALIZED VIEW billing.billable_ratelimits_per_month_mv_v1 -TO billing.billable_ratelimits_per_month_v1 -AS SELECT - workspace_id, - sum(passed) AS count, - toYear(time) AS year, - toMonth(time) AS month -FROM ratelimits.ratelimits_per_month_v1 -WHERE passed > 0 -GROUP BY - workspace_id, - year, - month -; --- +goose down -DROP VIEW billing.billable_ratelimits_per_month_mv_v1; -DROP TABLE billing.billable_ratelimits_per_month_v1; diff --git a/internal/clickhouse/schema/027_add_tags_to_verifications.raw_key_verifications_v1.sql b/internal/clickhouse/schema/027_add_tags_to_verifications.raw_key_verifications_v1.sql deleted file mode 100644 index 06b384707b..0000000000 --- a/internal/clickhouse/schema/027_add_tags_to_verifications.raw_key_verifications_v1.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose up -ALTER TABLE verifications.raw_key_verifications_v1 -ADD COLUMN IF NOT EXISTS tags Array(String) DEFAULT []; - - --- +goose down - - - -ALTER TABLE verifications.raw_key_verifications_v1 -DROP COLUMN IF EXISTS tags; diff --git a/internal/clickhouse/schema/028_create_verifications.key_verifications_per_hour_v2.sql b/internal/clickhouse/schema/028_create_verifications.key_verifications_per_hour_v2.sql deleted file mode 100644 index fa83cd0cef..0000000000 --- a/internal/clickhouse/schema/028_create_verifications.key_verifications_per_hour_v2.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_hour_v2 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - tags Array(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, identity_id, key_id, time, tags) -; - - --- +goose down -DROP TABLE verifications.key_verifications_per_hour_v2; diff --git a/internal/clickhouse/schema/029_create_verifications.key_verifications_per_hour_mv_v2.sql b/internal/clickhouse/schema/029_create_verifications.key_verifications_per_hour_mv_v2.sql deleted file mode 100644 index 23bd8f56d2..0000000000 --- a/internal/clickhouse/schema/029_create_verifications.key_verifications_per_hour_mv_v2.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW verifications.key_verifications_per_hour_mv_v2 -TO verifications.key_verifications_per_hour_v2 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfHour(fromUnixTimestamp64Milli(time)) AS time, - tags -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time, - tags -; - - --- +goose down -DROP VIEW verifications.key_verifications_per_hour_mv_v2; diff --git a/internal/clickhouse/schema/030_create_verifications.key_verifications_per_day_v2.sql b/internal/clickhouse/schema/030_create_verifications.key_verifications_per_day_v2.sql deleted file mode 100644 index 48018969b7..0000000000 --- a/internal/clickhouse/schema/030_create_verifications.key_verifications_per_day_v2.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_day_v2 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - tags Array(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, identity_id, key_id, time, tags) -; - - --- +goose down -DROP TABLE verifications.key_verifications_per_day_v2; diff --git a/internal/clickhouse/schema/031_create_verifications.key_verifications_per_day_mv_v2.sql b/internal/clickhouse/schema/031_create_verifications.key_verifications_per_day_mv_v2.sql deleted file mode 100644 index dbb8568e08..0000000000 --- a/internal/clickhouse/schema/031_create_verifications.key_verifications_per_day_mv_v2.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW verifications.key_verifications_per_day_mv_v2 -TO verifications.key_verifications_per_day_v2 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfDay(fromUnixTimestamp64Milli(time)) AS time, - tags -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time, - tags -; - - --- +goose down -DROP VIEW verifications.key_verifications_per_day_mv_v2; diff --git a/internal/clickhouse/schema/032_create_verifications.key_verifications_per_month_v2.sql b/internal/clickhouse/schema/032_create_verifications.key_verifications_per_month_v2.sql deleted file mode 100644 index a037c26018..0000000000 --- a/internal/clickhouse/schema/032_create_verifications.key_verifications_per_month_v2.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_month_v2 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - tags Array(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, identity_id, key_id, time, tags) -; - - --- +goose down -DROP TABLE verifications.key_verifications_per_month_v2; diff --git a/internal/clickhouse/schema/033_create_verifications.key_verifications_per_month_mv_v2.sql b/internal/clickhouse/schema/033_create_verifications.key_verifications_per_month_mv_v2.sql deleted file mode 100644 index a045769a07..0000000000 --- a/internal/clickhouse/schema/033_create_verifications.key_verifications_per_month_mv_v2.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW verifications.key_verifications_per_month_mv_v2 -TO verifications.key_verifications_per_month_v2 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfMonth(fromUnixTimestamp64Milli(time)) AS time, - tags -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time, - tags -; - - --- +goose down -DROP VIEW verifications.key_verifications_per_month_mv_v2; diff --git a/internal/clickhouse/schema/034_billing_read_from_verifications.key_verifications_per_month_v2.sql b/internal/clickhouse/schema/034_billing_read_from_verifications.key_verifications_per_month_v2.sql deleted file mode 100644 index cd5d6fbd3a..0000000000 --- a/internal/clickhouse/schema/034_billing_read_from_verifications.key_verifications_per_month_v2.sql +++ /dev/null @@ -1,33 +0,0 @@ --- +goose up -ALTER TABLE billing.billable_verifications_per_month_mv_v1 -MODIFY QUERY -SELECT - workspace_id, - count(*) AS count, - toYear(time) AS year, - toMonth(time) AS month -FROM verifications.key_verifications_per_month_v2 -WHERE outcome = 'VALID' -GROUP BY - workspace_id, - year, - month -; - - --- +goose down - -ALTER TABLE billing.billable_verifications_per_month_mv_v1 -MODIFY QUERY -SELECT - workspace_id, - count(*) AS count, - toYear(time) AS year, - toMonth(time) AS month -FROM verifications.key_verifications_per_month_v1 -WHERE outcome = 'VALID' -GROUP BY - workspace_id, - year, - month -; diff --git a/internal/clickhouse/schema/035_business_update_active_workspaces_keys_per_month_mv_v1_read_from_verifications.key_verifications_per_month_v2.sql b/internal/clickhouse/schema/035_business_update_active_workspaces_keys_per_month_mv_v1_read_from_verifications.key_verifications_per_month_v2.sql deleted file mode 100644 index 1a22f97904..0000000000 --- a/internal/clickhouse/schema/035_business_update_active_workspaces_keys_per_month_mv_v1_read_from_verifications.key_verifications_per_month_v2.sql +++ /dev/null @@ -1,16 +0,0 @@ --- +goose up -ALTER TABLE business.active_workspaces_keys_per_month_mv_v1 -MODIFY QUERY -SELECT - workspace_id, toDate(time) as time -FROM verifications.key_verifications_per_month_v2 -; - --- +goose down - -ALTER TABLE business.active_workspaces_keys_per_month_mv_v1 -MODIFY QUERY -SELECT - workspace_id, toDate(time) as time -FROM verifications.key_verifications_per_month_v1 -; diff --git a/internal/clickhouse/schema/036_create_verifications.key_verifications_per_hour_v3.sql b/internal/clickhouse/schema/036_create_verifications.key_verifications_per_hour_v3.sql deleted file mode 100644 index 6f99ddcefa..0000000000 --- a/internal/clickhouse/schema/036_create_verifications.key_verifications_per_hour_v3.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_hour_v3 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - tags Array(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, identity_id, key_id, time, tags, outcome) -; - - --- +goose down -DROP TABLE verifications.key_verifications_per_hour_v3; diff --git a/internal/clickhouse/schema/037_create_verifications.key_verifications_per_hour_mv_v3.sql b/internal/clickhouse/schema/037_create_verifications.key_verifications_per_hour_mv_v3.sql deleted file mode 100644 index 4cf157874c..0000000000 --- a/internal/clickhouse/schema/037_create_verifications.key_verifications_per_hour_mv_v3.sql +++ /dev/null @@ -1,49 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW IF NOT EXISTS verifications.key_verifications_per_hour_mv_v3 -TO verifications.key_verifications_per_hour_v3 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfHour(fromUnixTimestamp64Milli(time)) AS time, - tags -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time, - tags -; - - --- populate from existing data --- INSERT INTO verifications.key_verifications_per_hour_v3 --- SELECT --- toStartOfHour(fromUnixTimestamp64Milli(time)) AS time, --- workspace_id, --- key_space_id, --- identity_id, --- key_id, --- outcome, --- tags, --- count(*) as count --- FROM verifications.raw_key_verifications_v1 --- GROUP BY --- workspace_id, --- key_space_id, --- identity_id, --- key_id, --- outcome, --- time, --- tags --- ; - --- +goose down -DROP VIEW verifications.key_verifications_per_hour_mv_v3; diff --git a/internal/clickhouse/schema/038_create_verifications.key_verifications_per_day_v3.sql b/internal/clickhouse/schema/038_create_verifications.key_verifications_per_day_v3.sql deleted file mode 100644 index c4cd43aba5..0000000000 --- a/internal/clickhouse/schema/038_create_verifications.key_verifications_per_day_v3.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_day_v3 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - tags Array(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, identity_id, key_id, time, tags, outcome) -; - - --- +goose down -DROP TABLE verifications.key_verifications_per_day_v3; diff --git a/internal/clickhouse/schema/039_create_verifications.key_verifications_per_day_mv_v3.sql b/internal/clickhouse/schema/039_create_verifications.key_verifications_per_day_mv_v3.sql deleted file mode 100644 index 05e54a6857..0000000000 --- a/internal/clickhouse/schema/039_create_verifications.key_verifications_per_day_mv_v3.sql +++ /dev/null @@ -1,49 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW IF NOT EXISTS verifications.key_verifications_per_day_mv_v3 -TO verifications.key_verifications_per_day_v3 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfDay(fromUnixTimestamp64Milli(time)) AS time, - tags -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time, - tags -; - --- populate from existing data --- INSERT INTO verifications.key_verifications_per_day_v3 --- SELECT --- toStartOfDay(fromUnixTimestamp64Milli(time)) AS time, --- workspace_id, --- key_space_id, --- identity_id, --- key_id, --- outcome, --- tags, --- count(*) as count --- FROM verifications.raw_key_verifications_v1 --- GROUP BY --- workspace_id, --- key_space_id, --- identity_id, --- key_id, --- outcome, --- time, --- tags --- ; - - --- +goose down -DROP VIEW verifications.key_verifications_per_day_mv_v3; diff --git a/internal/clickhouse/schema/040_create_verifications.key_verifications_per_month_v3.sql b/internal/clickhouse/schema/040_create_verifications.key_verifications_per_month_v3.sql deleted file mode 100644 index 9b29bd585a..0000000000 --- a/internal/clickhouse/schema/040_create_verifications.key_verifications_per_month_v3.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_month_v3 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - tags Array(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, identity_id, key_id, time, tags, outcome) -; - - --- +goose down -DROP TABLE verifications.key_verifications_per_month_v3; diff --git a/internal/clickhouse/schema/041_create_verifications.key_verifications_per_month_mv_v3.sql b/internal/clickhouse/schema/041_create_verifications.key_verifications_per_month_mv_v3.sql deleted file mode 100644 index b53b3ae6ea..0000000000 --- a/internal/clickhouse/schema/041_create_verifications.key_verifications_per_month_mv_v3.sql +++ /dev/null @@ -1,49 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW IF NOT EXISTS verifications.key_verifications_per_month_mv_v3 -TO verifications.key_verifications_per_month_v3 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfMonth(fromUnixTimestamp64Milli(time)) AS time, - tags -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time, - tags -; - --- populate from existing data --- INSERT INTO verifications.key_verifications_per_month_v3 --- SELECT --- toStartOfMonth(fromUnixTimestamp64Milli(time)) AS time, --- workspace_id, --- key_space_id, --- identity_id, --- key_id, --- outcome, --- tags, --- count(*) as counts --- FROM verifications.raw_key_verifications_v1 --- GROUP BY --- workspace_id, --- key_space_id, --- identity_id, --- key_id, --- outcome, --- time, --- tags --- ; - - --- +goose down -DROP VIEW verifications.key_verifications_per_month_mv_v3; diff --git a/internal/clickhouse/schema/042_create_api_requests_per_hour_v1.sql b/internal/clickhouse/schema/042_create_api_requests_per_hour_v1.sql deleted file mode 100644 index e2c49a801b..0000000000 --- a/internal/clickhouse/schema/042_create_api_requests_per_hour_v1.sql +++ /dev/null @@ -1,24 +0,0 @@ --- +goose up -CREATE TABLE metrics.api_requests_per_hour_v1 ( - time DateTime, - workspace_id String, - path String, - response_status Int, - host String, - -- Upper case HTTP method - -- Examples: "GET", "POST", "PUT", "DELETE" - method LowCardinality(String), - count Int64 -) ENGINE = SummingMergeTree() -ORDER BY - ( - workspace_id, - time, - host, - path, - response_status, - method - ); - --- +goose down -DROP TABLE metrics.api_requests_per_hour_v1; diff --git a/internal/clickhouse/schema/043_create_api_requests_per_hour_mv_v1.sql b/internal/clickhouse/schema/043_create_api_requests_per_hour_mv_v1.sql deleted file mode 100644 index 1d2f96c113..0000000000 --- a/internal/clickhouse/schema/043_create_api_requests_per_hour_mv_v1.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW metrics.api_requests_per_hour_mv_v1 TO metrics.api_requests_per_hour_v1 AS -SELECT - workspace_id, - path, - response_status, - host, - method, - count(*) as count, - toStartOfHour(fromUnixTimestamp64Milli(time)) AS time -FROM - metrics.raw_api_requests_v1 -GROUP BY - workspace_id, - path, - response_status, - host, - method, - time; - --- +goose down -DROP VIEW metrics.api_requests_per_hour_mv_v1; \ No newline at end of file diff --git a/internal/clickhouse/schema/044_create_api_requests_per_minute_v1.sql b/internal/clickhouse/schema/044_create_api_requests_per_minute_v1.sql deleted file mode 100644 index 38651b146b..0000000000 --- a/internal/clickhouse/schema/044_create_api_requests_per_minute_v1.sql +++ /dev/null @@ -1,24 +0,0 @@ --- +goose up -CREATE TABLE metrics.api_requests_per_minute_v1 ( - time DateTime, - workspace_id String, - path String, - response_status Int, - host String, - -- Upper case HTTP method - -- Examples: "GET", "POST", "PUT", "DELETE" - method LowCardinality(String), - count Int64 -) ENGINE = SummingMergeTree() -ORDER BY - ( - workspace_id, - time, - host, - path, - response_status, - method - ); - --- +goose down -DROP TABLE metrics.api_requests_per_minute_v1; diff --git a/internal/clickhouse/schema/045_create_api_requests_per_minute_mv_v1.sql b/internal/clickhouse/schema/045_create_api_requests_per_minute_mv_v1.sql deleted file mode 100644 index 0f58a183bf..0000000000 --- a/internal/clickhouse/schema/045_create_api_requests_per_minute_mv_v1.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW metrics.api_requests_per_minute_mv_v1 TO metrics.api_requests_per_minute_v1 AS -SELECT - workspace_id, - path, - response_status, - host, - method, - count(*) as count, - toStartOfMinute(fromUnixTimestamp64Milli(time)) AS time -FROM - metrics.raw_api_requests_v1 -GROUP BY - workspace_id, - path, - response_status, - host, - method, - time; - --- +goose down -DROP VIEW metrics.api_requests_per_minute_mv_v1; diff --git a/internal/clickhouse/schema/046_create_api_requests_per_day_v1.sql b/internal/clickhouse/schema/046_create_api_requests_per_day_v1.sql deleted file mode 100644 index 6b6e6fb800..0000000000 --- a/internal/clickhouse/schema/046_create_api_requests_per_day_v1.sql +++ /dev/null @@ -1,24 +0,0 @@ --- +goose up -CREATE TABLE metrics.api_requests_per_day_v1 ( - time DateTime, - workspace_id String, - path String, - response_status Int, - host String, - -- Upper case HTTP method - -- Examples: "GET", "POST", "PUT", "DELETE" - method LowCardinality(String), - count Int64 -) ENGINE = SummingMergeTree() -ORDER BY - ( - workspace_id, - time, - host, - path, - response_status, - method - ); - --- +goose down -DROP TABLE metrics.api_requests_per_day_v1; diff --git a/internal/clickhouse/schema/047_create_api_requests_per_day_mv_v1.sql b/internal/clickhouse/schema/047_create_api_requests_per_day_mv_v1.sql deleted file mode 100644 index a5452eaf48..0000000000 --- a/internal/clickhouse/schema/047_create_api_requests_per_day_mv_v1.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW metrics.api_requests_per_day_mv_v1 TO metrics.api_requests_per_day_v1 AS -SELECT - workspace_id, - path, - response_status, - host, - method, - count(*) as count, - toStartOfDay(fromUnixTimestamp64Milli(time)) AS time -FROM - metrics.raw_api_requests_v1 -GROUP BY - workspace_id, - path, - response_status, - host, - method, - time; - --- +goose down -DROP VIEW metrics.api_requests_per_day_mv_v1; diff --git a/internal/clickhouse/schema/048_raw_ratelimits_metrics_indexes_v1.sql b/internal/clickhouse/schema/048_raw_ratelimits_metrics_indexes_v1.sql deleted file mode 100644 index 41d6116ac6..0000000000 --- a/internal/clickhouse/schema/048_raw_ratelimits_metrics_indexes_v1.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose up -ALTER TABLE ratelimits.raw_ratelimits_v1 - ADD INDEX idx_workspace_time (workspace_id, time) TYPE minmax GRANULARITY 1; - -ALTER TABLE ratelimits.raw_ratelimits_v1 - ADD INDEX idx_request_id (request_id) TYPE minmax GRANULARITY 1; - --- +goose down -ALTER TABLE ratelimits.raw_ratelimits_v1 - DROP INDEX idx_workspace_time; - -ALTER TABLE ratelimits.raw_ratelimits_v1 - DROP INDEX idx_request_id; diff --git a/internal/clickhouse/schema/049_raw_api_metrics_ratelimit_indexes_v1.sql b/internal/clickhouse/schema/049_raw_api_metrics_ratelimit_indexes_v1.sql deleted file mode 100644 index 882d430d74..0000000000 --- a/internal/clickhouse/schema/049_raw_api_metrics_ratelimit_indexes_v1.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose up -ALTER TABLE metrics.raw_api_requests_v1 - ADD INDEX idx_workspace_time (workspace_id, time) TYPE minmax GRANULARITY 1; - -ALTER TABLE metrics.raw_api_requests_v1 - ADD INDEX idx_request_id (request_id) TYPE minmax GRANULARITY 1; - --- +goose down -ALTER TABLE metrics.raw_api_requests_v1 - DROP INDEX idx_workspace_time; - -ALTER TABLE metrics.raw_api_requests_v1 - DROP INDEX idx_request_id; diff --git a/internal/clickhouse/schema/050_create_verifications.key_verifications_per_minute_v1.sql b/internal/clickhouse/schema/050_create_verifications.key_verifications_per_minute_v1.sql deleted file mode 100644 index 2b68530cdc..0000000000 --- a/internal/clickhouse/schema/050_create_verifications.key_verifications_per_minute_v1.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose up -CREATE TABLE verifications.key_verifications_per_minute_v1 -( - time DateTime, - workspace_id String, - key_space_id String, - identity_id String, - key_id String, - outcome LowCardinality(String), - tags Array(String), - count Int64 -) -ENGINE = SummingMergeTree() -ORDER BY (workspace_id, key_space_id, identity_id, key_id, time, tags, outcome) -; - - --- +goose down -DROP TABLE verifications.key_verifications_per_minute_v1; diff --git a/internal/clickhouse/schema/051_create_verifications.key_verifications_per_minute_mv_v1.sql b/internal/clickhouse/schema/051_create_verifications.key_verifications_per_minute_mv_v1.sql deleted file mode 100644 index 2a358d855c..0000000000 --- a/internal/clickhouse/schema/051_create_verifications.key_verifications_per_minute_mv_v1.sql +++ /dev/null @@ -1,26 +0,0 @@ --- +goose up -CREATE MATERIALIZED VIEW IF NOT EXISTS verifications.key_verifications_per_minute_mv_v1 -TO verifications.key_verifications_per_minute_v1 -AS -SELECT - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - count(*) as count, - toStartOfMinute(fromUnixTimestamp64Milli(time)) AS time, - tags -FROM verifications.raw_key_verifications_v1 -GROUP BY - workspace_id, - key_space_id, - identity_id, - key_id, - outcome, - time, - tags -; - --- +goose down -DROP VIEW verifications.key_verifications_per_minute_mv_v1; diff --git a/internal/clickhouse/src/billing.ts b/internal/clickhouse/src/billing.ts index 7b65ca055c..7e6615aef1 100644 --- a/internal/clickhouse/src/billing.ts +++ b/internal/clickhouse/src/billing.ts @@ -48,7 +48,7 @@ export function getBillableVerifications(ch: Querier) { query: ` SELECT sum(count) as count - FROM default.billable_verifications_per_month_v2 + FROM billable_verifications_per_month_v2 WHERE workspace_id = {workspaceId: String} AND year = {year: Int64} AND month = {month: Int64} diff --git a/internal/clickhouse/src/logs-timeseries.test.ts b/internal/clickhouse/src/logs-timeseries.test.ts index 5056d6e2fb..807bf24aca 100644 --- a/internal/clickhouse/src/logs-timeseries.test.ts +++ b/internal/clickhouse/src/logs-timeseries.test.ts @@ -57,7 +57,7 @@ describe.each([10, 100, 1_000, 10_000, 100_000])("with %i requests", (n) => { } const count = await ch.querier.query({ - query: "SELECT count(*) as count FROM metrics.raw_api_requests_v1", + query: "SELECT count(*) as count FROM api_requests_raw_v2", schema: z.object({ count: z.number().int() }), })({}); diff --git a/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts b/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts index 4f93d30f9a..6e88c2ef96 100644 --- a/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts +++ b/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts @@ -90,7 +90,7 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { SELECT outcome, COUNT(*) as count - FROM verifications.raw_key_verifications_v1 + FROM key_verifications_raw_v2 WHERE workspace_id = '${workspaceId}' AND key_space_id = '${keySpaceId}' AND @@ -109,7 +109,7 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { } await ch.querier.query({ - query: "OPTIMIZE TABLE verifications.key_verifications_per_day_v3 FINAL", + query: "OPTIMIZE TABLE key_verifications_per_day_v2 FINAL", schema: z.any(), })({}); @@ -119,7 +119,7 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { SELECT outcome, SUM(count) as total - FROM verifications.key_verifications_per_day_v3 + FROM key_verifications_per_day_v2 WHERE workspace_id = '${workspaceId}' AND key_space_id = '${keySpaceId}' AND diff --git a/tools/migrate/ch_logs.ts b/tools/migrate/ch_logs.ts index d954328e89..c8f91c3317 100644 --- a/tools/migrate/ch_logs.ts +++ b/tools/migrate/ch_logs.ts @@ -22,7 +22,7 @@ async function main() { const query = ch.querier.query({ query: ` - SELECT * FROM metrics.raw_api_requests_v1 + SELECT * FROM api_requests_raw_v2 WHERE workspace_id = '${workspaceId}' AND time > ${start} AND time < ${end} diff --git a/tools/migrate/ch_migrate_v2_simple.ts b/tools/migrate/ch_migrate_v2_simple.ts deleted file mode 100644 index b3532d6f97..0000000000 --- a/tools/migrate/ch_migrate_v2_simple.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { createClient } from "@clickhouse/client-web"; - -// Configuration -const CHUNK_SIZE_HOURS = 6; // Process 6 hours at a time -const CHUNK_SIZE_MS = CHUNK_SIZE_HOURS * 60 * 60 * 1000; -const MIGRATION_START = new Date("2023-05-01T00:00:00Z"); -const MIGRATION_END = new Date("2025-10-01T00:00:00Z"); - -async function main() { - const ch = createClient({ - url: process.env.CLICKHOUSE_URL, - - clickhouse_settings: { - output_format_json_quote_64bit_integers: 0, - output_format_json_quote_64bit_floats: 0, - }, - }); - - if (!process.env.CLICKHOUSE_URL) { - throw new Error("CLICKHOUSE_URL environment variable is required"); - } - - let end = MIGRATION_END.getTime(); - - while (end > MIGRATION_START.getTime()) { - const start = end - CHUNK_SIZE_MS; - - console.log(`⏳ Processing ${new Date(start).toLocaleString()} (${start})`); - - await Promise.all([ - ch.query({ - query: ` - INSERT INTO key_verifications_raw_v2 - SELECT - request_id, - time, - workspace_id, - key_space_id, - identity_id, - key_id, - region, - outcome, - tags, - 0 as spent_credits, -- v1 doesn't have this column, default to 0 - 0.0 as latency -- v1 doesn't have this column, default to 0.0 - FROM verifications.raw_key_verifications_v1 - WHERE time >= ${start} - AND time < ${end}; - `, - }), - ch.query({ - query: ` - INSERT INTO ratelimits_raw_v2 - SELECT - request_id, - time, - workspace_id, - namespace_id, - identifier, - passed, - 0.0 as latency -- v1 doesn't have this column, default to 0.0 - FROM - ratelimits.raw_ratelimits_v1 - WHERE time >= ${start} - AND time < ${end}; - - `, - }), - ch.query({ - query: ` - INSERT INTO api_requests_raw_v2 - SELECT - request_id, - time, - workspace_id, - host, - method, - path, - request_headers, - request_body, - response_status, - response_headers, - response_body, - error, - service_latency, - user_agent, - ip_address, - '' as region - FROM - metrics.raw_api_requests_v1 - WHERE time >= ${start} - AND time < ${end}; - - `, - }), - ]); - - end = start; - } -} -main(); diff --git a/tools/migrate/v1_deprecation.ts b/tools/migrate/v1_deprecation.ts index 523c6fb81a..3109cb078a 100644 --- a/tools/migrate/v1_deprecation.ts +++ b/tools/migrate/v1_deprecation.ts @@ -28,7 +28,7 @@ async function main() { SELECT workspace_id, splitByChar('?', path, 1)[1] as path - FROM metrics.api_requests_per_day_v1 + FROM api_requests_per_day_v2 WHERE startsWith(path, '/v1/') AND workspace_id != '' AND workspace_id != 'ws_2vUFz88G6TuzMQHZaUhXADNyZWMy' // filter out special workspaces @@ -47,7 +47,9 @@ async function main() { let emailsSent = 0; console.log( - `Found ${new Set(rows.val.map((r) => r.workspace_id)).size} workspaces across ${rows.val.length} paths`, + `Found ${ + new Set(rows.val.map((r) => r.workspace_id)).size + } workspaces across ${rows.val.length} paths`, ); const workspaceToPaths = new Map(); for (const row of rows.val) { From 9cb3fef6c583eefe1eae949e565d34220d9651c3 Mon Sep 17 00:00:00 2001 From: chronark Date: Thu, 6 Nov 2025 18:12:18 +0100 Subject: [PATCH 02/10] revert: version --- apps/docs/analytics/getting-started.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/analytics/getting-started.mdx b/apps/docs/analytics/getting-started.mdx index 031464310c..4ee7e6f66d 100644 --- a/apps/docs/analytics/getting-started.mdx +++ b/apps/docs/analytics/getting-started.mdx @@ -44,7 +44,7 @@ Count the total number of key verifications in the last 7 days across all your A ```sql SQL SELECT SUM(count) as total -FROM key_verifications_per_day_v2 +FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY ``` @@ -53,7 +53,7 @@ curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ - "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v2 WHERE time >= now() - INTERVAL 7 DAY" + "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY" }' ``` From a5a51706bb12f09d49512063a85d44b4f4975203 Mon Sep 17 00:00:00 2001 From: chronark Date: Thu, 6 Nov 2025 18:14:06 +0100 Subject: [PATCH 03/10] revert: version --- apps/docs/analytics/getting-started.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/docs/analytics/getting-started.mdx b/apps/docs/analytics/getting-started.mdx index 4ee7e6f66d..1ea887bceb 100644 --- a/apps/docs/analytics/getting-started.mdx +++ b/apps/docs/analytics/getting-started.mdx @@ -94,7 +94,7 @@ Identify your most active users by counting their total verifications over the l SELECT external_id, SUM(count) as verifications -FROM key_verifications_per_day_v2 +FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id ORDER BY verifications DESC @@ -114,10 +114,10 @@ curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ **Performance tip:** For longer time ranges, use pre-aggregated tables instead of the raw table: - - `key_verifications_per_minute_v2` - For queries spanning hours - - `key_verifications_per_hour_v2` - For queries spanning days - - `key_verifications_per_day_v2` - For queries spanning weeks/months - - `key_verifications_per_month_v2` - For queries spanning years + - `key_verifications_per_minute_v1` - For queries spanning hours + - `key_verifications_per_hour_v1` - For queries spanning days + - `key_verifications_per_day_v1` - For queries spanning weeks/months + - `key_verifications_per_month_v1` - For queries spanning years Use `SUM(count)` instead of `COUNT(*)` with aggregated tables. They scan far fewer rows and are much faster. From 74cb3c53c0442c93cdb192701ece65c6ab5187c0 Mon Sep 17 00:00:00 2001 From: chronark Date: Fri, 7 Nov 2025 09:49:01 +0100 Subject: [PATCH 04/10] fix: billable_ratelimits_per_month_v2 to default --- go/pkg/clickhouse/billable_ratelimits.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/pkg/clickhouse/billable_ratelimits.go b/go/pkg/clickhouse/billable_ratelimits.go index 2ef8c0f093..82a672446d 100644 --- a/go/pkg/clickhouse/billable_ratelimits.go +++ b/go/pkg/clickhouse/billable_ratelimits.go @@ -22,7 +22,7 @@ func (c *clickhouse) GetBillableRatelimits(ctx context.Context, workspaceID stri query := ` SELECT sum(count) as count - FROM billing.billable_ratelimits_per_month_v2 + FROM default.billable_ratelimits_per_month_v2 WHERE workspace_id = ? AND year = ? AND month = ? From 5f20d3ef1c806db44b08afac9b24cf7c355b506b Mon Sep 17 00:00:00 2001 From: chronark Date: Fri, 7 Nov 2025 09:57:42 +0100 Subject: [PATCH 05/10] chore: add default database everywhere --- go/pkg/clickhouse/billable_verifications.go | 2 +- go/pkg/clickhouse/key_verifications_test.go | 34 ++++++------ go/pkg/clickhouse/ratelimits_test.go | 18 +++---- internal/clickhouse/src/billing.ts | 2 +- .../clickhouse/src/logs-timeseries.test.ts | 26 ++++++--- ...ation_outcomes_propagate_correctly.test.ts | 54 +++++++++++++------ tools/migrate/ch_logs.ts | 4 +- tools/migrate/v1_deprecation.ts | 10 ++-- 8 files changed, 92 insertions(+), 58 deletions(-) diff --git a/go/pkg/clickhouse/billable_verifications.go b/go/pkg/clickhouse/billable_verifications.go index 7713c12956..05dc24fb5e 100644 --- a/go/pkg/clickhouse/billable_verifications.go +++ b/go/pkg/clickhouse/billable_verifications.go @@ -22,7 +22,7 @@ func (c *clickhouse) GetBillableVerifications(ctx context.Context, workspaceID s query := ` SELECT sum(count) as count - FROM billable_verifications_per_month_v2 + FROM default.billable_verifications_per_month_v2 WHERE workspace_id = ? AND year = ? AND month = ? diff --git a/go/pkg/clickhouse/key_verifications_test.go b/go/pkg/clickhouse/key_verifications_test.go index 7e96c0bdbf..6d6e8d62ed 100644 --- a/go/pkg/clickhouse/key_verifications_test.go +++ b/go/pkg/clickhouse/key_verifications_test.go @@ -98,7 +98,7 @@ func TestKeyVerifications(t *testing.T) { for i := 0; i < len(verifications); i += batchSize { t0 = time.Now() - batch, err := conn.PrepareBatch(ctx, "INSERT INTO key_verifications_raw_v2") + batch, err := conn.PrepareBatch(ctx, "INSERT INTO default.key_verifications_raw_v2") require.NoError(t, err) for _, row := range verifications[i:min(i+batchSize, len(verifications))] { @@ -112,13 +112,13 @@ func TestKeyVerifications(t *testing.T) { require.EventuallyWithT(t, func(c *assert.CollectT) { rawCount := uint64(0) - err := conn.QueryRow(ctx, "SELECT COUNT(*) FROM key_verifications_raw_v2 WHERE workspace_id = ?", workspaceID).Scan(&rawCount) + err := conn.QueryRow(ctx, "SELECT COUNT(*) FROM default.key_verifications_raw_v2 WHERE workspace_id = ?", workspaceID).Scan(&rawCount) require.NoError(c, err) require.Equal(c, len(verifications), int(rawCount)) }, time.Minute, time.Second) t.Run("totals are correct", func(t *testing.T) { - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { t.Run(table, func(t *testing.T) { require.EventuallyWithT(t, func(c *assert.CollectT) { queried := int64(0) @@ -137,7 +137,7 @@ func TestKeyVerifications(t *testing.T) { return acc }, map[string]int{}) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { t.Run(table, func(t *testing.T) { for outcome, count := range countByOutcome { require.EventuallyWithT(t, func(c *assert.CollectT) { @@ -163,7 +163,7 @@ func TestKeyVerifications(t *testing.T) { return acc }, map[string]int{}) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { t.Run(table, func(t *testing.T) { t.Parallel() @@ -190,7 +190,7 @@ func TestKeyVerifications(t *testing.T) { return acc }, map[string]int{}) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { t.Run(table, func(t *testing.T) { for outcome, count := range countByOutcome { require.EventuallyWithT(t, func(c *assert.CollectT) { @@ -218,7 +218,7 @@ func TestKeyVerifications(t *testing.T) { return acc }, map[string]int{}) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { t.Run(table, func(t *testing.T) { for outcome, count := range countByOutcome { require.EventuallyWithT(t, func(c *assert.CollectT) { @@ -241,7 +241,7 @@ func TestKeyVerifications(t *testing.T) { p75 := percentile(latencies, 0.75) p99 := percentile(latencies, 0.99) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { t.Run(table, func(t *testing.T) { t.Parallel() var ( @@ -265,7 +265,7 @@ func TestKeyVerifications(t *testing.T) { return acc + v.SpentCredits }, int64(0)) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { t.Run(table, func(t *testing.T) { t.Parallel() var queried int64 @@ -286,7 +286,7 @@ func TestKeyVerifications(t *testing.T) { return acc }, int64(0)) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { t.Run(table, func(t *testing.T) { t.Parallel() var queried int64 @@ -310,7 +310,7 @@ func TestKeyVerifications(t *testing.T) { return acc }, int64(0)) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { t.Run(table, func(t *testing.T) { t.Parallel() var queried int64 @@ -331,7 +331,7 @@ func TestKeyVerifications(t *testing.T) { id := identityID expectedExternalID := identityToExternalID[id] - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { tbl := table t.Run(tbl, func(t *testing.T) { t.Parallel() @@ -359,7 +359,7 @@ func TestKeyVerifications(t *testing.T) { return acc }, 0) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { tbl := table t.Run(tbl, func(t *testing.T) { t.Parallel() @@ -388,7 +388,7 @@ func TestKeyVerifications(t *testing.T) { return acc }, map[string]int{}) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { tbl := table t.Run(tbl, func(t *testing.T) { t.Parallel() @@ -414,7 +414,7 @@ func TestKeyVerifications(t *testing.T) { id := identityID extID := identityToExternalID[id] - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { tbl := table t.Run(tbl, func(t *testing.T) { t.Parallel() @@ -454,7 +454,7 @@ func TestKeyVerifications(t *testing.T) { return acc }, int64(0)) - for _, table := range []string{"key_verifications_per_minute_v2", "key_verifications_per_hour_v2", "key_verifications_per_day_v2", "key_verifications_per_month_v2"} { + for _, table := range []string{"default.key_verifications_per_minute_v2", "default.key_verifications_per_hour_v2", "default.key_verifications_per_day_v2", "default.key_verifications_per_month_v2"} { tbl := table t.Run(tbl, func(t *testing.T) { t.Parallel() @@ -479,7 +479,7 @@ func TestKeyVerifications(t *testing.T) { }, int64(0)) var queried int64 - err := conn.QueryRow(ctx, "SELECT sum(count) FROM billable_verifications_per_month_v2 WHERE workspace_id = ?;", workspaceID).Scan(&queried) + err := conn.QueryRow(ctx, "SELECT sum(count) FROM default.billable_verifications_per_month_v2 WHERE workspace_id = ?;", workspaceID).Scan(&queried) require.NoError(t, err) diff --git a/go/pkg/clickhouse/ratelimits_test.go b/go/pkg/clickhouse/ratelimits_test.go index 8a7aee922a..1f6272805a 100644 --- a/go/pkg/clickhouse/ratelimits_test.go +++ b/go/pkg/clickhouse/ratelimits_test.go @@ -107,7 +107,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { t0 = time.Now() - batch, err := conn.PrepareBatch(ctx, "INSERT INTO ratelimits_raw_v2") + batch, err := conn.PrepareBatch(ctx, "INSERT INTO default.ratelimits_raw_v2") require.NoError(t, err) for _, row := range ratelimits { @@ -121,7 +121,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { // Wait for raw data to be available require.EventuallyWithT(t, func(c *assert.CollectT) { rawCount := uint64(0) - err = conn.QueryRow(ctx, "SELECT COUNT(*) FROM ratelimits_raw_v2 WHERE workspace_id = ?", workspaceID).Scan(&rawCount) + err = conn.QueryRow(ctx, "SELECT COUNT(*) FROM default.ratelimits_raw_v2 WHERE workspace_id = ?", workspaceID).Scan(&rawCount) require.NoError(c, err) require.Equal(c, len(ratelimits), int(rawCount)) }, time.Minute, time.Second) @@ -137,7 +137,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { totalRequests := len(ratelimits) - for _, table := range []string{"ratelimits_per_minute_v2", "ratelimits_per_hour_v2", "ratelimits_per_day_v2", "ratelimits_per_month_v2"} { + for _, table := range []string{"default.ratelimits_per_minute_v2", "default.ratelimits_per_hour_v2", "default.ratelimits_per_day_v2", "default.ratelimits_per_month_v2"} { t.Run(table, func(t *testing.T) { require.EventuallyWithT(t, func(c *assert.CollectT) { var queriedPassed, queriedTotal int64 @@ -158,7 +158,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { p75 := percentile(latencies, 0.75) p99 := percentile(latencies, 0.99) - for _, table := range []string{"ratelimits_per_minute_v2", "ratelimits_per_hour_v2", "ratelimits_per_day_v2", "ratelimits_per_month_v2"} { + for _, table := range []string{"default.ratelimits_per_minute_v2", "default.ratelimits_per_hour_v2", "default.ratelimits_per_day_v2", "default.ratelimits_per_month_v2"} { t.Run(table, func(t *testing.T) { require.EventuallyWithT(t, func(c *assert.CollectT) { var ( @@ -189,7 +189,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { return acc }, make(map[string]struct{ passed, total int })) - for _, table := range []string{"ratelimits_per_minute_v2", "ratelimits_per_hour_v2", "ratelimits_per_day_v2", "ratelimits_per_month_v2"} { + for _, table := range []string{"default.ratelimits_per_minute_v2", "default.ratelimits_per_hour_v2", "default.ratelimits_per_day_v2", "default.ratelimits_per_month_v2"} { t.Run(table, func(t *testing.T) { for namespaceID, expectedStats := range namespaceStats { require.EventuallyWithT(t, func(c *assert.CollectT) { @@ -221,7 +221,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { return array.Random(identifiers) }) - for _, table := range []string{"ratelimits_per_minute_v2", "ratelimits_per_hour_v2", "ratelimits_per_day_v2", "ratelimits_per_month_v2"} { + for _, table := range []string{"default.ratelimits_per_minute_v2", "default.ratelimits_per_hour_v2", "default.ratelimits_per_day_v2", "default.ratelimits_per_month_v2"} { t.Run(table, func(t *testing.T) { for _, identifier := range sampleIdentifiers { expectedStats, exists := identifierStats[identifier] @@ -251,7 +251,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { passed += 1 } } - for _, table := range []string{"ratelimits_per_minute_v2", "ratelimits_per_hour_v2", "ratelimits_per_day_v2", "ratelimits_per_month_v2"} { + for _, table := range []string{"default.ratelimits_per_minute_v2", "default.ratelimits_per_hour_v2", "default.ratelimits_per_day_v2", "default.ratelimits_per_month_v2"} { t.Run(table, func(t *testing.T) { require.EventuallyWithT(t, func(c *assert.CollectT) { var queriedPassed, queriedTotal int64 @@ -279,7 +279,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { } } } - for _, table := range []string{"ratelimits_per_minute_v2", "ratelimits_per_hour_v2", "ratelimits_per_day_v2", "ratelimits_per_month_v2"} { + for _, table := range []string{"default.ratelimits_per_minute_v2", "default.ratelimits_per_hour_v2", "default.ratelimits_per_day_v2", "default.ratelimits_per_month_v2"} { t.Run(table, func(t *testing.T) { require.EventuallyWithT(t, func(c *assert.CollectT) { var queriedPassed, queriedTotal int64 @@ -309,7 +309,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { } } } - for _, table := range []string{"ratelimits_per_minute_v2", "ratelimits_per_hour_v2", "ratelimits_per_day_v2", "ratelimits_per_month_v2"} { + for _, table := range []string{"default.ratelimits_per_minute_v2", "default.ratelimits_per_hour_v2", "default.ratelimits_per_day_v2", "default.ratelimits_per_month_v2"} { t.Run(table, func(t *testing.T) { require.EventuallyWithT(t, func(c *assert.CollectT) { var queriedPassed, queriedTotal int64 diff --git a/internal/clickhouse/src/billing.ts b/internal/clickhouse/src/billing.ts index 7e6615aef1..7b65ca055c 100644 --- a/internal/clickhouse/src/billing.ts +++ b/internal/clickhouse/src/billing.ts @@ -48,7 +48,7 @@ export function getBillableVerifications(ch: Querier) { query: ` SELECT sum(count) as count - FROM billable_verifications_per_month_v2 + FROM default.billable_verifications_per_month_v2 WHERE workspace_id = {workspaceId: String} AND year = {year: Int64} AND month = {month: Int64} diff --git a/internal/clickhouse/src/logs-timeseries.test.ts b/internal/clickhouse/src/logs-timeseries.test.ts index 807bf24aca..1888c0f520 100644 --- a/internal/clickhouse/src/logs-timeseries.test.ts +++ b/internal/clickhouse/src/logs-timeseries.test.ts @@ -14,7 +14,11 @@ function generateTimeBasedData(n: number, workspaceId: string) { return Array.from({ length: n }).map(() => { const timeRange = - Math.random() < 0.6 ? intervals.hour : Math.random() < 0.8 ? intervals.day : intervals.week; + Math.random() < 0.6 + ? intervals.hour + : Math.random() < 0.8 + ? intervals.day + : intervals.week; const start = now - timeRange; return { @@ -24,16 +28,21 @@ function generateTimeBasedData(n: number, workspaceId: string) { host: `api${Math.floor(Math.random() * 5)}.example.com`, method: ["GET", "POST", "PUT", "DELETE"][Math.floor(Math.random() * 4)], path: "/v1/keys.verifyKey", - request_headers: ["content-type: application/json", "authorization: Bearer ${randomUUID()}"], + request_headers: [ + "content-type: application/json", + "authorization: Bearer ${randomUUID()}", + ], request_body: JSON.stringify({ data: randomUUID() }), - response_status: [200, 201, 400, 401, 403, 500][Math.floor(Math.random() * 6)], + response_status: [200, 201, 400, 401, 403, 500][ + Math.floor(Math.random() * 6) + ], response_headers: ["content-type: application/json"], response_body: JSON.stringify({ status: "success", id: randomUUID() }), error: Math.random() < 0.1 ? "Internal server error" : "", service_latency: Math.floor(Math.random() * 1000), user_agent: "Mozilla/5.0 (compatible; Bot/1.0)", ip_address: `${Math.floor(Math.random() * 256)}.${Math.floor( - Math.random() * 256, + Math.random() * 256 )}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`, }; }); @@ -57,7 +66,7 @@ describe.each([10, 100, 1_000, 10_000, 100_000])("with %i requests", (n) => { } const count = await ch.querier.query({ - query: "SELECT count(*) as count FROM api_requests_raw_v2", + query: "SELECT count(*) as count FROM default.api_requests_raw_v2", schema: z.object({ count: z.number().int() }), })({}); @@ -107,10 +116,13 @@ describe.each([10, 100, 1_000, 10_000, 100_000])("with %i requests", (n) => { // Verifies that buckets have some valid data in it. for (const buckets of [hourly.val!, daily.val!, minutely.val!]) { - const totalEvents = buckets.reduce((sum, bucket) => sum + bucket.y.total, 0); + const totalEvents = buckets.reduce( + (sum, bucket) => sum + bucket.y.total, + 0 + ); expect(totalEvents).toBeGreaterThan(0); } }, - { timeout: 120_000 }, + { timeout: 120_000 } ); }); diff --git a/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts b/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts index 6e88c2ef96..13bf907646 100644 --- a/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts +++ b/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts @@ -44,7 +44,8 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { for (let i = 0; i < expectedOutcomes.RATE_LIMITED; i++) { verifications.push({ request_id: `rate-limited-${i}-${randomUUID()}`, - time: start + Math.floor(i * (interval / expectedOutcomes.RATE_LIMITED)), + time: + start + Math.floor(i * (interval / expectedOutcomes.RATE_LIMITED)), workspace_id: workspaceId, key_space_id: keySpaceId, key_id: keyId, @@ -90,7 +91,7 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { SELECT outcome, COUNT(*) as count - FROM key_verifications_raw_v2 + FROM default.key_verifications_raw_v2 WHERE workspace_id = '${workspaceId}' AND key_space_id = '${keySpaceId}' AND @@ -104,12 +105,15 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { })({}); for (const [outcome, expectedCount] of Object.entries(expectedOutcomes)) { - const actualCount = rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; - expect(actualCount, `Raw ${outcome} count should match`).toBe(expectedCount); + const actualCount = + rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; + expect(actualCount, `Raw ${outcome} count should match`).toBe( + expectedCount + ); } await ch.querier.query({ - query: "OPTIMIZE TABLE key_verifications_per_day_v2 FINAL", + query: "OPTIMIZE TABLE default.key_verifications_per_day_v2 FINAL", schema: z.any(), })({}); @@ -119,7 +123,7 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { SELECT outcome, SUM(count) as total - FROM key_verifications_per_day_v2 + FROM default.key_verifications_per_day_v2 WHERE workspace_id = '${workspaceId}' AND key_space_id = '${keySpaceId}' AND @@ -135,7 +139,10 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { }), })({}); - if (directResult.val && directResult.val.length >= Object.keys(expectedOutcomes).length) { + if ( + directResult.val && + directResult.val.length >= Object.keys(expectedOutcomes).length + ) { return directResult.val; } @@ -181,31 +188,44 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { directAggregateData.forEach((row) => { if (row.outcome in dbAggregates) { - dbAggregates[row.outcome as keyof typeof dbAggregates] = row.total; + dbAggregates[row.outcome as keyof typeof dbAggregates] = + row.total; } }); } if (apiCounts.VALID === 0 && expectedOutcomes.VALID > 0) { - for (const [outcome, expectedCount] of Object.entries(expectedOutcomes)) { - const rawCount = rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; - expect(rawCount, `Raw ${outcome} count should match expected`).toBe(expectedCount); + for (const [outcome, expectedCount] of Object.entries( + expectedOutcomes + )) { + const rawCount = + rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; + expect(rawCount, `Raw ${outcome} count should match expected`).toBe( + expectedCount + ); } } else { - for (const [outcome, expectedCount] of Object.entries(expectedOutcomes)) { + for (const [outcome, expectedCount] of Object.entries( + expectedOutcomes + )) { expect( apiCounts[outcome as keyof typeof apiCounts], - `API ${outcome} count should match expected`, + `API ${outcome} count should match expected` ).toBe(expectedCount); } } } else { - for (const [outcome, expectedCount] of Object.entries(expectedOutcomes)) { - const rawCount = rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; - expect(rawCount, `Raw ${outcome} count should match expected`).toBe(expectedCount); + for (const [outcome, expectedCount] of Object.entries( + expectedOutcomes + )) { + const rawCount = + rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; + expect(rawCount, `Raw ${outcome} count should match expected`).toBe( + expectedCount + ); } } }, - { timeout: 120_000 }, + { timeout: 120_000 } ); }); diff --git a/tools/migrate/ch_logs.ts b/tools/migrate/ch_logs.ts index c8f91c3317..66b24603e1 100644 --- a/tools/migrate/ch_logs.ts +++ b/tools/migrate/ch_logs.ts @@ -8,7 +8,7 @@ async function main() { }); const conn = await mysql.createConnection( - `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey?ssl={}`, + `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey?ssl={}` ); await conn.ping(); @@ -22,7 +22,7 @@ async function main() { const query = ch.querier.query({ query: ` - SELECT * FROM api_requests_raw_v2 + SELECT * FROM default.api_requests_raw_v2 WHERE workspace_id = '${workspaceId}' AND time > ${start} AND time < ${end} diff --git a/tools/migrate/v1_deprecation.ts b/tools/migrate/v1_deprecation.ts index 3109cb078a..21fa3cf106 100644 --- a/tools/migrate/v1_deprecation.ts +++ b/tools/migrate/v1_deprecation.ts @@ -10,7 +10,7 @@ async function main() { }); const conn = await mysql.createConnection( - `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey?ssl={}`, + `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey?ssl={}` ); await conn.ping(); @@ -28,7 +28,7 @@ async function main() { SELECT workspace_id, splitByChar('?', path, 1)[1] as path - FROM api_requests_per_day_v2 + FROM default.api_requests_per_day_v2 WHERE startsWith(path, '/v1/') AND workspace_id != '' AND workspace_id != 'ws_2vUFz88G6TuzMQHZaUhXADNyZWMy' // filter out special workspaces @@ -49,7 +49,7 @@ async function main() { console.log( `Found ${ new Set(rows.val.map((r) => r.workspace_id)).size - } workspaces across ${rows.val.length} paths`, + } workspaces across ${rows.val.length} paths` ); const workspaceToPaths = new Map(); for (const row of rows.val) { @@ -64,7 +64,9 @@ async function main() { for (const [workspaceId, paths] of workspaceToPaths.entries()) { if (paths.includes("/v1/analytics.getVerifications")) { console.warn( - `Skipping workspace ${workspaceId} due to analytics endpoint: ${paths.join(", ")}`, + `Skipping workspace ${workspaceId} due to analytics endpoint: ${paths.join( + ", " + )}` ); continue; } From f653122665a694db989cf99b10915dfeea22f638 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:58:59 +0000 Subject: [PATCH 06/10] [autofix.ci] apply automated fixes --- .../clickhouse/src/logs-timeseries.test.ts | 24 +++------- ...ation_outcomes_propagate_correctly.test.ts | 48 ++++++------------- tools/migrate/ch_logs.ts | 2 +- tools/migrate/v1_deprecation.ts | 8 ++-- 4 files changed, 24 insertions(+), 58 deletions(-) diff --git a/internal/clickhouse/src/logs-timeseries.test.ts b/internal/clickhouse/src/logs-timeseries.test.ts index 1888c0f520..1b0302bad0 100644 --- a/internal/clickhouse/src/logs-timeseries.test.ts +++ b/internal/clickhouse/src/logs-timeseries.test.ts @@ -14,11 +14,7 @@ function generateTimeBasedData(n: number, workspaceId: string) { return Array.from({ length: n }).map(() => { const timeRange = - Math.random() < 0.6 - ? intervals.hour - : Math.random() < 0.8 - ? intervals.day - : intervals.week; + Math.random() < 0.6 ? intervals.hour : Math.random() < 0.8 ? intervals.day : intervals.week; const start = now - timeRange; return { @@ -28,21 +24,16 @@ function generateTimeBasedData(n: number, workspaceId: string) { host: `api${Math.floor(Math.random() * 5)}.example.com`, method: ["GET", "POST", "PUT", "DELETE"][Math.floor(Math.random() * 4)], path: "/v1/keys.verifyKey", - request_headers: [ - "content-type: application/json", - "authorization: Bearer ${randomUUID()}", - ], + request_headers: ["content-type: application/json", "authorization: Bearer ${randomUUID()}"], request_body: JSON.stringify({ data: randomUUID() }), - response_status: [200, 201, 400, 401, 403, 500][ - Math.floor(Math.random() * 6) - ], + response_status: [200, 201, 400, 401, 403, 500][Math.floor(Math.random() * 6)], response_headers: ["content-type: application/json"], response_body: JSON.stringify({ status: "success", id: randomUUID() }), error: Math.random() < 0.1 ? "Internal server error" : "", service_latency: Math.floor(Math.random() * 1000), user_agent: "Mozilla/5.0 (compatible; Bot/1.0)", ip_address: `${Math.floor(Math.random() * 256)}.${Math.floor( - Math.random() * 256 + Math.random() * 256, )}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`, }; }); @@ -116,13 +107,10 @@ describe.each([10, 100, 1_000, 10_000, 100_000])("with %i requests", (n) => { // Verifies that buckets have some valid data in it. for (const buckets of [hourly.val!, daily.val!, minutely.val!]) { - const totalEvents = buckets.reduce( - (sum, bucket) => sum + bucket.y.total, - 0 - ); + const totalEvents = buckets.reduce((sum, bucket) => sum + bucket.y.total, 0); expect(totalEvents).toBeGreaterThan(0); } }, - { timeout: 120_000 } + { timeout: 120_000 }, ); }); diff --git a/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts b/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts index 13bf907646..28ebd15420 100644 --- a/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts +++ b/internal/clickhouse/src/verification_outcomes_propagate_correctly.test.ts @@ -44,8 +44,7 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { for (let i = 0; i < expectedOutcomes.RATE_LIMITED; i++) { verifications.push({ request_id: `rate-limited-${i}-${randomUUID()}`, - time: - start + Math.floor(i * (interval / expectedOutcomes.RATE_LIMITED)), + time: start + Math.floor(i * (interval / expectedOutcomes.RATE_LIMITED)), workspace_id: workspaceId, key_space_id: keySpaceId, key_id: keyId, @@ -105,11 +104,8 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { })({}); for (const [outcome, expectedCount] of Object.entries(expectedOutcomes)) { - const actualCount = - rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; - expect(actualCount, `Raw ${outcome} count should match`).toBe( - expectedCount - ); + const actualCount = rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; + expect(actualCount, `Raw ${outcome} count should match`).toBe(expectedCount); } await ch.querier.query({ @@ -139,10 +135,7 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { }), })({}); - if ( - directResult.val && - directResult.val.length >= Object.keys(expectedOutcomes).length - ) { + if (directResult.val && directResult.val.length >= Object.keys(expectedOutcomes).length) { return directResult.val; } @@ -188,44 +181,31 @@ describe.each([10, 100, 1_000, 10_000])("with %i verifications", (n) => { directAggregateData.forEach((row) => { if (row.outcome in dbAggregates) { - dbAggregates[row.outcome as keyof typeof dbAggregates] = - row.total; + dbAggregates[row.outcome as keyof typeof dbAggregates] = row.total; } }); } if (apiCounts.VALID === 0 && expectedOutcomes.VALID > 0) { - for (const [outcome, expectedCount] of Object.entries( - expectedOutcomes - )) { - const rawCount = - rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; - expect(rawCount, `Raw ${outcome} count should match expected`).toBe( - expectedCount - ); + for (const [outcome, expectedCount] of Object.entries(expectedOutcomes)) { + const rawCount = rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; + expect(rawCount, `Raw ${outcome} count should match expected`).toBe(expectedCount); } } else { - for (const [outcome, expectedCount] of Object.entries( - expectedOutcomes - )) { + for (const [outcome, expectedCount] of Object.entries(expectedOutcomes)) { expect( apiCounts[outcome as keyof typeof apiCounts], - `API ${outcome} count should match expected` + `API ${outcome} count should match expected`, ).toBe(expectedCount); } } } else { - for (const [outcome, expectedCount] of Object.entries( - expectedOutcomes - )) { - const rawCount = - rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; - expect(rawCount, `Raw ${outcome} count should match expected`).toBe( - expectedCount - ); + for (const [outcome, expectedCount] of Object.entries(expectedOutcomes)) { + const rawCount = rawCounts.val?.find((row) => row.outcome === outcome)?.count || 0; + expect(rawCount, `Raw ${outcome} count should match expected`).toBe(expectedCount); } } }, - { timeout: 120_000 } + { timeout: 120_000 }, ); }); diff --git a/tools/migrate/ch_logs.ts b/tools/migrate/ch_logs.ts index 66b24603e1..df62862b3a 100644 --- a/tools/migrate/ch_logs.ts +++ b/tools/migrate/ch_logs.ts @@ -8,7 +8,7 @@ async function main() { }); const conn = await mysql.createConnection( - `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey?ssl={}` + `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey?ssl={}`, ); await conn.ping(); diff --git a/tools/migrate/v1_deprecation.ts b/tools/migrate/v1_deprecation.ts index 21fa3cf106..737561756d 100644 --- a/tools/migrate/v1_deprecation.ts +++ b/tools/migrate/v1_deprecation.ts @@ -10,7 +10,7 @@ async function main() { }); const conn = await mysql.createConnection( - `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey?ssl={}` + `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey?ssl={}`, ); await conn.ping(); @@ -49,7 +49,7 @@ async function main() { console.log( `Found ${ new Set(rows.val.map((r) => r.workspace_id)).size - } workspaces across ${rows.val.length} paths` + } workspaces across ${rows.val.length} paths`, ); const workspaceToPaths = new Map(); for (const row of rows.val) { @@ -64,9 +64,7 @@ async function main() { for (const [workspaceId, paths] of workspaceToPaths.entries()) { if (paths.includes("/v1/analytics.getVerifications")) { console.warn( - `Skipping workspace ${workspaceId} due to analytics endpoint: ${paths.join( - ", " - )}` + `Skipping workspace ${workspaceId} due to analytics endpoint: ${paths.join(", ")}`, ); continue; } From fcc85bca9a89ce323dd9aef1d76f22e24a2aee19 Mon Sep 17 00:00:00 2001 From: chronark Date: Fri, 7 Nov 2025 10:33:57 +0100 Subject: [PATCH 07/10] wip --- go/apps/api/routes/chproxy_metrics/handler.go | 4 +- .../api/routes/chproxy_ratelimits/handler.go | 2 +- .../routes/chproxy_verifications/handler.go | 4 +- go/apps/api/routes/register.go | 4 +- .../api/routes/v2_ratelimit_limit/handler.go | 2 +- go/apps/api/run.go | 34 +++--- go/internal/services/keys/verifier.go | 2 +- go/pkg/clickhouse/client.go | 106 ++---------------- go/pkg/clickhouse/interface.go | 10 +- go/pkg/clickhouse/noop.go | 13 +-- go/pkg/clickhouse/schema/requests.go | 101 ----------------- go/pkg/zen/instance.go | 6 + go/pkg/zen/middleware_metrics.go | 13 +-- 13 files changed, 51 insertions(+), 250 deletions(-) delete mode 100644 go/pkg/clickhouse/schema/requests.go create mode 100644 go/pkg/zen/instance.go diff --git a/go/apps/api/routes/chproxy_metrics/handler.go b/go/apps/api/routes/chproxy_metrics/handler.go index 260f38b7df..399a453a8f 100644 --- a/go/apps/api/routes/chproxy_metrics/handler.go +++ b/go/apps/api/routes/chproxy_metrics/handler.go @@ -48,7 +48,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { fault.Public("The provided token is invalid.")) } - events, err := zen.BindBody[[]schema.ApiRequestV1](s) + events, err := zen.BindBody[[]schema.ApiRequestV2](s) if err != nil { return err } @@ -59,7 +59,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { // Buffer all events to ClickHouse for _, event := range events { - h.ClickHouse.BufferRequest(event) + h.ClickHouse.BufferApiRequest(event) } return s.JSON(http.StatusOK, map[string]string{"status": "OK"}) diff --git a/go/apps/api/routes/chproxy_ratelimits/handler.go b/go/apps/api/routes/chproxy_ratelimits/handler.go index ed02c8bb1b..2d232e8502 100644 --- a/go/apps/api/routes/chproxy_ratelimits/handler.go +++ b/go/apps/api/routes/chproxy_ratelimits/handler.go @@ -48,7 +48,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { fault.Public("The provided token is invalid.")) } - events, err := zen.BindBody[[]schema.RatelimitRequestV1](s) + events, err := zen.BindBody[[]schema.RatelimitV2](s) if err != nil { return err } diff --git a/go/apps/api/routes/chproxy_verifications/handler.go b/go/apps/api/routes/chproxy_verifications/handler.go index eafcc1fe4c..0b152f57f3 100644 --- a/go/apps/api/routes/chproxy_verifications/handler.go +++ b/go/apps/api/routes/chproxy_verifications/handler.go @@ -48,7 +48,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { fault.Public("The provided token is invalid.")) } - events, err := zen.BindBody[[]schema.KeyVerificationRequestV1](s) + events, err := zen.BindBody[[]schema.KeyVerificationV2](s) if err != nil { return err } @@ -59,7 +59,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { // Buffer all events to ClickHouse for _, event := range events { - h.ClickHouse.BufferKeyVerification(event) + h.ClickHouse.BufferKeyVerificationV2(event) } return s.JSON(http.StatusOK, map[string]string{"status": "OK"}) diff --git a/go/apps/api/routes/register.go b/go/apps/api/routes/register.go index 6f3f211dcb..52c16b83a7 100644 --- a/go/apps/api/routes/register.go +++ b/go/apps/api/routes/register.go @@ -59,9 +59,9 @@ import ( // here we register all of the routes. // this function runs during startup. -func Register(srv *zen.Server, svc *Services) { +func Register(srv *zen.Server, svc *Services, info zen.InstanceInfo) { withTracing := zen.WithTracing() - withMetrics := zen.WithMetrics(svc.ClickHouse) + withMetrics := zen.WithMetrics(svc.ClickHouse, info) withLogging := zen.WithLogging(svc.Logger) withPanicRecovery := zen.WithPanicRecovery(svc.Logger) diff --git a/go/apps/api/routes/v2_ratelimit_limit/handler.go b/go/apps/api/routes/v2_ratelimit_limit/handler.go index c877d148eb..3837dbc3a4 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/handler.go +++ b/go/apps/api/routes/v2_ratelimit_limit/handler.go @@ -286,7 +286,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } if s.ShouldLogRequestToClickHouse() { - h.ClickHouse.BufferRatelimit(schema.RatelimitRequestV1{ + h.ClickHouse.BufferRatelimit(schema.RatelimitV2{ RequestID: s.RequestID(), WorkspaceID: auth.AuthorizedWorkspaceID, Time: time.Now().UnixMilli(), diff --git a/go/apps/api/run.go b/go/apps/api/run.go index 0da2f4d259..6d52c3beab 100644 --- a/go/apps/api/run.go +++ b/go/apps/api/run.go @@ -291,20 +291,26 @@ func Run(ctx context.Context, cfg Config) error { } } - routes.Register(srv, &routes.Services{ - Logger: logger, - Database: db, - ClickHouse: ch, - Keys: keySvc, - Validator: validator, - Ratelimit: rlSvc, - Auditlogs: auditlogSvc, - Caches: caches, - Vault: vaultSvc, - ChproxyToken: cfg.ChproxyToken, - UsageLimiter: ulSvc, - AnalyticsConnectionManager: analyticsConnMgr, - }) + routes.Register( + srv, + &routes.Services{ + Logger: logger, + Database: db, + ClickHouse: ch, + Keys: keySvc, + Validator: validator, + Ratelimit: rlSvc, + Auditlogs: auditlogSvc, + Caches: caches, + Vault: vaultSvc, + ChproxyToken: cfg.ChproxyToken, + UsageLimiter: ulSvc, + AnalyticsConnectionManager: analyticsConnMgr, + }, + zen.InstanceInfo{ + ID: cfg.InstanceID, + Region: cfg.Region, + }) if cfg.Listener == nil { // Create listener from HttpPort (production) diff --git a/go/internal/services/keys/verifier.go b/go/internal/services/keys/verifier.go index 242b09f40e..503a16565e 100644 --- a/go/internal/services/keys/verifier.go +++ b/go/internal/services/keys/verifier.go @@ -122,7 +122,7 @@ func (k *KeyVerifier) Verify(ctx context.Context, opts ...VerifyOption) error { } func (k *KeyVerifier) log() { - k.clickhouse.BufferKeyVerification(schema.KeyVerificationRequestV1{ + k.clickhouse.BufferKeyVerificationV2(schema.KeyVerificationV2{ RequestID: k.session.RequestID(), WorkspaceID: k.Key.WorkspaceID, Time: time.Now().UnixMilli(), diff --git a/go/pkg/clickhouse/client.go b/go/pkg/clickhouse/client.go index 39085405d5..c0b8b78047 100644 --- a/go/pkg/clickhouse/client.go +++ b/go/pkg/clickhouse/client.go @@ -22,11 +22,9 @@ type clickhouse struct { logger logging.Logger // Batched processors for different event types - requests *batch.BatchProcessor[schema.ApiRequestV1] apiRequests *batch.BatchProcessor[schema.ApiRequestV2] - keyVerifications *batch.BatchProcessor[schema.KeyVerificationRequestV1] keyVerificationsV2 *batch.BatchProcessor[schema.KeyVerificationV2] - ratelimits *batch.BatchProcessor[schema.RatelimitRequestV1] + ratelimitsV2 *batch.BatchProcessor[schema.RatelimitV2] } var _ Bufferer = (*clickhouse)(nil) @@ -103,25 +101,6 @@ func New(config Config) (*clickhouse, error) { conn: conn, logger: config.Logger, - requests: batch.New(batch.Config[schema.ApiRequestV1]{ - Name: "requests", - Drop: true, - BatchSize: 50_000, - BufferSize: 200_000, - FlushInterval: 5 * time.Second, - Consumers: 2, - Flush: func(ctx context.Context, rows []schema.ApiRequestV1) { - table := "metrics.raw_api_requests_v1" - err := flush(ctx, conn, table, rows) - if err != nil { - config.Logger.Error("failed to flush batch", - "table", table, - "err", err.Error(), - ) - } - }, - }), - apiRequests: batch.New(batch.Config[schema.ApiRequestV2]{ Name: "api_requests", Drop: true, @@ -141,27 +120,6 @@ func New(config Config) (*clickhouse, error) { }, }), - keyVerifications: batch.New[schema.KeyVerificationRequestV1]( - batch.Config[schema.KeyVerificationRequestV1]{ - Name: "key_verifications", - Drop: true, - BatchSize: 50_000, - BufferSize: 200_000, - FlushInterval: 5 * time.Second, - Consumers: 2, - Flush: func(ctx context.Context, rows []schema.KeyVerificationRequestV1) { - table := "verifications.raw_key_verifications_v1" - err := flush(ctx, conn, table, rows) - if err != nil { - config.Logger.Error("failed to flush batch", - "table", table, - "error", err.Error(), - ) - } - }, - }, - ), - keyVerificationsV2: batch.New[schema.KeyVerificationV2]( batch.Config[schema.KeyVerificationV2]{ Name: "key_verifications_v2", @@ -183,16 +141,16 @@ func New(config Config) (*clickhouse, error) { }, ), - ratelimits: batch.New[schema.RatelimitRequestV1]( - batch.Config[schema.RatelimitRequestV1]{ + ratelimitsV2: batch.New( + batch.Config[schema.RatelimitV2]{ Name: "ratelimits", Drop: true, BatchSize: 50_000, BufferSize: 200_000, FlushInterval: 5 * time.Second, Consumers: 2, - Flush: func(ctx context.Context, rows []schema.RatelimitRequestV1) { - table := "ratelimits.raw_ratelimits_v1" + Flush: func(ctx context.Context, rows []schema.RatelimitV2) { + table := "default.ratelimits_raw_v2" err := flush(ctx, conn, table, rows) if err != nil { config.Logger.Error("failed to flush batch", @@ -224,29 +182,6 @@ func isAuthenticationError(err error) bool { strings.Contains(errStr, "code: 517") // Wrong password } -// BufferRequest adds an API request event to the buffer for batch processing. -// The event will be flushed to ClickHouse automatically based on the configured -// batch size and flush interval. -// -// This method is non-blocking if the buffer has available capacity. If the buffer -// is full and the Drop option is enabled (which is the default), the event will -// be silently dropped. -// -// Example: -// -// ch.BufferRequest(schema.ApiRequestV1{ -// RequestID: requestID, -// Time: time.Now().UnixMilli(), -// WorkspaceID: workspaceID, -// Host: r.Host, -// Method: r.Method, -// Path: r.URL.Path, -// ResponseStatus: status, -// }) -func (c *clickhouse) BufferRequest(req schema.ApiRequestV1) { - c.requests.Buffer(req) -} - // BufferApiRequest adds an API request event to the buffer for batch processing. // The event will be flushed to ClickHouse automatically based on the configured // batch size and flush interval. @@ -270,27 +205,6 @@ func (c *clickhouse) BufferApiRequest(req schema.ApiRequestV2) { c.apiRequests.Buffer(req) } -// BufferKeyVerification adds a key verification event to the buffer for batch processing. -// The event will be flushed to ClickHouse automatically based on the configured -// batch size and flush interval. -// -// This method is non-blocking if the buffer has available capacity. If the buffer -// is full and the Drop option is enabled (which is the default), the event will -// be silently dropped. -// -// Example: -// -// ch.BufferKeyVerification(schema.KeyVerificationRequestV1{ -// RequestID: requestID, -// Time: time.Now().UnixMilli(), -// WorkspaceID: workspaceID, -// KeyID: keyID, -// Outcome: "success", -// }) -func (c *clickhouse) BufferKeyVerification(req schema.KeyVerificationRequestV1) { - c.keyVerifications.Buffer(req) -} - // BufferKeyVerificationV2 adds a key verification event to the buffer for batch processing. // The event will be flushed to ClickHouse automatically based on the configured // batch size and flush interval. @@ -322,7 +236,7 @@ func (c *clickhouse) BufferKeyVerificationV2(req schema.KeyVerificationV2) { // // Example: // -// ch.BufferRatelimit(schema.RatelimitRequestV1{ +// ch.BufferRatelimit(schema.RatelimitV2{ // RequestID: requestID, // Time: time.Now().UnixMilli(), // WorkspaceID: workspaceID, @@ -330,8 +244,8 @@ func (c *clickhouse) BufferKeyVerificationV2(req schema.KeyVerificationV2) { // Identifier: identifier, // Passed: passed, // }) -func (c *clickhouse) BufferRatelimit(req schema.RatelimitRequestV1) { - c.ratelimits.Buffer(req) +func (c *clickhouse) BufferRatelimit(req schema.RatelimitV2) { + c.ratelimitsV2.Buffer(req) } func (c *clickhouse) Conn() ch.Conn { @@ -393,11 +307,9 @@ func (c *clickhouse) Ping(ctx context.Context) error { // It closes all batch processors (waiting for them to flush remaining data), // then closes the underlying ClickHouse connection. func (c *clickhouse) Close() error { - c.requests.Close() c.apiRequests.Close() - c.keyVerifications.Close() c.keyVerificationsV2.Close() - c.ratelimits.Close() + c.ratelimitsV2.Close() err := c.conn.Close() if err != nil { diff --git a/go/pkg/clickhouse/interface.go b/go/pkg/clickhouse/interface.go index b1a5d5c719..8a93a02fe4 100644 --- a/go/pkg/clickhouse/interface.go +++ b/go/pkg/clickhouse/interface.go @@ -14,25 +14,17 @@ import ( // This interface allows for different implementations, such as a real // ClickHouse client or a no-op implementation for testing or development. type Bufferer interface { - // BufferRequest adds an API request event to the buffer. - // These are typically HTTP requests to the API with request and response details. - BufferRequest(schema.ApiRequestV1) - // BufferApiRequest adds an API request event to the buffer. // These are typically HTTP requests to the API with request and response details. BufferApiRequest(schema.ApiRequestV2) - // BufferKeyVerification adds a key verification event to the buffer. - // These represent API key validation operations with their outcomes. - BufferKeyVerification(schema.KeyVerificationRequestV1) - // BufferKeyVerification adds a key verification event to the buffer. // These represent API key validation operations with their outcomes. BufferKeyVerificationV2(schema.KeyVerificationV2) // BufferRatelimit adds a ratelimit event to the buffer. // These represent API ratelimit operations with their outcome. - BufferRatelimit(schema.RatelimitRequestV1) + BufferRatelimit(schema.RatelimitV2) } type Querier interface { diff --git a/go/pkg/clickhouse/noop.go b/go/pkg/clickhouse/noop.go index b39978a32b..32a0f71c07 100644 --- a/go/pkg/clickhouse/noop.go +++ b/go/pkg/clickhouse/noop.go @@ -15,28 +15,17 @@ type noop struct{} var _ Bufferer = (*noop)(nil) var _ Bufferer = (*noop)(nil) -// BufferRequest implements the Bufferer interface but discards the event. -func (n *noop) BufferRequest(schema.ApiRequestV1) { - // Intentionally empty - discards the event -} - -// BufferApiRequest implements the Bufferer interface but discards the event. func (n *noop) BufferApiRequest(schema.ApiRequestV2) { // Intentionally empty - discards the event } -// BufferKeyVerification implements the Bufferer interface but discards the event. -func (n *noop) BufferKeyVerification(schema.KeyVerificationRequestV1) { - // Intentionally empty - discards the event -} - // BufferKeyVerificationV2 implements the Bufferer interface but discards the event. func (n *noop) BufferKeyVerificationV2(schema.KeyVerificationV2) { // Intentionally empty - discards the event } // BufferRatelimit implements the Bufferer interface but discards the event. -func (n *noop) BufferRatelimit(req schema.RatelimitRequestV1) { +func (n *noop) BufferRatelimit(req schema.RatelimitV2) { // Intentionally empty - discards the event } diff --git a/go/pkg/clickhouse/schema/requests.go b/go/pkg/clickhouse/schema/requests.go deleted file mode 100644 index 084ae39b79..0000000000 --- a/go/pkg/clickhouse/schema/requests.go +++ /dev/null @@ -1,101 +0,0 @@ -package schema - -// ApiRequestV1 represents an HTTP API request with its associated metadata, -// request details, and response information. This structure is used to log -// and analyze API usage patterns, performance metrics, and error rates. -// -// Fields are mapped to ClickHouse columns using the `ch` struct tags. -type ApiRequestV1 struct { - // WorkspaceID identifies the workspace that the request belongs to - WorkspaceID string `ch:"workspace_id" json:"workspace_id"` - - // RequestID is a unique identifier for this request - RequestID string `ch:"request_id" json:"request_id"` - - // Time is the Unix timestamp in milliseconds when the request was received - Time int64 `ch:"time" json:"time"` - - // Host is the hostname from the request (e.g., "api.unkey.dev") - Host string `ch:"host" json:"host"` - - // Method is the HTTP method (GET, POST, etc.) - Method string `ch:"method" json:"method"` - - // Path is the request URI path - Path string `ch:"path" json:"path"` - - // RequestHeaders contains the HTTP request headers - RequestHeaders []string `ch:"request_headers" json:"request_headers"` - - // RequestBody contains the HTTP request body (sanitized of sensitive data) - RequestBody string `ch:"request_body" json:"request_body"` - - // ResponseStatus is the HTTP status code returned - ResponseStatus int `ch:"response_status" json:"response_status"` - - // ResponseHeaders contains the HTTP response headers - ResponseHeaders []string `ch:"response_headers" json:"response_headers"` - - // ResponseBody contains the HTTP response body (sanitized of sensitive data) - ResponseBody string `ch:"response_body" json:"response_body"` - - // Error contains any error message if the request failed - Error string `ch:"error" json:"error"` - - // ServiceLatency is the time in milliseconds it took to process the request - ServiceLatency int64 `ch:"service_latency" json:"service_latency"` - - UserAgent string `ch:"user_agent" json:"user_agent"` - - IpAddress string `ch:"ip_address" json:"ip_address"` - - Country string `ch:"country" json:"country"` - - City string `ch:"city" json:"city"` - - Colo string `ch:"colo" json:"colo"` - Continent string `ch:"continent" json:"continent"` -} - -// KeyVerificationRequestV1 represents a key verification operation, tracking -// when and how API keys are validated. This structure is used to analyze -// key usage patterns, identify unauthorized access attempts, and track -// verification performance. -// -// Fields are mapped to ClickHouse columns using the `ch` struct tags. -type KeyVerificationRequestV1 struct { - // RequestID is a unique identifier for this verification request - RequestID string `ch:"request_id" json:"request_id"` - - // Time is the Unix timestamp in milliseconds when the verification occurred - Time int64 `ch:"time" json:"time"` - - // WorkspaceID identifies the workspace that the key belongs to - WorkspaceID string `ch:"workspace_id" json:"workspace_id"` - - // KeySpaceID identifies the key space that the key belongs to - KeySpaceID string `ch:"key_space_id" json:"key_space_id"` - - // KeyID is the unique identifier of the key being verified - KeyID string `ch:"key_id" json:"key_id"` - - // Region indicates the geographic region where the verification occurred - Region string `ch:"region" json:"region"` - - // Outcome is the result of the verification (e.g., "success", "invalid", "expired") - Outcome string `ch:"outcome" json:"outcome"` - - // IdentityID links the key to a specific identity, if applicable - IdentityID string `ch:"identity_id" json:"identity_id"` - - Tags []string `ch:"tags" json:"tags"` -} - -type RatelimitRequestV1 struct { - RequestID string `ch:"request_id" json:"request_id"` - Time int64 `ch:"time" json:"time"` - WorkspaceID string `ch:"workspace_id" json:"workspace_id"` - NamespaceID string `ch:"namespace_id" json:"namespace_id"` - Identifier string `ch:"identifier" json:"identifier"` - Passed bool `ch:"passed" json:"passed"` -} diff --git a/go/pkg/zen/instance.go b/go/pkg/zen/instance.go new file mode 100644 index 0000000000..f2e2963687 --- /dev/null +++ b/go/pkg/zen/instance.go @@ -0,0 +1,6 @@ +package zen + +type InstanceInfo struct { + ID string + Region string +} diff --git a/go/pkg/zen/middleware_metrics.go b/go/pkg/zen/middleware_metrics.go index 9b23e74c23..e97f17c121 100644 --- a/go/pkg/zen/middleware_metrics.go +++ b/go/pkg/zen/middleware_metrics.go @@ -14,7 +14,7 @@ import ( ) type EventBuffer interface { - BufferRequest(schema.ApiRequestV1) + BufferApiRequest(schema.ApiRequestV2) } type redactionRule struct { @@ -56,7 +56,7 @@ func redact(in []byte) []byte { // []zen.Middleware{zen.WithMetrics(eventBuffer)}, // route, // ) -func WithMetrics(eventBuffer EventBuffer) Middleware { +func WithMetrics(eventBuffer EventBuffer, info InstanceInfo) Middleware { return func(next HandleFunc) HandleFunc { return func(ctx context.Context, s *Session) error { start := time.Now() @@ -93,7 +93,7 @@ func WithMetrics(eventBuffer EventBuffer) Middleware { ipAddress = ips[0] } - eventBuffer.BufferRequest(schema.ApiRequestV1{ + eventBuffer.BufferApiRequest(schema.ApiRequestV2{ WorkspaceID: s.WorkspaceID, RequestID: s.RequestID(), Time: start.UnixMilli(), @@ -102,17 +102,14 @@ func WithMetrics(eventBuffer EventBuffer) Middleware { Path: s.r.URL.Path, RequestHeaders: requestHeaders, RequestBody: string(redact(s.requestBody)), - ResponseStatus: s.responseStatus, + ResponseStatus: int32(s.responseStatus), ResponseHeaders: responseHeaders, ResponseBody: string(redact(s.responseBody)), Error: fault.UserFacingMessage(nextErr), ServiceLatency: serviceLatency.Milliseconds(), UserAgent: s.r.Header.Get("User-Agent"), IpAddress: ipAddress, - Country: "", - City: "", - Colo: "", - Continent: "", + Region: info.Region, }) } return nextErr From 63fbd2393ed82199ae7d545f202202f2ceb402bc Mon Sep 17 00:00:00 2001 From: chronark Date: Tue, 11 Nov 2025 17:29:36 +0100 Subject: [PATCH 08/10] refactor: rename schemas --- cpu.prof | Bin 0 -> 34602 bytes go/apps/api/integration/harness.go | 3 ++ go/apps/api/routes/chproxy_metrics/handler.go | 2 +- .../api/routes/chproxy_ratelimits/handler.go | 2 +- .../routes/chproxy_verifications/handler.go | 4 +- go/apps/api/routes/register.go | 2 + .../200_test.go | 10 ++--- .../422_test.go | 2 +- .../429_test.go | 4 +- .../api/routes/v2_ratelimit_limit/200_test.go | 4 +- .../api/routes/v2_ratelimit_limit/handler.go | 14 ++++-- go/apps/api/run.go | 1 - go/apps/gw/server/middleware_metrics.go | 4 +- go/cmd/dev/seed/verifications.go | 2 +- go/go.sum | 2 - go/internal/services/keys/verifier.go | 24 +++++----- go/pkg/clickhouse/client.go | 42 +++++++++--------- go/pkg/clickhouse/interface.go | 6 +-- go/pkg/clickhouse/key_verifications_test.go | 28 ++++++------ go/pkg/clickhouse/noop.go | 8 ++-- go/pkg/clickhouse/ratelimits_test.go | 12 ++--- go/pkg/clickhouse/schema/types.go | 12 ++--- go/pkg/zen/middleware_metrics.go | 4 +- 23 files changed, 102 insertions(+), 90 deletions(-) create mode 100644 cpu.prof diff --git a/cpu.prof b/cpu.prof new file mode 100644 index 0000000000000000000000000000000000000000..d523c7014e126c12418bfe3285161be3a146744d GIT binary patch literal 34602 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 literal 0 HcmV?d00001 diff --git a/go/apps/api/integration/harness.go b/go/apps/api/integration/harness.go index 9fb94e372f..1eaf435b9d 100644 --- a/go/apps/api/integration/harness.go +++ b/go/apps/api/integration/harness.go @@ -163,6 +163,9 @@ func (h *Harness) RunAPI(config ApiConfig) *ApiCluster { VaultS3: nil, KafkaBrokers: kafkaBrokers, // Use host brokers for test runner connections DebugCacheHeaders: true, // Enable cache debug headers for integration tests + PprofEnabled: true, + PprofUsername: "unkey", + PprofPassword: "password", } // Start API server in goroutine diff --git a/go/apps/api/routes/chproxy_metrics/handler.go b/go/apps/api/routes/chproxy_metrics/handler.go index 399a453a8f..ea003d0a2a 100644 --- a/go/apps/api/routes/chproxy_metrics/handler.go +++ b/go/apps/api/routes/chproxy_metrics/handler.go @@ -48,7 +48,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { fault.Public("The provided token is invalid.")) } - events, err := zen.BindBody[[]schema.ApiRequestV2](s) + events, err := zen.BindBody[[]schema.ApiRequest](s) if err != nil { return err } diff --git a/go/apps/api/routes/chproxy_ratelimits/handler.go b/go/apps/api/routes/chproxy_ratelimits/handler.go index 2d232e8502..2bbf10905e 100644 --- a/go/apps/api/routes/chproxy_ratelimits/handler.go +++ b/go/apps/api/routes/chproxy_ratelimits/handler.go @@ -48,7 +48,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { fault.Public("The provided token is invalid.")) } - events, err := zen.BindBody[[]schema.RatelimitV2](s) + events, err := zen.BindBody[[]schema.Ratelimit](s) if err != nil { return err } diff --git a/go/apps/api/routes/chproxy_verifications/handler.go b/go/apps/api/routes/chproxy_verifications/handler.go index 0b152f57f3..4a45a65101 100644 --- a/go/apps/api/routes/chproxy_verifications/handler.go +++ b/go/apps/api/routes/chproxy_verifications/handler.go @@ -48,7 +48,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { fault.Public("The provided token is invalid.")) } - events, err := zen.BindBody[[]schema.KeyVerificationV2](s) + events, err := zen.BindBody[[]schema.KeyVerification](s) if err != nil { return err } @@ -59,7 +59,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { // Buffer all events to ClickHouse for _, event := range events { - h.ClickHouse.BufferKeyVerificationV2(event) + h.ClickHouse.BufferKeyVerification(event) } return s.JSON(http.StatusOK, map[string]string{"status": "OK"}) diff --git a/go/apps/api/routes/register.go b/go/apps/api/routes/register.go index 2bc9d7ca71..5f9f1fce9a 100644 --- a/go/apps/api/routes/register.go +++ b/go/apps/api/routes/register.go @@ -61,6 +61,8 @@ import ( // here we register all of the routes. // this function runs during startup. +func Register(srv *zen.Server, svc *Services, info zen.InstanceInfo) { + withObservability := zen.WithObservability() withMetrics := zen.WithMetrics(svc.ClickHouse, info) withLogging := zen.WithLogging(svc.Logger) diff --git a/go/apps/api/routes/v2_analytics_get_verifications/200_test.go b/go/apps/api/routes/v2_analytics_get_verifications/200_test.go index 3a5c9b3a08..16e86592a2 100644 --- a/go/apps/api/routes/v2_analytics_get_verifications/200_test.go +++ b/go/apps/api/routes/v2_analytics_get_verifications/200_test.go @@ -27,7 +27,7 @@ func Test200_Success(t *testing.T) { // Buffer some key verifications for i := range 5 { - h.ClickHouse.BufferKeyVerification(schema.KeyVerificationRequestV1{ + h.ClickHouse.BufferKeyVerification(schema.KeyVerification{ RequestID: uid.New(uid.RequestPrefix), Time: now - int64(i*1000), WorkspaceID: workspace.ID, @@ -89,7 +89,7 @@ func Test200_PermissionFiltersByApiId(t *testing.T) { // Buffer verifications for api1 for i := range 3 { - h.ClickHouse.BufferKeyVerification(schema.KeyVerificationRequestV1{ + h.ClickHouse.BufferKeyVerification(schema.KeyVerification{ RequestID: uid.New(uid.RequestPrefix), Time: now - int64(i*1000), WorkspaceID: workspace.ID, @@ -104,7 +104,7 @@ func Test200_PermissionFiltersByApiId(t *testing.T) { // Buffer verifications for api2 (should NOT be returned) for i := range 5 { - h.ClickHouse.BufferKeyVerification(schema.KeyVerificationRequestV1{ + h.ClickHouse.BufferKeyVerification(schema.KeyVerification{ RequestID: uid.New(uid.RequestPrefix), Time: now - int64(i*1000), WorkspaceID: workspace.ID, @@ -170,7 +170,7 @@ func Test200_PermissionFiltersByKeySpaceId(t *testing.T) { // Buffer verifications for api1 for i := range 3 { - h.ClickHouse.BufferKeyVerification(schema.KeyVerificationRequestV1{ + h.ClickHouse.BufferKeyVerification(schema.KeyVerification{ RequestID: uid.New(uid.RequestPrefix), Time: now - int64(i*1000), WorkspaceID: workspace.ID, @@ -185,7 +185,7 @@ func Test200_PermissionFiltersByKeySpaceId(t *testing.T) { // Buffer verifications for api2 (should NOT be returned) for i := range 5 { - h.ClickHouse.BufferKeyVerification(schema.KeyVerificationRequestV1{ + h.ClickHouse.BufferKeyVerification(schema.KeyVerification{ RequestID: uid.New(uid.RequestPrefix), Time: now - int64(i*1000), WorkspaceID: workspace.ID, diff --git a/go/apps/api/routes/v2_analytics_get_verifications/422_test.go b/go/apps/api/routes/v2_analytics_get_verifications/422_test.go index 0ad8e3b6a2..7ea71dfe6b 100644 --- a/go/apps/api/routes/v2_analytics_get_verifications/422_test.go +++ b/go/apps/api/routes/v2_analytics_get_verifications/422_test.go @@ -28,7 +28,7 @@ func Test422_ExceedsMaxMemory(t *testing.T) { // Buffer many verifications to ensure memory usage exceeds limit for i := range 50_000 { - h.ClickHouse.BufferKeyVerification(schema.KeyVerificationRequestV1{ + h.ClickHouse.BufferKeyVerification(schema.KeyVerification{ RequestID: uid.New(uid.RequestPrefix), Time: now - int64(i*1000), WorkspaceID: workspace.ID, diff --git a/go/apps/api/routes/v2_analytics_get_verifications/429_test.go b/go/apps/api/routes/v2_analytics_get_verifications/429_test.go index d6e7fffb33..44f7d6aea5 100644 --- a/go/apps/api/routes/v2_analytics_get_verifications/429_test.go +++ b/go/apps/api/routes/v2_analytics_get_verifications/429_test.go @@ -27,8 +27,8 @@ func Test429_QueryQuotaExceeded(t *testing.T) { now := h.Clock.Now().UnixMilli() // Buffer some key verifications - for i := 0; i < 5; i++ { - h.ClickHouse.BufferKeyVerification(schema.KeyVerificationRequestV1{ + for i := range 5 { + h.ClickHouse.BufferKeyVerification(schema.KeyVerification{ RequestID: uid.New(uid.RequestPrefix), Time: now - int64(i*1000), WorkspaceID: workspace.ID, diff --git a/go/apps/api/routes/v2_ratelimit_limit/200_test.go b/go/apps/api/routes/v2_ratelimit_limit/200_test.go index d1fb9e7653..7011f2c8d5 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/200_test.go @@ -127,10 +127,10 @@ func TestLimitSuccessfully(t *testing.T) { res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) require.Equal(t, 200, res.Status, "expected 200, received: %v", res.Body) - row := schema.RatelimitV2{} + row := schema.Ratelimit{} require.Eventually(t, func() bool { - data, err := clickhouse.Select[schema.RatelimitV2]( + data, err := clickhouse.Select[schema.Ratelimit]( ctx, h.ClickHouse.Conn(), "SELECT * FROM default.ratelimits_raw_v2 WHERE workspace_id = {workspace_id:String} AND namespace_id = {namespace_id:String}", diff --git a/go/apps/api/routes/v2_ratelimit_limit/handler.go b/go/apps/api/routes/v2_ratelimit_limit/handler.go index b83cf12489..c20c8d98cc 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/handler.go +++ b/go/apps/api/routes/v2_ratelimit_limit/handler.go @@ -246,7 +246,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } // Apply override if found, otherwise use request values - limit, duration, overrideId, err := getLimitAndDuration(req, namespace) + limit, duration, overrideID, err := getLimitAndDuration(req, namespace) if err != nil { return fault.Wrap(err, fault.Code(codes.App.Internal.UnexpectedError.URN()), @@ -277,6 +277,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } } + t0 := time.Now() result, err := h.Ratelimit.Ratelimit(ctx, limitReq) if err != nil { return fault.Wrap(err, @@ -284,15 +285,20 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { fault.Public("We're unable to process the rate limit request."), ) } - + latency := time.Since(t0).Milliseconds() if s.ShouldLogRequestToClickHouse() { - h.ClickHouse.BufferRatelimit(schema.RatelimitV2{ + h.ClickHouse.BufferRatelimit(schema.Ratelimit{ RequestID: s.RequestID(), WorkspaceID: auth.AuthorizedWorkspaceID, Time: time.Now().UnixMilli(), NamespaceID: namespace.ID, Identifier: req.Identifier, Passed: result.Success, + Latency: float64(latency), + OverrideID: overrideID, + Limit: uint64(result.Limit), + Remaining: uint64(result.Remaining), + ResetAt: result.Reset.UnixMilli(), }) } @@ -305,7 +311,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { Limit: limit, Remaining: result.Remaining, Reset: result.Reset.UnixMilli(), - OverrideId: overrideId, + OverrideId: overrideID, }, } diff --git a/go/apps/api/run.go b/go/apps/api/run.go index c6d7701a22..7920dd0164 100644 --- a/go/apps/api/run.go +++ b/go/apps/api/run.go @@ -291,7 +291,6 @@ func Run(ctx context.Context, cfg Config) error { } } - routes.Register(srv, &routes.Services{ Logger: logger, Database: db, diff --git a/go/apps/gw/server/middleware_metrics.go b/go/apps/gw/server/middleware_metrics.go index 7db90616c0..14fb7be798 100644 --- a/go/apps/gw/server/middleware_metrics.go +++ b/go/apps/gw/server/middleware_metrics.go @@ -10,7 +10,7 @@ import ( // EventBuffer defines the interface for buffering events to be sent to ClickHouse. type EventBuffer interface { - BufferApiRequest(schema.ApiRequestV2) + BufferApiRequest(schema.ApiRequest) } // WithMetrics returns middleware that collects metrics about each request, @@ -62,7 +62,7 @@ func WithMetrics(eventBuffer EventBuffer, region string) Middleware { ipAddress = s.Location() } - eventBuffer.BufferApiRequest(schema.ApiRequestV2{ + eventBuffer.BufferApiRequest(schema.ApiRequest{ QueryString: "", QueryParams: map[string][]string{}, WorkspaceID: s.WorkspaceID, diff --git a/go/cmd/dev/seed/verifications.go b/go/cmd/dev/seed/verifications.go index 61015946c7..d4127e313d 100644 --- a/go/cmd/dev/seed/verifications.go +++ b/go/cmd/dev/seed/verifications.go @@ -469,7 +469,7 @@ func (s *Seeder) generateVerifications(ctx context.Context, workspaceID string, } // Use BufferKeyVerification to let the clickhouse client batch automatically - s.clickhouse.BufferKeyVerificationV2(schema.KeyVerificationV2{ + s.clickhouse.BufferKeyVerification(schema.KeyVerification{ RequestID: uid.New("req"), Time: timestamp.UnixMilli(), WorkspaceID: workspaceID, diff --git a/go/go.sum b/go/go.sum index d0a61b8992..372338bb5e 100644 --- a/go/go.sum +++ b/go/go.sum @@ -10,8 +10,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/AfterShip/clickhouse-sql-parser v0.4.15 h1:OJCabxkbtnWHtjkEUH0BHZQGzDGyRrRIoMdR1ayRvJA= -github.com/AfterShip/clickhouse-sql-parser v0.4.15/go.mod h1:W0Z82wJWkJxz2RVun/RMwxue3g7ut47Xxl+SFqdJGus= github.com/AfterShip/clickhouse-sql-parser v0.4.16 h1:gpl+wXclYUKT0p4+gBq22XeRYWwEoZ9f35vogqMvkLQ= github.com/AfterShip/clickhouse-sql-parser v0.4.16/go.mod h1:W0Z82wJWkJxz2RVun/RMwxue3g7ut47Xxl+SFqdJGus= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= diff --git a/go/internal/services/keys/verifier.go b/go/internal/services/keys/verifier.go index 503a16565e..abb6caec2d 100644 --- a/go/internal/services/keys/verifier.go +++ b/go/internal/services/keys/verifier.go @@ -122,16 +122,20 @@ func (k *KeyVerifier) Verify(ctx context.Context, opts ...VerifyOption) error { } func (k *KeyVerifier) log() { - k.clickhouse.BufferKeyVerificationV2(schema.KeyVerificationV2{ - RequestID: k.session.RequestID(), - WorkspaceID: k.Key.WorkspaceID, - Time: time.Now().UnixMilli(), - Outcome: string(k.Status), - KeySpaceID: k.Key.KeyAuthID, - KeyID: k.Key.ID, - IdentityID: k.Key.IdentityID.String, - Tags: k.tags, - Region: k.region, + + k.clickhouse.BufferKeyVerification(schema.KeyVerification{ + RequestID: k.session.RequestID(), + WorkspaceID: k.Key.WorkspaceID, + Time: time.Now().UnixMilli(), + Outcome: string(k.Status), + KeySpaceID: k.Key.KeyAuthID, + KeyID: k.Key.ID, + IdentityID: k.Key.IdentityID.String, + Tags: k.tags, + Region: k.region, + ExternalID: k.Key.ExternalID.String, + SpentCredits: 0, // TODO + Latency: 0.0, // TODO }) keyType := "key" diff --git a/go/pkg/clickhouse/client.go b/go/pkg/clickhouse/client.go index c0b8b78047..2f2cd028bf 100644 --- a/go/pkg/clickhouse/client.go +++ b/go/pkg/clickhouse/client.go @@ -22,9 +22,9 @@ type clickhouse struct { logger logging.Logger // Batched processors for different event types - apiRequests *batch.BatchProcessor[schema.ApiRequestV2] - keyVerificationsV2 *batch.BatchProcessor[schema.KeyVerificationV2] - ratelimitsV2 *batch.BatchProcessor[schema.RatelimitV2] + apiRequests *batch.BatchProcessor[schema.ApiRequest] + keyVerifications *batch.BatchProcessor[schema.KeyVerification] + ratelimits *batch.BatchProcessor[schema.Ratelimit] } var _ Bufferer = (*clickhouse)(nil) @@ -101,14 +101,14 @@ func New(config Config) (*clickhouse, error) { conn: conn, logger: config.Logger, - apiRequests: batch.New(batch.Config[schema.ApiRequestV2]{ + apiRequests: batch.New(batch.Config[schema.ApiRequest]{ Name: "api_requests", Drop: true, BatchSize: 50_000, BufferSize: 200_000, FlushInterval: 5 * time.Second, Consumers: 2, - Flush: func(ctx context.Context, rows []schema.ApiRequestV2) { + Flush: func(ctx context.Context, rows []schema.ApiRequest) { table := "default.api_requests_raw_v2" err := flush(ctx, conn, table, rows) if err != nil { @@ -120,15 +120,15 @@ func New(config Config) (*clickhouse, error) { }, }), - keyVerificationsV2: batch.New[schema.KeyVerificationV2]( - batch.Config[schema.KeyVerificationV2]{ + keyVerifications: batch.New[schema.KeyVerification]( + batch.Config[schema.KeyVerification]{ Name: "key_verifications_v2", Drop: true, BatchSize: 50_000, BufferSize: 200_000, FlushInterval: 5 * time.Second, Consumers: 2, - Flush: func(ctx context.Context, rows []schema.KeyVerificationV2) { + Flush: func(ctx context.Context, rows []schema.KeyVerification) { table := "default.key_verifications_raw_v2" err := flush(ctx, conn, table, rows) if err != nil { @@ -141,15 +141,15 @@ func New(config Config) (*clickhouse, error) { }, ), - ratelimitsV2: batch.New( - batch.Config[schema.RatelimitV2]{ + ratelimits: batch.New( + batch.Config[schema.Ratelimit]{ Name: "ratelimits", Drop: true, BatchSize: 50_000, BufferSize: 200_000, FlushInterval: 5 * time.Second, Consumers: 2, - Flush: func(ctx context.Context, rows []schema.RatelimitV2) { + Flush: func(ctx context.Context, rows []schema.Ratelimit) { table := "default.ratelimits_raw_v2" err := flush(ctx, conn, table, rows) if err != nil { @@ -192,7 +192,7 @@ func isAuthenticationError(err error) bool { // // Example: // -// ch.BufferApiRequest(schema.ApiRequestV2{ +// ch.BufferApiRequest(schema.ApiRequest{ // RequestID: requestID, // Time: time.Now().UnixMilli(), // WorkspaceID: workspaceID, @@ -201,11 +201,11 @@ func isAuthenticationError(err error) bool { // Path: r.URL.Path, // ResponseStatus: status, // }) -func (c *clickhouse) BufferApiRequest(req schema.ApiRequestV2) { +func (c *clickhouse) BufferApiRequest(req schema.ApiRequest) { c.apiRequests.Buffer(req) } -// BufferKeyVerificationV2 adds a key verification event to the buffer for batch processing. +// BufferKeyVerification adds a key verification event to the buffer for batch processing. // The event will be flushed to ClickHouse automatically based on the configured // batch size and flush interval. // @@ -222,8 +222,8 @@ func (c *clickhouse) BufferApiRequest(req schema.ApiRequestV2) { // KeyID: keyID, // Outcome: "success", // }) -func (c *clickhouse) BufferKeyVerificationV2(req schema.KeyVerificationV2) { - c.keyVerificationsV2.Buffer(req) +func (c *clickhouse) BufferKeyVerification(req schema.KeyVerification) { + c.keyVerifications.Buffer(req) } // BufferRatelimit adds a ratelimit event to the buffer for batch processing. @@ -236,7 +236,7 @@ func (c *clickhouse) BufferKeyVerificationV2(req schema.KeyVerificationV2) { // // Example: // -// ch.BufferRatelimit(schema.RatelimitV2{ +// ch.BufferRatelimit(schema.Ratelimit{ // RequestID: requestID, // Time: time.Now().UnixMilli(), // WorkspaceID: workspaceID, @@ -244,8 +244,8 @@ func (c *clickhouse) BufferKeyVerificationV2(req schema.KeyVerificationV2) { // Identifier: identifier, // Passed: passed, // }) -func (c *clickhouse) BufferRatelimit(req schema.RatelimitV2) { - c.ratelimitsV2.Buffer(req) +func (c *clickhouse) BufferRatelimit(req schema.Ratelimit) { + c.ratelimits.Buffer(req) } func (c *clickhouse) Conn() ch.Conn { @@ -308,8 +308,8 @@ func (c *clickhouse) Ping(ctx context.Context) error { // then closes the underlying ClickHouse connection. func (c *clickhouse) Close() error { c.apiRequests.Close() - c.keyVerificationsV2.Close() - c.ratelimitsV2.Close() + c.keyVerifications.Close() + c.ratelimits.Close() err := c.conn.Close() if err != nil { diff --git a/go/pkg/clickhouse/interface.go b/go/pkg/clickhouse/interface.go index 8a93a02fe4..b64ec384f6 100644 --- a/go/pkg/clickhouse/interface.go +++ b/go/pkg/clickhouse/interface.go @@ -16,15 +16,15 @@ import ( type Bufferer interface { // BufferApiRequest adds an API request event to the buffer. // These are typically HTTP requests to the API with request and response details. - BufferApiRequest(schema.ApiRequestV2) + BufferApiRequest(schema.ApiRequest) // BufferKeyVerification adds a key verification event to the buffer. // These represent API key validation operations with their outcomes. - BufferKeyVerificationV2(schema.KeyVerificationV2) + BufferKeyVerification(schema.KeyVerification) // BufferRatelimit adds a ratelimit event to the buffer. // These represent API ratelimit operations with their outcome. - BufferRatelimit(schema.RatelimitV2) + BufferRatelimit(schema.Ratelimit) } type Querier interface { diff --git a/go/pkg/clickhouse/key_verifications_test.go b/go/pkg/clickhouse/key_verifications_test.go index 177c40be4b..91632bef00 100644 --- a/go/pkg/clickhouse/key_verifications_test.go +++ b/go/pkg/clickhouse/key_verifications_test.go @@ -68,7 +68,7 @@ func TestKeyVerifications(t *testing.T) { t0 := time.Now() // Source of truth: track all inserted rows - verifications := array.Fill(numRecords, func() schema.KeyVerificationV2 { + verifications := array.Fill(numRecords, func() schema.KeyVerification { timeRange := endTime.Sub(startTime) randomOffset := time.Duration(rand.Int63n(int64(timeRange))) timestamp := startTime.Add(randomOffset) @@ -78,7 +78,7 @@ func TestKeyVerifications(t *testing.T) { latency += rand.Float64() * 400 // Up to 500ms } identityID := array.Random(identities) - return schema.KeyVerificationV2{ + return schema.KeyVerification{ RequestID: uid.New(uid.RequestPrefix), Time: timestamp.UnixMilli(), WorkspaceID: workspaceID, @@ -133,7 +133,7 @@ func TestKeyVerifications(t *testing.T) { }) t.Run("all outcomes are correct", func(t *testing.T) { - countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerificationV2) map[string]int { + countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerification) map[string]int { acc[v.Outcome] = acc[v.Outcome] + 1 return acc }, map[string]int{}) @@ -157,7 +157,7 @@ func TestKeyVerifications(t *testing.T) { t.Parallel() for _, keyID := range keys[:10] { - countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerificationV2) map[string]int { + countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerification) map[string]int { if v.KeyID == keyID { acc[v.Outcome] = acc[v.Outcome] + 1 } @@ -184,7 +184,7 @@ func TestKeyVerifications(t *testing.T) { t.Parallel() for _, identityID := range identities[:10] { - countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerificationV2) map[string]int { + countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerification) map[string]int { if v.IdentityID == identityID { acc[v.Outcome] = acc[v.Outcome] + 1 } @@ -212,7 +212,7 @@ func TestKeyVerifications(t *testing.T) { continue } tag := usedTags[0] - countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerificationV2) map[string]int { + countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerification) map[string]int { if slices.Contains(v.Tags, tag) { acc[v.Outcome] = acc[v.Outcome] + 1 } @@ -235,7 +235,7 @@ func TestKeyVerifications(t *testing.T) { }) t.Run("latency aggregates are correct", func(t *testing.T) { t.Parallel() - latencies := array.Map(verifications, func(v schema.KeyVerificationV2) float64 { + latencies := array.Map(verifications, func(v schema.KeyVerification) float64 { return v.Latency }) avg := calculateAverage(latencies) @@ -262,7 +262,7 @@ func TestKeyVerifications(t *testing.T) { t.Run("credits spent globally are correct", func(t *testing.T) { t.Parallel() - credits := array.Reduce(verifications, func(acc int64, v schema.KeyVerificationV2) int64 { + credits := array.Reduce(verifications, func(acc int64, v schema.KeyVerification) int64 { return acc + v.SpentCredits }, int64(0)) @@ -280,7 +280,7 @@ func TestKeyVerifications(t *testing.T) { t.Run("credits spent per identity are correct", func(t *testing.T) { t.Parallel() for _, identityID := range identities[:10] { - credits := array.Reduce(verifications, func(acc int64, v schema.KeyVerificationV2) int64 { + credits := array.Reduce(verifications, func(acc int64, v schema.KeyVerification) int64 { if v.IdentityID == identityID { acc += v.SpentCredits } @@ -304,7 +304,7 @@ func TestKeyVerifications(t *testing.T) { t.Run("credits spent per key are correct", func(t *testing.T) { t.Parallel() for _, keyID := range keys[:10] { - credits := array.Reduce(verifications, func(acc int64, v schema.KeyVerificationV2) int64 { + credits := array.Reduce(verifications, func(acc int64, v schema.KeyVerification) int64 { if v.KeyID == keyID { acc += v.SpentCredits } @@ -353,7 +353,7 @@ func TestKeyVerifications(t *testing.T) { for _, identityID := range identities[:10] { id := identityID extID := identityToExternalID[id] - expectedCount := array.Reduce(verifications, func(acc int, v schema.KeyVerificationV2) int { + expectedCount := array.Reduce(verifications, func(acc int, v schema.KeyVerification) int { if v.ExternalID == extID { acc++ } @@ -382,7 +382,7 @@ func TestKeyVerifications(t *testing.T) { id := identityID extID := identityToExternalID[id] - countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerificationV2) map[string]int { + countByOutcome := array.Reduce(verifications, func(acc map[string]int, v schema.KeyVerification) map[string]int { if v.ExternalID == extID { acc[v.Outcome]++ } @@ -448,7 +448,7 @@ func TestKeyVerifications(t *testing.T) { for _, identityID := range identities[:10] { id := identityID extID := identityToExternalID[id] - expectedCredits := array.Reduce(verifications, func(acc int64, v schema.KeyVerificationV2) int64 { + expectedCredits := array.Reduce(verifications, func(acc int64, v schema.KeyVerification) int64 { if v.ExternalID == extID { acc += v.SpentCredits } @@ -472,7 +472,7 @@ func TestKeyVerifications(t *testing.T) { t.Run("billing per workspace is correct", func(t *testing.T) { t.Parallel() - billableVerifications := array.Reduce(verifications, func(acc int64, v schema.KeyVerificationV2) int64 { + billableVerifications := array.Reduce(verifications, func(acc int64, v schema.KeyVerification) int64 { if v.Outcome == "VALID" { acc += 1 } diff --git a/go/pkg/clickhouse/noop.go b/go/pkg/clickhouse/noop.go index 32a0f71c07..a25b783f6a 100644 --- a/go/pkg/clickhouse/noop.go +++ b/go/pkg/clickhouse/noop.go @@ -15,17 +15,17 @@ type noop struct{} var _ Bufferer = (*noop)(nil) var _ Bufferer = (*noop)(nil) -func (n *noop) BufferApiRequest(schema.ApiRequestV2) { +func (n *noop) BufferApiRequest(schema.ApiRequest) { // Intentionally empty - discards the event } -// BufferKeyVerificationV2 implements the Bufferer interface but discards the event. -func (n *noop) BufferKeyVerificationV2(schema.KeyVerificationV2) { +// BufferKeyVerification implements the Bufferer interface but discards the event. +func (n *noop) BufferKeyVerification(schema.KeyVerification) { // Intentionally empty - discards the event } // BufferRatelimit implements the Bufferer interface but discards the event. -func (n *noop) BufferRatelimit(req schema.RatelimitV2) { +func (n *noop) BufferRatelimit(req schema.Ratelimit) { // Intentionally empty - discards the event } diff --git a/go/pkg/clickhouse/ratelimits_test.go b/go/pkg/clickhouse/ratelimits_test.go index 4515d0e5a3..6004db6dd9 100644 --- a/go/pkg/clickhouse/ratelimits_test.go +++ b/go/pkg/clickhouse/ratelimits_test.go @@ -61,7 +61,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { t0 := time.Now() // Source of truth: track all inserted ratelimit records - ratelimits := array.Fill(numRecords, func() schema.RatelimitV2 { + ratelimits := array.Fill(numRecords, func() schema.Ratelimit { timeRange := endTime.Sub(startTime) randomOffset := time.Duration(rand.Int63n(int64(timeRange))) timestamp := startTime.Add(randomOffset) @@ -89,7 +89,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { latency += rand.Float64() * 10 // Up to 15ms for complex checks } - return schema.RatelimitV2{ + return schema.Ratelimit{ RequestID: uid.New(uid.RequestPrefix), Time: timestamp.UnixMilli(), WorkspaceID: workspaceID, @@ -128,7 +128,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { t.Run("pass/total counts are correct", func(t *testing.T) { // Calculate expected totals from source data - totalPassed := array.Reduce(ratelimits, func(acc int, r schema.RatelimitV2) int { + totalPassed := array.Reduce(ratelimits, func(acc int, r schema.Ratelimit) int { if r.Passed { return acc + 1 } @@ -151,7 +151,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { }) t.Run("latency aggregates are correct", func(t *testing.T) { - latencies := array.Map(ratelimits, func(r schema.RatelimitV2) float64 { + latencies := array.Map(ratelimits, func(r schema.Ratelimit) float64 { return r.Latency }) avg := calculateAverage(latencies) @@ -179,7 +179,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { t.Run("namespace-level aggregation is correct", func(t *testing.T) { // Group by namespace and calculate expected totals - namespaceStats := array.Reduce(ratelimits, func(acc map[string]struct{ passed, total int }, r schema.RatelimitV2) map[string]struct{ passed, total int } { + namespaceStats := array.Reduce(ratelimits, func(acc map[string]struct{ passed, total int }, r schema.Ratelimit) map[string]struct{ passed, total int } { stats := acc[r.NamespaceID] stats.total++ if r.Passed { @@ -206,7 +206,7 @@ func TestRatelimits_ComprehensiveLoadTest(t *testing.T) { t.Run("identifier-level aggregation is correct", func(t *testing.T) { // Group by identifier and calculate expected totals - identifierStats := array.Reduce(ratelimits, func(acc map[string]struct{ passed, total int }, r schema.RatelimitV2) map[string]struct{ passed, total int } { + identifierStats := array.Reduce(ratelimits, func(acc map[string]struct{ passed, total int }, r schema.Ratelimit) map[string]struct{ passed, total int } { stats := acc[r.Identifier] stats.total++ if r.Passed { diff --git a/go/pkg/clickhouse/schema/types.go b/go/pkg/clickhouse/schema/types.go index 01a1f5a4c6..57eb47ea79 100644 --- a/go/pkg/clickhouse/schema/types.go +++ b/go/pkg/clickhouse/schema/types.go @@ -1,9 +1,9 @@ package schema -// KeyVerificationV2 represents the v2 key verification raw table structure. +// KeyVerification represents the v2 key verification raw table structure. // This matches the key_verifications_raw_v2 table schema with additional // fields like spent_credits and latency compared to v1. -type KeyVerificationV2 struct { +type KeyVerification struct { RequestID string `ch:"request_id" json:"request_id"` Time int64 `ch:"time" json:"time"` WorkspaceID string `ch:"workspace_id" json:"workspace_id"` @@ -18,10 +18,10 @@ type KeyVerificationV2 struct { Latency float64 `ch:"latency" json:"latency"` } -// RatelimitV2 represents the v2 ratelimit raw table structure. +// Ratelimit represents the v2 ratelimit raw table structure. // This matches the ratelimits_raw_v2 table schema with additional // latency field compared to v1. -type RatelimitV2 struct { +type Ratelimit struct { RequestID string `ch:"request_id" json:"request_id"` Time int64 `ch:"time" json:"time"` WorkspaceID string `ch:"workspace_id" json:"workspace_id"` @@ -35,10 +35,10 @@ type RatelimitV2 struct { ResetAt int64 `ch:"reset_at" json:"reset_at"` } -// ApiRequestV2 represents the v2 API request raw table structure. +// ApiRequest represents the v2 API request raw table structure. // This matches the api_requests_raw_v2 table schema with query parameters // and region field compared to v1. -type ApiRequestV2 struct { +type ApiRequest struct { RequestID string `ch:"request_id" json:"request_id"` Time int64 `ch:"time" json:"time"` WorkspaceID string `ch:"workspace_id" json:"workspace_id"` diff --git a/go/pkg/zen/middleware_metrics.go b/go/pkg/zen/middleware_metrics.go index 315494c796..fc79ce8c1d 100644 --- a/go/pkg/zen/middleware_metrics.go +++ b/go/pkg/zen/middleware_metrics.go @@ -12,7 +12,7 @@ import ( ) type EventBuffer interface { - BufferApiRequest(schema.ApiRequestV2) + BufferApiRequest(schema.ApiRequest) } type redactionRule struct { @@ -84,7 +84,7 @@ func WithMetrics(eventBuffer EventBuffer, info InstanceInfo) Middleware { ipAddress = ips[0] } - eventBuffer.BufferApiRequest(schema.ApiRequestV2{ + eventBuffer.BufferApiRequest(schema.ApiRequest{ WorkspaceID: s.WorkspaceID, RequestID: s.RequestID(), Time: start.UnixMilli(), From 0d9862e448b1878686ab5af0bdd409ac9714e8fc Mon Sep 17 00:00:00 2001 From: chronark Date: Tue, 11 Nov 2025 19:01:34 +0100 Subject: [PATCH 09/10] chore: clean up merge mess --- go/apps/api/routes/register.go | 1 - go/pkg/clickhouse/noop.go | 1 - 2 files changed, 2 deletions(-) diff --git a/go/apps/api/routes/register.go b/go/apps/api/routes/register.go index 5f9f1fce9a..75298ad023 100644 --- a/go/apps/api/routes/register.go +++ b/go/apps/api/routes/register.go @@ -62,7 +62,6 @@ import ( // here we register all of the routes. // this function runs during startup. func Register(srv *zen.Server, svc *Services, info zen.InstanceInfo) { - withObservability := zen.WithObservability() withMetrics := zen.WithMetrics(svc.ClickHouse, info) withLogging := zen.WithLogging(svc.Logger) diff --git a/go/pkg/clickhouse/noop.go b/go/pkg/clickhouse/noop.go index a6650a58dc..8b647eb1cc 100644 --- a/go/pkg/clickhouse/noop.go +++ b/go/pkg/clickhouse/noop.go @@ -14,7 +14,6 @@ type noop struct{} var ( _ Bufferer = (*noop)(nil) - _ Bufferer = (*noop)(nil) ) func (n *noop) BufferApiRequest(schema.ApiRequest) { From ba64f97e36c5e6abb472923394c2d6e077393106 Mon Sep 17 00:00:00 2001 From: chronark Date: Wed, 12 Nov 2025 12:02:57 +0100 Subject: [PATCH 10/10] chore: remove profile --- cpu.prof | Bin 34602 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cpu.prof diff --git a/cpu.prof b/cpu.prof deleted file mode 100644 index d523c7014e126c12418bfe3285161be3a146744d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34602 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