@oneuptime/common 11.0.14 → 11.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +23 -0
- package/Models/AnalyticsModels/AuditLog.ts +2 -0
- package/Models/AnalyticsModels/ExceptionInstance.ts +5 -0
- package/Models/AnalyticsModels/Log.ts +10 -0
- package/Models/AnalyticsModels/Metric.ts +7 -0
- package/Models/AnalyticsModels/MetricBaselineHourly.ts +5 -0
- package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -0
- package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +2 -0
- package/Models/AnalyticsModels/MonitorLog.ts +2 -0
- package/Models/AnalyticsModels/Profile.ts +9 -0
- package/Models/AnalyticsModels/ProfileSample.ts +2 -0
- package/Models/AnalyticsModels/Span.ts +5 -0
- package/Server/EnvironmentConfig.ts +25 -0
- package/Server/Services/AnalyticsDatabaseService.ts +27 -14
- package/Server/Utils/AnalyticsDatabase/ClusterConfig.ts +177 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +101 -21
- package/Server/Utils/CodeRepository/CodeRepository.ts +8 -0
- package/Server/Utils/Execute.ts +9 -0
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +8 -3
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +296 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +5 -3
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +8 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/AuditLog.js +2 -0
- package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +5 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +10 -0
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +7 -0
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +5 -0
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -0
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +2 -0
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MonitorLog.js +2 -0
- package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Profile.js +9 -0
- package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ProfileSample.js +2 -0
- package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +5 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +21 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +22 -14
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js +144 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +68 -15
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/CodeRepository.js +2 -6
- package/build/dist/Server/Utils/CodeRepository/CodeRepository.js.map +1 -1
- package/build/dist/Server/Utils/Execute.js +3 -1
- package/build/dist/Server/Utils/Execute.js.map +1 -1
- package/package.json +1 -1
|
@@ -58,6 +58,20 @@ export default class AnalyticsBaseModel extends CommonModel {
|
|
|
58
58
|
enableWorkflowOn?: EnableWorkflowOn | undefined;
|
|
59
59
|
enableRealtimeEventsOn?: EnableRealtimeEventsOn | undefined;
|
|
60
60
|
partitionKey: string;
|
|
61
|
+
/*
|
|
62
|
+
* Sharding-key EXPRESSION for the Distributed table created in cluster
|
|
63
|
+
* mode (e.g. "cityHash64(traceId)"). Pick a HIGH-cardinality column so a
|
|
64
|
+
* single big project/tenant spreads evenly across shards, and co-locate
|
|
65
|
+
* what you read/aggregate together (a trace's spans, a metric series).
|
|
66
|
+
* Correctness never depends on this — the Distributed table fans reads out
|
|
67
|
+
* to every shard regardless — it is purely a distribution/locality choice.
|
|
68
|
+
* Falls back to cityHash64(projectId) when unset.
|
|
69
|
+
*
|
|
70
|
+
* MUST evaluate to a NON-nullable integer: ClickHouse rejects a Distributed
|
|
71
|
+
* sharding expression of type Nullable(...). Wrap nullable columns, e.g.
|
|
72
|
+
* cityHash64(ifNull(traceId, '')).
|
|
73
|
+
*/
|
|
74
|
+
shardingKey?: string | undefined;
|
|
61
75
|
tableSettings?: string | undefined;
|
|
62
76
|
projections?: Array<Projection> | undefined;
|
|
63
77
|
materializedViews?: Array<MaterializedView> | undefined;
|
|
@@ -172,6 +186,7 @@ export default class AnalyticsBaseModel extends CommonModel {
|
|
|
172
186
|
this.isMasterAdminApiDocs = data.isMasterAdminApiDocs || false;
|
|
173
187
|
this.enableRealtimeEventsOn = data.enableRealtimeEventsOn;
|
|
174
188
|
this.partitionKey = data.partitionKey;
|
|
189
|
+
this.shardingKey = data.shardingKey;
|
|
175
190
|
this.tableSettings = data.tableSettings;
|
|
176
191
|
this.projections = data.projections || [];
|
|
177
192
|
this.materializedViews = data.materializedViews || [];
|
|
@@ -250,6 +265,14 @@ export default class AnalyticsBaseModel extends CommonModel {
|
|
|
250
265
|
this._partitionKey = v;
|
|
251
266
|
}
|
|
252
267
|
|
|
268
|
+
private _shardingKey: string | undefined = undefined;
|
|
269
|
+
public get shardingKey(): string | undefined {
|
|
270
|
+
return this._shardingKey;
|
|
271
|
+
}
|
|
272
|
+
public set shardingKey(v: string | undefined) {
|
|
273
|
+
this._shardingKey = v;
|
|
274
|
+
}
|
|
275
|
+
|
|
253
276
|
private _tableSettings: string | undefined = undefined;
|
|
254
277
|
public get tableSettings(): string | undefined {
|
|
255
278
|
return this._tableSettings;
|
|
@@ -289,6 +289,8 @@ export default class AuditLog extends AnalyticsBaseModel {
|
|
|
289
289
|
sortKeys: ["projectId", "createdAt", "resourceType", "resourceId"],
|
|
290
290
|
primaryKeys: ["projectId", "createdAt"],
|
|
291
291
|
partitionKey: "toYYYYMM(createdAt)",
|
|
292
|
+
// Shard by the audited resource so its history co-locates; spreads across shards.
|
|
293
|
+
shardingKey: "cityHash64(projectId, resourceId)",
|
|
292
294
|
tableSettings:
|
|
293
295
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
294
296
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -802,6 +802,11 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
|
|
|
802
802
|
sortKeys: ["projectId", "time", "primaryEntityId", "fingerprint"],
|
|
803
803
|
primaryKeys: ["projectId", "time", "primaryEntityId", "fingerprint"],
|
|
804
804
|
partitionKey: "toYYYYMMDD(time)",
|
|
805
|
+
/*
|
|
806
|
+
* Shard by fingerprint so all occurrences of one exception co-locate and a
|
|
807
|
+
* big project's distinct exceptions spread across shards.
|
|
808
|
+
*/
|
|
809
|
+
shardingKey: "cityHash64(projectId, fingerprint)",
|
|
805
810
|
tableSettings:
|
|
806
811
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
807
812
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -697,6 +697,16 @@ export default class Log extends AnalyticsBaseModel {
|
|
|
697
697
|
sortKeys: ["projectId", "time", "primaryEntityId"],
|
|
698
698
|
primaryKeys: ["projectId", "time", "primaryEntityId"],
|
|
699
699
|
partitionKey: "toYYYYMMDD(time)",
|
|
700
|
+
/*
|
|
701
|
+
* Shard by (projectId, primaryEntityId, time). traceId is NOT used: it is
|
|
702
|
+
* Nullable on logs AND most logs have no trace, so a traceId hash would pile
|
|
703
|
+
* the majority onto one shard. These three columns are always present
|
|
704
|
+
* (non-nullable), and including the high-entropy `time` means even a single
|
|
705
|
+
* very-high-volume service spreads evenly across all shards — no hotspot.
|
|
706
|
+
* (Trace-scoped log reads scatter-gather, which is cheap via the traceId
|
|
707
|
+
* bloom-filter skip index and rare here.)
|
|
708
|
+
*/
|
|
709
|
+
shardingKey: "cityHash64(projectId, primaryEntityId, time)",
|
|
700
710
|
tableSettings:
|
|
701
711
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
702
712
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -1209,6 +1209,13 @@ export default class Metric extends AnalyticsBaseModel {
|
|
|
1209
1209
|
sortKeys: ["projectId", "name", "primaryEntityId", "time"],
|
|
1210
1210
|
primaryKeys: ["projectId", "name", "primaryEntityId", "time"],
|
|
1211
1211
|
partitionKey: "toYYYYMMDD(time)",
|
|
1212
|
+
/*
|
|
1213
|
+
* Shard by the metric series (projectId, name, primaryEntityId): spreads a
|
|
1214
|
+
* big project's many series across shards, and co-locates each series on
|
|
1215
|
+
* one shard so the minute/hour rollup MVs aggregate per-shard cleanly
|
|
1216
|
+
* (the MV GROUP BY starts with these columns).
|
|
1217
|
+
*/
|
|
1218
|
+
shardingKey: "cityHash64(projectId, name, primaryEntityId)",
|
|
1212
1219
|
tableSettings:
|
|
1213
1220
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
1214
1221
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -226,6 +226,11 @@ GROUP BY projectId, name, primaryEntityId, day, hourOfWeek`,
|
|
|
226
226
|
"day",
|
|
227
227
|
],
|
|
228
228
|
partitionKey: "toYYYYMM(day)",
|
|
229
|
+
/*
|
|
230
|
+
* Match the source Metric sharding (the series) so each series' baseline
|
|
231
|
+
* states stay on a single shard.
|
|
232
|
+
*/
|
|
233
|
+
shardingKey: "cityHash64(projectId, name, primaryEntityId)",
|
|
229
234
|
tableSettings:
|
|
230
235
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
231
236
|
ttlExpression: "day + INTERVAL 90 DAY",
|
|
@@ -164,6 +164,11 @@ GROUP BY projectId, name, primaryEntityId, bucketTime`,
|
|
|
164
164
|
sortKeys: ["projectId", "name", "primaryEntityId", "bucketTime"],
|
|
165
165
|
primaryKeys: ["projectId", "name", "primaryEntityId", "bucketTime"],
|
|
166
166
|
partitionKey: "toYYYYMM(bucketTime)",
|
|
167
|
+
/*
|
|
168
|
+
* Match the source Metric sharding (the series) so each series' rollup
|
|
169
|
+
* states stay on a single shard — no cross-shard partial-state merge.
|
|
170
|
+
*/
|
|
171
|
+
shardingKey: "cityHash64(projectId, name, primaryEntityId)",
|
|
167
172
|
tableSettings:
|
|
168
173
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
169
174
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -171,6 +171,8 @@ GROUP BY projectId, name, hostEntityKey, bucketTime`,
|
|
|
171
171
|
sortKeys: ["projectId", "name", "hostEntityKey", "bucketTime"],
|
|
172
172
|
primaryKeys: ["projectId", "name", "hostEntityKey", "bucketTime"],
|
|
173
173
|
partitionKey: "toYYYYMM(bucketTime)",
|
|
174
|
+
// Align with this MV's GROUP BY (projectId, name, hostEntityKey).
|
|
175
|
+
shardingKey: "cityHash64(projectId, name, hostEntityKey)",
|
|
174
176
|
tableSettings:
|
|
175
177
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
176
178
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -194,6 +194,8 @@ export default class MonitorLog extends AnalyticsBaseModel {
|
|
|
194
194
|
sortKeys: ["projectId", "time", "monitorId"],
|
|
195
195
|
primaryKeys: ["projectId", "time", "monitorId"],
|
|
196
196
|
partitionKey: "toYYYYMMDD(time)",
|
|
197
|
+
// Shard by monitorId so a monitor's logs co-locate; spreads across shards.
|
|
198
|
+
shardingKey: "cityHash64(monitorId)",
|
|
197
199
|
tableSettings:
|
|
198
200
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
199
201
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -809,6 +809,15 @@ export default class Profile extends AnalyticsBaseModel {
|
|
|
809
809
|
sortKeys: ["projectId", "startTime", "primaryEntityId", "profileType"],
|
|
810
810
|
primaryKeys: ["projectId", "startTime", "primaryEntityId", "profileType"],
|
|
811
811
|
partitionKey: "toYYYYMMDD(startTime)",
|
|
812
|
+
/*
|
|
813
|
+
* Shard by (projectId, primaryEntityId, startTime). All three are
|
|
814
|
+
* non-nullable. Without startTime, a single heavily-profiled service
|
|
815
|
+
* would pile all its profiles onto one shard; the high-entropy startTime
|
|
816
|
+
* spreads them evenly (same reasoning as Log). A profile's samples live in
|
|
817
|
+
* ProfileSample (sharded by profileId), so co-locating Profile rows by
|
|
818
|
+
* service buys little — even spread wins here.
|
|
819
|
+
*/
|
|
820
|
+
shardingKey: "cityHash64(projectId, primaryEntityId, startTime)",
|
|
812
821
|
tableSettings:
|
|
813
822
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
814
823
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -665,6 +665,8 @@ export default class ProfileSample extends AnalyticsBaseModel {
|
|
|
665
665
|
"stacktraceHash",
|
|
666
666
|
],
|
|
667
667
|
partitionKey: "toYYYYMMDD(time)",
|
|
668
|
+
// Shard by profileId so all samples of a profile co-locate on one shard.
|
|
669
|
+
shardingKey: "cityHash64(profileId)",
|
|
668
670
|
tableSettings:
|
|
669
671
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
670
672
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -996,6 +996,11 @@ export default class Span extends AnalyticsBaseModel {
|
|
|
996
996
|
sortKeys: ["projectId", "startTime", "primaryEntityId", "traceId"],
|
|
997
997
|
primaryKeys: ["projectId", "startTime", "primaryEntityId", "traceId"],
|
|
998
998
|
partitionKey: "toYYYYMMDD(startTime)",
|
|
999
|
+
/*
|
|
1000
|
+
* Shard by traceId: high-cardinality so a big project spreads evenly, and
|
|
1001
|
+
* it keeps every span of a trace on one shard for a fast single-trace view.
|
|
1002
|
+
*/
|
|
1003
|
+
shardingKey: "cityHash64(traceId)",
|
|
999
1004
|
tableSettings:
|
|
1000
1005
|
"ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
|
|
1001
1006
|
ttlExpression: "retentionDate DELETE",
|
|
@@ -426,6 +426,31 @@ export const MaxClickhouseIngestConnections: number = parseInt(
|
|
|
426
426
|
10,
|
|
427
427
|
);
|
|
428
428
|
|
|
429
|
+
/*
|
|
430
|
+
* Cluster name. The analytics schema ALWAYS runs as a sharded + replicated
|
|
431
|
+
* cluster (Distributed tables over local ReplicatedMergeTree, `ON CLUSTER
|
|
432
|
+
* '<name>'`); a single node is just a 1-shard/1-replica cluster backed by an
|
|
433
|
+
* embedded Keeper. The name must match the cluster defined in the ClickHouse
|
|
434
|
+
* config / ClickHouseInstallation; it defaults to 'oneuptime' (what the bundled
|
|
435
|
+
* StatefulSet config and the Altinity operator both create).
|
|
436
|
+
*
|
|
437
|
+
* NOTE: the live, test-toggleable readers live in
|
|
438
|
+
* Common/Server/Utils/AnalyticsDatabase/ClusterConfig.ts (which reads
|
|
439
|
+
* process.env directly). These consts mirror the same keys/defaults and exist
|
|
440
|
+
* so the env surface is discoverable here alongside the other CLICKHOUSE_* vars.
|
|
441
|
+
*/
|
|
442
|
+
export const ClickhouseClusterName: string =
|
|
443
|
+
process.env["CLICKHOUSE_CLUSTER_NAME"] || "oneuptime";
|
|
444
|
+
|
|
445
|
+
/*
|
|
446
|
+
* Optional GLOBAL override of the Distributed sharding-key expression. Empty by
|
|
447
|
+
* default, which means each model's own `shardingKey` is used (e.g.
|
|
448
|
+
* cityHash64(traceId) for spans, the series tuple for metrics). Set this to
|
|
449
|
+
* force one expression across all tables.
|
|
450
|
+
*/
|
|
451
|
+
export const ClickhouseShardingKeyOverride: string =
|
|
452
|
+
process.env["CLICKHOUSE_SHARDING_KEY"] || "";
|
|
453
|
+
|
|
429
454
|
export const GitSha: string = process.env["GIT_SHA"] || "unknown";
|
|
430
455
|
|
|
431
456
|
export const AppVersion: string = process.env["APP_VERSION"] || "unknown";
|
|
@@ -29,6 +29,10 @@ import UpdateBy from "../Types/AnalyticsDatabase/UpdateBy";
|
|
|
29
29
|
import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
|
|
30
30
|
import StatementGenerator from "../Utils/AnalyticsDatabase/StatementGenerator";
|
|
31
31
|
import { getQuerySettings } from "../Utils/AnalyticsDatabase/QuerySettingsHelper";
|
|
32
|
+
import {
|
|
33
|
+
getStorageTableName,
|
|
34
|
+
onClusterClause,
|
|
35
|
+
} from "../Utils/AnalyticsDatabase/ClusterConfig";
|
|
32
36
|
import logger, { LogAttributes } from "../Utils/Logger";
|
|
33
37
|
import Realtime from "../Utils/Realtime";
|
|
34
38
|
import StreamUtil from "../Utils/Stream";
|
|
@@ -472,7 +476,8 @@ export default class AnalyticsDatabaseService<
|
|
|
472
476
|
}
|
|
473
477
|
|
|
474
478
|
public async doesColumnExist(columnName: string): Promise<boolean> {
|
|
475
|
-
|
|
479
|
+
// Columns live on the physical (local) storage table in cluster mode.
|
|
480
|
+
const tableName: string = getStorageTableName(this.model.tableName);
|
|
476
481
|
const result: { data: Array<JSONObject> } = await (
|
|
477
482
|
await this.executeQuery(
|
|
478
483
|
`SELECT count() as cnt FROM system.columns WHERE database = currentDatabase() AND table = '${tableName}' AND name = '${columnName}'`,
|
|
@@ -485,7 +490,7 @@ export default class AnalyticsDatabaseService<
|
|
|
485
490
|
}
|
|
486
491
|
|
|
487
492
|
public async getColumnCodec(columnName: string): Promise<string> {
|
|
488
|
-
const tableName: string = this.model.tableName;
|
|
493
|
+
const tableName: string = getStorageTableName(this.model.tableName);
|
|
489
494
|
const result: { data: Array<JSONObject> } = await (
|
|
490
495
|
await this.executeQuery(
|
|
491
496
|
`SELECT compression_codec FROM system.columns WHERE database = currentDatabase() AND table = '${tableName}' AND name = '${columnName}'`,
|
|
@@ -508,7 +513,7 @@ export default class AnalyticsDatabaseService<
|
|
|
508
513
|
* re-state a column's type in a MODIFY COLUMN without guessing it.
|
|
509
514
|
*/
|
|
510
515
|
public async getColumnDatabaseType(columnName: string): Promise<string> {
|
|
511
|
-
const tableName: string = this.model.tableName;
|
|
516
|
+
const tableName: string = getStorageTableName(this.model.tableName);
|
|
512
517
|
const result: { data: Array<JSONObject> } = await (
|
|
513
518
|
await this.executeQuery(
|
|
514
519
|
`SELECT type FROM system.columns WHERE database = currentDatabase() AND table = '${tableName}' AND name = '${columnName}'`,
|
|
@@ -530,7 +535,11 @@ export default class AnalyticsDatabaseService<
|
|
|
530
535
|
codec: string;
|
|
531
536
|
expectedCodecValue: string;
|
|
532
537
|
}): Promise<void> {
|
|
533
|
-
|
|
538
|
+
/*
|
|
539
|
+
* MODIFY COLUMN on the local table; ReplicatedMergeTree fans the codec
|
|
540
|
+
* change out to the other replicas through Keeper.
|
|
541
|
+
*/
|
|
542
|
+
const tableName: string = getStorageTableName(this.model.tableName);
|
|
534
543
|
const currentCodec: string = await this.getColumnCodec(data.columnName);
|
|
535
544
|
|
|
536
545
|
if (currentCodec === data.expectedCodecValue) {
|
|
@@ -1294,19 +1303,23 @@ export default class AnalyticsDatabaseService<
|
|
|
1294
1303
|
);
|
|
1295
1304
|
|
|
1296
1305
|
/*
|
|
1297
|
-
*
|
|
1298
|
-
* `
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1303
|
-
* and are reconciled during normal merges, so they don't accumulate
|
|
1304
|
-
* in the mutations queue.
|
|
1306
|
+
* Lightweight `DELETE FROM` cannot target a Distributed table and does not
|
|
1307
|
+
* accept `ON CLUSTER`, so deletes are an `ALTER TABLE <local> ON CLUSTER …
|
|
1308
|
+
* DELETE` mutation dispatched to every shard (and replicated within each
|
|
1309
|
+
* shard via Keeper). ALTER ... DELETE mutations are bounded by
|
|
1310
|
+
* `number_of_mutations_to_throw` (default 1000) — but deletes are rare here
|
|
1311
|
+
* (retention is handled by TTL), so the queue does not accumulate.
|
|
1305
1312
|
*/
|
|
1306
1313
|
/* eslint-disable prettier/prettier */
|
|
1314
|
+
const localTableName: string = getStorageTableName(this.model.tableName);
|
|
1307
1315
|
const statement: Statement = SQL`
|
|
1308
|
-
|
|
1309
|
-
|
|
1316
|
+
ALTER TABLE ${databaseName}.${localTableName}`
|
|
1317
|
+
.append(onClusterClause())
|
|
1318
|
+
.append(
|
|
1319
|
+
SQL`
|
|
1320
|
+
DELETE WHERE TRUE `,
|
|
1321
|
+
)
|
|
1322
|
+
.append(whereStatement);
|
|
1310
1323
|
|
|
1311
1324
|
logger.debug(`${this.model.tableName} Delete Statement`, {
|
|
1312
1325
|
tableName: this.model.tableName,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import AnalyticsTableEngine from "../../../Types/AnalyticsDatabase/AnalyticsTableEngine";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* ClickHouse cluster helpers.
|
|
5
|
+
*
|
|
6
|
+
* OneUptime's analytics schema runs as a sharded + replicated cluster on EVERY
|
|
7
|
+
* deployment — there is no separate single-node code path. A single node is just
|
|
8
|
+
* a "cluster of one": a 1-shard / 1-replica cluster backed by an (embedded)
|
|
9
|
+
* Keeper. For each model:
|
|
10
|
+
*
|
|
11
|
+
* - the model's `tableName` is a `Distributed` table the app reads from and
|
|
12
|
+
* writes to (scatter-gather on read, shard-routing on write);
|
|
13
|
+
* - the data lives in a local `<tableName>Local` table whose engine is the
|
|
14
|
+
* `Replicated*` variant of the model engine, so each shard's replicas hold a
|
|
15
|
+
* consistent copy of that shard's data (coordinated through Keeper).
|
|
16
|
+
*
|
|
17
|
+
* Because there is no dual mode, these helpers are unconditional: the storage
|
|
18
|
+
* table is always `<tableName>Local`, the engine is always `Replicated*`, and
|
|
19
|
+
* every object-lifecycle DDL statement carries `ON CLUSTER`.
|
|
20
|
+
*
|
|
21
|
+
* The cluster NAME is read live from process.env (CLICKHOUSE_CLUSTER_NAME,
|
|
22
|
+
* default "oneuptime") so it can be pointed at a differently-named external
|
|
23
|
+
* cluster, and so unit tests can vary it. It must match the cluster defined in
|
|
24
|
+
* the ClickHouse config / ClickHouseInstallation.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_CLICKHOUSE_CLUSTER_NAME: string = "oneuptime";
|
|
28
|
+
export const DEFAULT_CLICKHOUSE_SHARDING_KEY: string = "cityHash64(projectId)";
|
|
29
|
+
export const DEFAULT_CLICKHOUSE_DATABASE: string = "oneuptime";
|
|
30
|
+
|
|
31
|
+
// Suffix appended to a model's tableName to name its local (data) table.
|
|
32
|
+
export const LOCAL_TABLE_SUFFIX: string = "Local";
|
|
33
|
+
|
|
34
|
+
export function getClickhouseClusterName(): string {
|
|
35
|
+
const name: string = (process.env["CLICKHOUSE_CLUSTER_NAME"] || "").trim();
|
|
36
|
+
return name.length > 0 ? name : DEFAULT_CLICKHOUSE_CLUSTER_NAME;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* Global sharding-key OVERRIDE (CLICKHOUSE_SHARDING_KEY). Empty by default,
|
|
41
|
+
* which means each model's own `shardingKey` is used (see getDistributedEngine).
|
|
42
|
+
* Set it to force one expression across all tables.
|
|
43
|
+
*/
|
|
44
|
+
export function getClickhouseShardingKeyOverride(): string {
|
|
45
|
+
return (process.env["CLICKHOUSE_SHARDING_KEY"] || "").trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getClickhouseDatabaseName(): string {
|
|
49
|
+
return process.env["CLICKHOUSE_DATABASE"] || DEFAULT_CLICKHOUSE_DATABASE;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* The `ON CLUSTER '<name>'` clause (with a leading space), emitted on every
|
|
54
|
+
* object-lifecycle DDL statement (CREATE TABLE / CREATE MATERIALIZED VIEW /
|
|
55
|
+
* DROP / RENAME) so the object is created or dropped on every node of the
|
|
56
|
+
* cluster. Per-table ALTERs and data mutations on ReplicatedMergeTree propagate
|
|
57
|
+
* through Keeper automatically; emitting ON CLUSTER on them too is harmless and
|
|
58
|
+
* keeps the reconcilers deterministic across replicas.
|
|
59
|
+
*/
|
|
60
|
+
export function onClusterClause(): string {
|
|
61
|
+
return ` ON CLUSTER '${getClickhouseClusterName()}'`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* The physical table that stores a model's rows: `<tableName>Local` (a
|
|
66
|
+
* Replicated* table). All schema DDL (columns / indexes / projections) and data
|
|
67
|
+
* mutations (ALTER ... DELETE/UPDATE) target this name; the app-facing
|
|
68
|
+
* `tableName` is the Distributed wrapper.
|
|
69
|
+
*/
|
|
70
|
+
export function getStorageTableName(tableName: string): string {
|
|
71
|
+
return `${tableName}${LOCAL_TABLE_SUFFIX}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* Map a logical model engine to the `Replicated*` engine string for the local
|
|
76
|
+
* storage table, written WITHOUT explicit Keeper-path arguments so it relies on
|
|
77
|
+
* the server's `default_replica_path` / `default_replica_name` macros (the
|
|
78
|
+
* Altinity operator and the bundled embedded-Keeper config both provision these
|
|
79
|
+
* as `/clickhouse/tables/{uuid}/{shard}` and `{replica}`).
|
|
80
|
+
*/
|
|
81
|
+
export function getStorageEngine(engine: AnalyticsTableEngine): string {
|
|
82
|
+
switch (engine) {
|
|
83
|
+
case AnalyticsTableEngine.AggregatingMergeTree:
|
|
84
|
+
return "ReplicatedAggregatingMergeTree";
|
|
85
|
+
case AnalyticsTableEngine.MergeTree:
|
|
86
|
+
default:
|
|
87
|
+
return "ReplicatedMergeTree";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
* The `Distributed(...)` engine string for the app-facing table wrapping a local
|
|
93
|
+
* storage table. The sharding key resolves as: global override
|
|
94
|
+
* (CLICKHOUSE_SHARDING_KEY) > the model's own shardingKey > cityHash64(projectId).
|
|
95
|
+
* internal_replication is configured on the cluster definition (true) so the
|
|
96
|
+
* Distributed table writes each row to one replica per shard and lets
|
|
97
|
+
* ReplicatedMergeTree fan it out.
|
|
98
|
+
*/
|
|
99
|
+
export function getDistributedEngine(
|
|
100
|
+
localTableName: string,
|
|
101
|
+
modelShardingKey?: string | undefined,
|
|
102
|
+
): string {
|
|
103
|
+
const cluster: string = getClickhouseClusterName();
|
|
104
|
+
const database: string = getClickhouseDatabaseName();
|
|
105
|
+
const shardingKey: string =
|
|
106
|
+
getClickhouseShardingKeyOverride() ||
|
|
107
|
+
(modelShardingKey && modelShardingKey.trim().length > 0
|
|
108
|
+
? modelShardingKey.trim()
|
|
109
|
+
: DEFAULT_CLICKHOUSE_SHARDING_KEY);
|
|
110
|
+
return `Distributed('${cluster}', ${database}, ${localTableName}, ${shardingKey})`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* ReplicatedMergeTree deduplicates inserts via `replicated_deduplication_window`
|
|
115
|
+
* (coordinated through Keeper), whereas plain MergeTree uses
|
|
116
|
+
* `non_replicated_deduplication_window`. OneUptime's models declare the latter
|
|
117
|
+
* in their tableSettings; rewrite it for the replicated local tables so insert
|
|
118
|
+
* idempotency (retried telemetry batches keyed by a dedup token) is preserved.
|
|
119
|
+
*/
|
|
120
|
+
export function adaptTableSettingsForStorage(
|
|
121
|
+
tableSettings: string | undefined,
|
|
122
|
+
): string | undefined {
|
|
123
|
+
if (!tableSettings) {
|
|
124
|
+
return tableSettings;
|
|
125
|
+
}
|
|
126
|
+
return tableSettings.replace(
|
|
127
|
+
/non_replicated_deduplication_window/g,
|
|
128
|
+
"replicated_deduplication_window",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* Rewrite a model's canonical `CREATE MATERIALIZED VIEW … TO <target> AS SELECT
|
|
134
|
+
* … FROM <source> …` statement for the cluster:
|
|
135
|
+
*
|
|
136
|
+
* 1. inject `ON CLUSTER '<name>'` after the view name, so the trigger exists
|
|
137
|
+
* on every node;
|
|
138
|
+
* 2. point `TO <target>` at the LOCAL target table (`<target>Local`), so each
|
|
139
|
+
* shard aggregates into its own replicated table;
|
|
140
|
+
* 3. point `FROM <source>` at the LOCAL source table (`<source>Local`), so the
|
|
141
|
+
* MV fires per-shard on local inserts rather than on the Distributed table.
|
|
142
|
+
*
|
|
143
|
+
* The Distributed wrapper over the target (created by the agg model's own
|
|
144
|
+
* CREATE) then scatter-gathers reads across shards.
|
|
145
|
+
*
|
|
146
|
+
* The replacements are deliberately precise: only the FIRST `TO`/`FROM` clause
|
|
147
|
+
* (the view's target/source) is rewritten, matched on the uppercase keyword our
|
|
148
|
+
* canonical definitions use — so `toStartOfMinute(...)` and aggregate columns
|
|
149
|
+
* are never touched. OneUptime MV definitions are single-source with no JOINs or
|
|
150
|
+
* subqueries, which this relies on.
|
|
151
|
+
*/
|
|
152
|
+
export function applyClusterToMaterializedViewQuery(query: string): string {
|
|
153
|
+
const cluster: string = getClickhouseClusterName();
|
|
154
|
+
|
|
155
|
+
let result: string = query.replace(
|
|
156
|
+
/^(\s*CREATE\s+MATERIALIZED\s+VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?[A-Za-z_][A-Za-z0-9_]*)/,
|
|
157
|
+
(match: string): string => {
|
|
158
|
+
return `${match} ON CLUSTER '${cluster}'`;
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
result = result.replace(
|
|
163
|
+
/(\bTO\s+)([A-Za-z_][A-Za-z0-9_]*)/,
|
|
164
|
+
(_match: string, lead: string, table: string): string => {
|
|
165
|
+
return `${lead}${getStorageTableName(table)}`;
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
result = result.replace(
|
|
170
|
+
/(\bFROM\s+)([A-Za-z_][A-Za-z0-9_]*)/,
|
|
171
|
+
(_match: string, lead: string, table: string): string => {
|
|
172
|
+
return `${lead}${getStorageTableName(table)}`;
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return result;
|
|
177
|
+
}
|