-
Notifications
You must be signed in to change notification settings - Fork 498
clickhouse setup #1032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
clickhouse setup #1032
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
0c08896
clickhouse setup
BilalG1 41287db
fix clickhouse env vars
BilalG1 1320221
fix docker server test
BilalG1 dfb0245
fix default access management for docker test
BilalG1 0077d16
Merge branch 'dev' into clickhouse-setup
BilalG1 62c6646
merge dev
BilalG1 90cc6a9
remove unused table
BilalG1 c999d16
Merge branch 'dev' into clickhouse-setup
BilalG1 20b597f
Merge branch 'dev' into clickhouse-setup
BilalG1 73407b3
merge dev
BilalG1 688c9ce
small fixes
BilalG1 f6fb8cf
Merge remote-tracking branch 'origin/dev' into clickhouse-setup
BilalG1 44a3496
fix lint
BilalG1 63da706
Clickhouse events (#1038)
BilalG1 7a4bab9
Merge branch 'dev' into clickhouse-setup
BilalG1 39f94c9
fix frontend build
BilalG1 beae857
fix lint
BilalG1 8c030db
add query analytics page
BilalG1 e622514
stricter user in clickhouse migration
BilalG1 fae5166
modify settings test, clickhouse fixes
BilalG1 49c8d0f
various changes
N2D4 1134883
todos
N2D4 30a8212
clickhouse error code parsing
BilalG1 8e72a7b
merge dev
BilalG1 ba8110f
fix tests
BilalG1 136b25f
small fixes, use view for ch events
BilalG1 3760836
fix
BilalG1 3ff2f4f
merge
BilalG1 42bfed5
fix build
BilalG1 b9d72b0
small fix
BilalG1 9f501c8
Merge remote-tracking branch 'origin/dev' into clickhouse-setup
BilalG1 39099af
merge dev
BilalG1 176775d
merge dev
BilalG1 aa2a5dc
Merge branch 'dev' into clickhouse-setup
BilalG1 ab30ea3
Merge branch 'dev' into clickhouse-setup
BilalG1 14c70ef
remove analyzer
BilalG1 4f1ded7
Merge branch 'clickhouse-setup' of https://github.com/stack-auth/stac…
BilalG1 55fa597
pnpm lock file
BilalG1 bf0675f
change wal info port
BilalG1 54bbcf9
fix wall info port
BilalG1 e7b4bfc
Merge branch 'dev' into clickhouse-setup
BilalG1 1d8e9ec
Merge branch 'dev' into clickhouse-setup
BilalG1 360176f
Merge branch 'dev' into clickhouse-setup
BilalG1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { getClickhouseAdminClient } from "@/lib/clickhouse"; | ||
| import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; | ||
|
|
||
| export async function runClickhouseMigrations() { | ||
| console.log("[Clickhouse] Running Clickhouse migrations..."); | ||
| const client = getClickhouseAdminClient(); | ||
| const clickhouseExternalPassword = getEnvVariable("STACK_CLICKHOUSE_EXTERNAL_PASSWORD"); | ||
| await client.exec({ | ||
| query: "CREATE USER IF NOT EXISTS limited_user IDENTIFIED WITH sha256_password BY {clickhouseExternalPassword:String}", | ||
| query_params: { clickhouseExternalPassword }, | ||
| }); | ||
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // todo: create migration files | ||
| await client.exec({ query: EXTERNAL_ANALYTICS_DB_SQL }); | ||
| await client.exec({ query: EVENTS_TABLE_BASE_SQL }); | ||
| await client.exec({ query: EVENTS_VIEW_SQL }); | ||
| const queries = [ | ||
| "REVOKE ALL PRIVILEGES ON *.* FROM limited_user;", | ||
| "REVOKE ALL FROM limited_user;", | ||
| "GRANT SELECT ON default.events TO limited_user;", | ||
| ]; | ||
| await client.exec({ | ||
| query: "CREATE ROW POLICY IF NOT EXISTS events_project_isolation ON default.events FOR SELECT USING project_id = getSetting('SQL_project_id') AND branch_id = getSetting('SQL_branch_id') TO limited_user", | ||
| }); | ||
| for (const query of queries) { | ||
| await client.exec({ query }); | ||
| } | ||
| console.log("[Clickhouse] Clickhouse migrations complete"); | ||
| await client.close(); | ||
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| const EVENTS_TABLE_BASE_SQL = ` | ||
| CREATE TABLE IF NOT EXISTS analytics_internal.events ( | ||
| event_type LowCardinality(String), | ||
| event_at DateTime64(3, 'UTC'), | ||
| data JSON, | ||
| project_id String, | ||
| branch_id String, | ||
| user_id String, | ||
| team_id String, | ||
| refresh_token_id String, | ||
| is_anonymous Boolean, | ||
| session_id String, | ||
| ip_address String, | ||
| created_at DateTime64(3, 'UTC') DEFAULT now64(3) | ||
| ) | ||
| ENGINE MergeTree | ||
| PARTITION BY toYYYYMM(event_at) | ||
| ORDER BY (project_id, branch_id, event_at); | ||
| `; | ||
|
|
||
| const EVENTS_VIEW_SQL = ` | ||
| CREATE OR REPLACE VIEW default.events | ||
| SQL SECURITY DEFINER | ||
| AS | ||
| SELECT * | ||
| FROM analytics_internal.events; | ||
| `; | ||
|
|
||
| const EXTERNAL_ANALYTICS_DB_SQL = ` | ||
| CREATE DATABASE IF NOT EXISTS analytics_internal; | ||
| `; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
apps/backend/src/app/api/latest/internal/analytics/query/route.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import { getClickhouseExternalClient, getQueryTimingStats, isClickhouseConfigured } from "@/lib/clickhouse"; | ||
| import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; | ||
| import { KnownErrors } from "@stackframe/stack-shared"; | ||
| import { adaptSchema, adminAuthTypeSchema, jsonSchema, yupBoolean, yupMixed, yupNumber, yupObject, yupRecord, yupString } from "@stackframe/stack-shared/dist/schema-fields"; | ||
| import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; | ||
| import { Result } from "@stackframe/stack-shared/dist/utils/results"; | ||
| import { randomUUID } from "crypto"; | ||
|
|
||
| export const POST = createSmartRouteHandler({ | ||
| metadata: { hidden: true }, | ||
| request: yupObject({ | ||
| auth: yupObject({ | ||
| type: adminAuthTypeSchema, | ||
| tenancy: adaptSchema, | ||
| }).defined(), | ||
| body: yupObject({ | ||
| include_all_branches: yupBoolean().default(false), | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| query: yupString().defined().nonEmpty(), | ||
| params: yupRecord(yupString().defined(), yupMixed().defined()).default({}), | ||
| timeout_ms: yupNumber().integer().min(1_000).default(10_000), | ||
| }).defined(), | ||
| }), | ||
| response: yupObject({ | ||
| statusCode: yupNumber().oneOf([200]).defined(), | ||
| bodyType: yupString().oneOf(["json"]).defined(), | ||
| body: yupObject({ | ||
| result: jsonSchema.defined(), | ||
| stats: yupObject({ | ||
| cpu_time: yupNumber().defined(), | ||
| wall_clock_time: yupNumber().defined(), | ||
| }).defined(), | ||
| }).defined(), | ||
| }), | ||
| async handler({ body, auth }) { | ||
| if (body.include_all_branches) { | ||
| throw new StackAssertionError("include_all_branches is not supported yet"); | ||
| } | ||
| if (!isClickhouseConfigured()) { | ||
| throw new StackAssertionError("ClickHouse is not configured"); | ||
| } | ||
| const client = getClickhouseExternalClient(); | ||
| const queryId = randomUUID(); | ||
| const resultSet = await Result.fromPromise(client.query({ | ||
| query: body.query, | ||
| query_id: queryId, | ||
| query_params: body.params, | ||
| clickhouse_settings: { | ||
| SQL_project_id: auth.tenancy.project.id, | ||
| SQL_branch_id: auth.tenancy.branchId, | ||
| max_execution_time: body.timeout_ms / 1000, | ||
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| readonly: "1", | ||
| allow_ddl: 0, | ||
| max_result_rows: MAX_RESULT_ROWS.toString(), | ||
| max_result_bytes: MAX_RESULT_BYTES.toString(), | ||
| result_overflow_mode: "throw", | ||
| }, | ||
| format: "JSONEachRow", | ||
| })); | ||
|
|
||
| if (resultSet.status === "error") { | ||
| const message = getSafeClickhouseErrorMessage(resultSet.error); | ||
| if (message === null) { | ||
| throw new StackAssertionError("Unknown Clickhouse error", { cause: resultSet.error }); | ||
| } | ||
| throw new KnownErrors.AnalyticsQueryError(message); | ||
| } | ||
|
|
||
| const rows = await resultSet.data.json<Record<string, unknown>[]>(); | ||
| const stats = await getQueryTimingStats(client, queryId); | ||
N2D4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return { | ||
| statusCode: 200, | ||
| bodyType: "json", | ||
| body: { | ||
| result: rows, | ||
| stats: { | ||
| cpu_time: stats.cpu_time_ms, | ||
| wall_clock_time: stats.wall_clock_time_ms, | ||
| }, | ||
| }, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| const SAFE_CLICKHOUSE_ERROR_CODES = [ | ||
| 62, // SYNTAX_ERROR | ||
| 159, // TIMEOUT_EXCEEDED | ||
| 164, // READONLY | ||
| 158, // TOO_MANY_ROWS | ||
| 396, // TOO_MANY_ROWS_OR_BYTES | ||
| 636, // CANNOT_EXTRACT_TABLE_STRUCTURE | ||
| ]; | ||
|
|
||
| const UNSAFE_CLICKHOUSE_ERROR_CODES = [ | ||
| 36, // BAD_ARGUMENTS | ||
| 60, // UNKNOWN_TABLE | ||
| 497, // ACCESS_DENIED | ||
| ]; | ||
|
|
||
| const DEFAULT_CLICKHOUSE_ERROR_MESSAGE = "Error during execution of this query."; | ||
| const MAX_RESULT_ROWS = 10_000; | ||
| const MAX_RESULT_BYTES = 10 * 1024 * 1024; | ||
|
|
||
| function getSafeClickhouseErrorMessage(error: unknown): string | null { | ||
| if (typeof error !== "object" || error === null || !("code" in error) || typeof error.code !== "string") { | ||
| return null; | ||
| } | ||
| const errorCode = Number(error.code); | ||
| if (isNaN(errorCode)) { | ||
| return null; | ||
| } | ||
| const message = "message" in error && typeof error.message === "string" ? error.message : null; | ||
| if (SAFE_CLICKHOUSE_ERROR_CODES.includes(errorCode)) { | ||
| return message; | ||
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| if (UNSAFE_CLICKHOUSE_ERROR_CODES.includes(errorCode)) { | ||
| return DEFAULT_CLICKHOUSE_ERROR_MESSAGE; | ||
| } | ||
| return null; | ||
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.