@oneuptime/common 10.5.37 → 10.7.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.
Files changed (49) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
  2. package/Models/AnalyticsModels/Log.ts +4 -0
  3. package/Models/AnalyticsModels/Metric.ts +27 -0
  4. package/Models/AnalyticsModels/Profile.ts +1 -0
  5. package/Models/AnalyticsModels/ProfileSample.ts +1 -0
  6. package/Models/AnalyticsModels/Span.ts +2 -0
  7. package/Server/Infrastructure/ClickhouseConfig.ts +15 -0
  8. package/Server/Services/AnalyticsDatabaseService.ts +112 -0
  9. package/Server/Services/IncidentService.ts +2 -4
  10. package/Server/Services/LogAggregationService.ts +49 -0
  11. package/Server/Services/ProfileAggregationService.ts +25 -0
  12. package/Server/Services/TelemetryAttributeService.ts +20 -0
  13. package/Server/Types/AnalyticsDatabase/ExistsBy.ts +8 -0
  14. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +55 -30
  15. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +52 -0
  16. package/Types/AnalyticsDatabase/TableColumn.ts +49 -6
  17. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
  18. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  19. package/build/dist/Models/AnalyticsModels/Log.js +4 -0
  20. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  21. package/build/dist/Models/AnalyticsModels/Metric.js +27 -0
  22. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  23. package/build/dist/Models/AnalyticsModels/Profile.js +1 -0
  24. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  25. package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -0
  26. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  27. package/build/dist/Models/AnalyticsModels/Span.js +2 -0
  28. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  29. package/build/dist/Server/Infrastructure/ClickhouseConfig.js +15 -0
  30. package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
  31. package/build/dist/Server/Services/AnalyticsDatabaseService.js +82 -0
  32. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  33. package/build/dist/Server/Services/IncidentService.js +2 -4
  34. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  35. package/build/dist/Server/Services/LogAggregationService.js +31 -0
  36. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  37. package/build/dist/Server/Services/ProfileAggregationService.js +16 -0
  38. package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
  39. package/build/dist/Server/Services/TelemetryAttributeService.js +14 -0
  40. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  41. package/build/dist/Server/Types/AnalyticsDatabase/ExistsBy.js +2 -0
  42. package/build/dist/Server/Types/AnalyticsDatabase/ExistsBy.js.map +1 -0
  43. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +41 -22
  44. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  45. package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js +42 -0
  46. package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js.map +1 -1
  47. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +16 -0
  48. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  49. package/package.json +1 -1
@@ -80,6 +80,7 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
80
80
 
81
81
  const serviceTypeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
82
82
  key: "serviceType",
83
+ isLowCardinality: true,
83
84
  title: "Service Type",
84
85
  description:
85
86
  "Discriminator for serviceId — tells the read side which resource table to dispatch to",
@@ -81,6 +81,7 @@ export default class Log extends AnalyticsBaseModel {
81
81
 
82
82
  const serviceTypeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
83
83
  key: "serviceType",
84
+ isLowCardinality: true,
84
85
  title: "Service Type",
85
86
  description:
86
87
  "Discriminator for serviceId — tells the read side which resource table to dispatch to",
@@ -175,6 +176,7 @@ export default class Log extends AnalyticsBaseModel {
175
176
 
176
177
  const severityTextColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
177
178
  key: "severityText",
179
+ isLowCardinality: true,
178
180
  title: "Severity Text",
179
181
  description: "Log Severity Text",
180
182
  required: true,
@@ -241,6 +243,7 @@ export default class Log extends AnalyticsBaseModel {
241
243
 
242
244
  const attributesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
243
245
  key: "attributes",
246
+ codec: { codec: "ZSTD", level: 3 },
244
247
  title: "Attributes",
245
248
  description: "Attributes",
246
249
  required: true,
@@ -271,6 +274,7 @@ export default class Log extends AnalyticsBaseModel {
271
274
 
272
275
  const attributeKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
273
276
  key: "attributeKeys",
277
+ codec: { codec: "ZSTD", level: 3 },
274
278
  title: "Attribute Keys",
275
279
  description: "Attribute keys extracted from attributes",
276
280
  required: true,
@@ -94,6 +94,7 @@ export default class Metric extends AnalyticsBaseModel {
94
94
  // this can also be the monitor id or the telemetry service id.
95
95
  const serviceTypeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
96
96
  key: "serviceType",
97
+ isLowCardinality: true,
97
98
  title: "Service Type",
98
99
  description: "Type of the service that this telemetry belongs to",
99
100
  required: false,
@@ -130,6 +131,7 @@ export default class Metric extends AnalyticsBaseModel {
130
131
  // add name and description
131
132
  const nameColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
132
133
  key: "name",
134
+ codec: { codec: "ZSTD", level: 1 },
133
135
  title: "Name",
134
136
  description: "Name of the Metric",
135
137
  required: true,
@@ -196,6 +198,7 @@ export default class Metric extends AnalyticsBaseModel {
196
198
  const metricPointTypeColumn: AnalyticsTableColumn =
197
199
  new AnalyticsTableColumn({
198
200
  key: "metricPointType",
201
+ isLowCardinality: true,
199
202
  title: "Metric Point Type",
200
203
  description: "Metric Point Type of this Metric",
201
204
  required: false,
@@ -232,6 +235,7 @@ export default class Metric extends AnalyticsBaseModel {
232
235
  // this is end time.
233
236
  const timeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
234
237
  key: "time",
238
+ codec: [{ codec: "DoubleDelta" }, { codec: "ZSTD", level: 1 }],
235
239
  title: "Time",
236
240
  description: "When did the Metric happen?",
237
241
  required: true,
@@ -261,6 +265,7 @@ export default class Metric extends AnalyticsBaseModel {
261
265
 
262
266
  const startTimeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
263
267
  key: "startTime",
268
+ codec: { codec: "ZSTD", level: 1 },
264
269
  title: "Start Time",
265
270
  description: "When did the Metric happen?",
266
271
  required: false,
@@ -291,6 +296,7 @@ export default class Metric extends AnalyticsBaseModel {
291
296
  // end time.
292
297
  const timeUnixNanoColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
293
298
  key: "timeUnixNano",
299
+ codec: { codec: "ZSTD", level: 1 },
294
300
  title: "Time (in Unix Nano)",
295
301
  description: "When did the Metric happen?",
296
302
  required: true,
@@ -321,6 +327,7 @@ export default class Metric extends AnalyticsBaseModel {
321
327
  const startTimeUnixNanoColumn: AnalyticsTableColumn =
322
328
  new AnalyticsTableColumn({
323
329
  key: "startTimeUnixNano",
330
+ codec: { codec: "ZSTD", level: 1 },
324
331
  title: "Start Time (in Unix Nano)",
325
332
  description: "When did the Metric happen?",
326
333
  required: false,
@@ -350,6 +357,7 @@ export default class Metric extends AnalyticsBaseModel {
350
357
 
351
358
  const attributesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
352
359
  key: "attributes",
360
+ codec: { codec: "ZSTD", level: 3 },
353
361
  title: "Attributes",
354
362
  description: "Attributes",
355
363
  required: true,
@@ -380,6 +388,7 @@ export default class Metric extends AnalyticsBaseModel {
380
388
 
381
389
  const attributeKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
382
390
  key: "attributeKeys",
391
+ codec: { codec: "ZSTD", level: 3 },
383
392
  title: "Attribute Keys",
384
393
  description: "Attribute keys extracted from attributes",
385
394
  required: true,
@@ -445,6 +454,7 @@ export default class Metric extends AnalyticsBaseModel {
445
454
 
446
455
  const countColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
447
456
  key: "count",
457
+ codec: { codec: "ZSTD", level: 1 },
448
458
  title: "Count",
449
459
  description: "Count",
450
460
  required: false,
@@ -474,6 +484,7 @@ export default class Metric extends AnalyticsBaseModel {
474
484
 
475
485
  const sumColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
476
486
  key: "sum",
487
+ codec: { codec: "ZSTD", level: 1 },
477
488
  title: "Sum",
478
489
  description: "Sum",
479
490
  required: false,
@@ -503,6 +514,7 @@ export default class Metric extends AnalyticsBaseModel {
503
514
 
504
515
  const valueColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
505
516
  key: "value",
517
+ codec: { codec: "ZSTD", level: 1 },
506
518
  title: "Value",
507
519
  description: "Value",
508
520
  required: false,
@@ -532,6 +544,7 @@ export default class Metric extends AnalyticsBaseModel {
532
544
 
533
545
  const minColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
534
546
  key: "min",
547
+ codec: { codec: "ZSTD", level: 1 },
535
548
  title: "Min",
536
549
  description: "Min",
537
550
  required: false,
@@ -561,6 +574,7 @@ export default class Metric extends AnalyticsBaseModel {
561
574
 
562
575
  const maxColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
563
576
  key: "max",
577
+ codec: { codec: "ZSTD", level: 1 },
564
578
  title: "Max",
565
579
  description: "Max",
566
580
  required: false,
@@ -590,6 +604,7 @@ export default class Metric extends AnalyticsBaseModel {
590
604
 
591
605
  const bucketCountsColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
592
606
  key: "bucketCounts",
607
+ codec: { codec: "ZSTD", level: 1 },
593
608
  title: "Bucket Counts",
594
609
  description: "Bucket Counts",
595
610
  required: true,
@@ -621,6 +636,7 @@ export default class Metric extends AnalyticsBaseModel {
621
636
  const explicitBoundsColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
622
637
  {
623
638
  key: "explicitBounds",
639
+ codec: { codec: "ZSTD", level: 1 },
624
640
  title: "Explicit Bounds",
625
641
  description:
626
642
  "Upper bounds (exclusive of the +inf overflow bucket) for each explicit-bucket histogram bucket. Stored as Float64 so sub-integer boundaries (e.g. 0.005, 0.01) survive ingest — the previous Array(Int64) representation silently truncated those to 0.",
@@ -659,6 +675,7 @@ export default class Metric extends AnalyticsBaseModel {
659
675
 
660
676
  const scaleColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
661
677
  key: "scale",
678
+ codec: { codec: "ZSTD", level: 1 },
662
679
  title: "Scale",
663
680
  description:
664
681
  "ExponentialHistogram resolution. base = 2^(2^-scale); bucket index `i` covers (base^i, base^(i+1)].",
@@ -689,6 +706,7 @@ export default class Metric extends AnalyticsBaseModel {
689
706
 
690
707
  const zeroCountColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
691
708
  key: "zeroCount",
709
+ codec: { codec: "ZSTD", level: 1 },
692
710
  title: "Zero Count",
693
711
  description:
694
712
  "ExponentialHistogram count of values within the zero region (|v| <= zeroThreshold).",
@@ -720,6 +738,7 @@ export default class Metric extends AnalyticsBaseModel {
720
738
  const positiveOffsetColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
721
739
  {
722
740
  key: "positiveOffset",
741
+ codec: { codec: "ZSTD", level: 1 },
723
742
  title: "Positive Bucket Offset",
724
743
  description:
725
744
  "Bucket index of the first entry in positiveBucketCounts (ExponentialHistogram).",
@@ -752,6 +771,7 @@ export default class Metric extends AnalyticsBaseModel {
752
771
  const positiveBucketCountsColumn: AnalyticsTableColumn =
753
772
  new AnalyticsTableColumn({
754
773
  key: "positiveBucketCounts",
774
+ codec: { codec: "ZSTD", level: 1 },
755
775
  title: "Positive Bucket Counts",
756
776
  description:
757
777
  "Counts for the positive range of an ExponentialHistogram, indexed from positiveOffset.",
@@ -784,6 +804,7 @@ export default class Metric extends AnalyticsBaseModel {
784
804
  const negativeOffsetColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
785
805
  {
786
806
  key: "negativeOffset",
807
+ codec: { codec: "ZSTD", level: 1 },
787
808
  title: "Negative Bucket Offset",
788
809
  description:
789
810
  "Bucket index of the first entry in negativeBucketCounts (ExponentialHistogram).",
@@ -816,6 +837,7 @@ export default class Metric extends AnalyticsBaseModel {
816
837
  const negativeBucketCountsColumn: AnalyticsTableColumn =
817
838
  new AnalyticsTableColumn({
818
839
  key: "negativeBucketCounts",
840
+ codec: { codec: "ZSTD", level: 1 },
819
841
  title: "Negative Bucket Counts",
820
842
  description:
821
843
  "Counts for the negative range of an ExponentialHistogram, indexed from negativeOffset.",
@@ -855,6 +877,7 @@ export default class Metric extends AnalyticsBaseModel {
855
877
  const summaryQuantilesColumn: AnalyticsTableColumn =
856
878
  new AnalyticsTableColumn({
857
879
  key: "summaryQuantiles",
880
+ codec: { codec: "ZSTD", level: 1 },
858
881
  title: "Summary Quantiles",
859
882
  description:
860
883
  "Quantile percentages in [0,1] for a Summary metric (parallel to summaryValues).",
@@ -886,6 +909,7 @@ export default class Metric extends AnalyticsBaseModel {
886
909
 
887
910
  const summaryValuesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
888
911
  key: "summaryValues",
912
+ codec: { codec: "ZSTD", level: 1 },
889
913
  title: "Summary Values",
890
914
  description:
891
915
  "Values corresponding to each quantile in summaryQuantiles for a Summary metric.",
@@ -917,6 +941,7 @@ export default class Metric extends AnalyticsBaseModel {
917
941
 
918
942
  const traceIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
919
943
  key: "traceId",
944
+ codec: { codec: "ZSTD", level: 1 },
920
945
  title: "Trace ID",
921
946
  description:
922
947
  "Trace ID from an exemplar associated with this metric data point",
@@ -953,6 +978,7 @@ export default class Metric extends AnalyticsBaseModel {
953
978
 
954
979
  const spanIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
955
980
  key: "spanId",
981
+ codec: { codec: "ZSTD", level: 1 },
956
982
  title: "Span ID",
957
983
  description:
958
984
  "Span ID from an exemplar associated with this metric data point",
@@ -989,6 +1015,7 @@ export default class Metric extends AnalyticsBaseModel {
989
1015
 
990
1016
  const retentionDateColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
991
1017
  key: "retentionDate",
1018
+ codec: [{ codec: "DoubleDelta" }, { codec: "ZSTD", level: 1 }],
992
1019
  title: "Retention Date",
993
1020
  description:
994
1021
  "Date after which this row is eligible for TTL deletion, computed at ingest time as time + service.retainTelemetryDataForDays",
@@ -80,6 +80,7 @@ export default class Profile extends AnalyticsBaseModel {
80
80
 
81
81
  const serviceTypeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
82
82
  key: "serviceType",
83
+ isLowCardinality: true,
83
84
  title: "Service Type",
84
85
  description:
85
86
  "Discriminator for serviceId — tells the read side which resource table to dispatch to",
@@ -80,6 +80,7 @@ export default class ProfileSample extends AnalyticsBaseModel {
80
80
 
81
81
  const serviceTypeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
82
82
  key: "serviceType",
83
+ isLowCardinality: true,
83
84
  title: "Service Type",
84
85
  description:
85
86
  "Discriminator for serviceId — tells the read side which resource table to dispatch to",
@@ -112,6 +112,7 @@ export default class Span extends AnalyticsBaseModel {
112
112
 
113
113
  const serviceTypeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
114
114
  key: "serviceType",
115
+ isLowCardinality: true,
115
116
  title: "Service Type",
116
117
  description:
117
118
  "Discriminator for serviceId — tells the read side which resource table to dispatch to",
@@ -664,6 +665,7 @@ export default class Span extends AnalyticsBaseModel {
664
665
 
665
666
  const kindColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
666
667
  key: "kind",
668
+ isLowCardinality: true,
667
669
  title: "Kind",
668
670
  description: "Kind of the span",
669
671
  required: false,
@@ -44,6 +44,21 @@ const options: ClickHouseClientConfigOptions = {
44
44
  * user-facing queries of HTTP sockets.
45
45
  */
46
46
  max_open_connections: MaxClickhouseConnections,
47
+ /*
48
+ * Enable HTTP gzip compression in both directions. `request: true`
49
+ * gzips the client request body (large telemetry insert batches) before
50
+ * it goes over the wire; `response: true` asks ClickHouse to gzip query
51
+ * results (the wide log / span / metric JSON result sets dashboards
52
+ * read back). Both cut network bytes several-fold for the JSON payloads
53
+ * OneUptime exchanges, at a small CPU cost that the transfer savings
54
+ * outweigh. Response compression sends `enable_http_compression=1` per
55
+ * request, which requires a non-readonly user — the OneUptime ClickHouse
56
+ * user runs DDL and inserts, so that condition is satisfied.
57
+ */
58
+ compression: {
59
+ request: true,
60
+ response: true,
61
+ },
47
62
  };
48
63
 
49
64
  if (ShouldClickhouseSslEnable && ClickhouseTlsCa) {
@@ -6,6 +6,7 @@ import ClickhouseDatabase, {
6
6
  } from "../Infrastructure/ClickhouseDatabase";
7
7
  import ClusterKeyAuthorization from "../Middleware/ClusterKeyAuthorization";
8
8
  import CountBy from "../Types/AnalyticsDatabase/CountBy";
9
+ import ExistsBy from "../Types/AnalyticsDatabase/ExistsBy";
9
10
  import CreateBy from "../Types/AnalyticsDatabase/CreateBy";
10
11
  import CreateManyBy from "../Types/AnalyticsDatabase/CreateManyBy";
11
12
  import DeleteBy from "../Types/AnalyticsDatabase/DeleteBy";
@@ -182,6 +183,16 @@ export default class AnalyticsDatabaseService<
182
183
  dbResult.stream,
183
184
  );
184
185
 
186
+ /*
187
+ * Unwrap LowCardinality(...) first so dictionary-encoded columns
188
+ * (e.g. LowCardinality(String), LowCardinality(Nullable(String)))
189
+ * map back to their logical type instead of falling through to null.
190
+ */
191
+ if (strResult.includes("LowCardinality(")) {
192
+ const inner: string = strResult.split("LowCardinality(")[1] as string;
193
+ strResult = inner.substring(0, inner.lastIndexOf(")"));
194
+ }
195
+
185
196
  // if strResult includes Nullable(type) then extract type.
186
197
 
187
198
  if (strResult.includes("Nullable")) {
@@ -267,6 +278,46 @@ export default class AnalyticsDatabaseService<
267
278
  }
268
279
  }
269
280
 
281
+ /**
282
+ * Returns whether at least one row matches the query, without counting
283
+ * every match. Prefer this over `countBy(...).toNumber() === 0` for
284
+ * existence checks: `count()` scans every matching row, whereas this
285
+ * issues `SELECT 1 ... LIMIT 1`, which lets ClickHouse short-circuit at
286
+ * the first matching granule — dramatically cheaper on large tables
287
+ * (Metric / Span / Log).
288
+ */
289
+ @CaptureSpan()
290
+ public async existsBy(existsBy: ExistsBy<TBaseModel>): Promise<boolean> {
291
+ try {
292
+ const checkReadPermissionType: CheckReadPermissionType<TBaseModel> =
293
+ await ModelPermission.checkReadPermission(
294
+ this.modelType,
295
+ existsBy.query,
296
+ null,
297
+ existsBy.props,
298
+ );
299
+
300
+ existsBy.query = checkReadPermissionType.query;
301
+
302
+ const existsStatement: Statement = this.toExistsStatement(existsBy);
303
+
304
+ const dbResult: ResultSet<"JSON"> =
305
+ await this.executeQuery(existsStatement);
306
+
307
+ const resultInJSON: ResponseJSON<JSONObject> =
308
+ await dbResult.json<JSONObject>();
309
+
310
+ return Boolean(
311
+ resultInJSON.data &&
312
+ Array.isArray(resultInJSON.data) &&
313
+ resultInJSON.data.length > 0,
314
+ );
315
+ } catch (error) {
316
+ await this.onFindError(error as Exception);
317
+ throw this.getException(error as Exception);
318
+ }
319
+ }
320
+
270
321
  @CaptureSpan()
271
322
  public async addColumnInDatabase(
272
323
  column: AnalyticsTableColumn,
@@ -335,6 +386,29 @@ export default class AnalyticsDatabaseService<
335
386
  return (rows[0]!["compression_codec"] as string) || "";
336
387
  }
337
388
 
389
+ /**
390
+ * The exact ClickHouse type string for a column as stored in the DB
391
+ * (e.g. "String", "Nullable(Int32)", "LowCardinality(Nullable(String))").
392
+ * Returns "" if the column does not exist. Used by migrations that need to
393
+ * re-state a column's type in a MODIFY COLUMN without guessing it.
394
+ */
395
+ public async getColumnDatabaseType(columnName: string): Promise<string> {
396
+ const tableName: string = this.model.tableName;
397
+ const result: { data: Array<JSONObject> } = await (
398
+ await this.executeQuery(
399
+ `SELECT type FROM system.columns WHERE database = currentDatabase() AND table = '${tableName}' AND name = '${columnName}'`,
400
+ )
401
+ ).json();
402
+
403
+ const rows: Array<JSONObject> = result.data || [];
404
+
405
+ if (rows.length === 0) {
406
+ return "";
407
+ }
408
+
409
+ return (rows[0]!["type"] as string) || "";
410
+ }
411
+
338
412
  public async setColumnCodecIfNotSet(data: {
339
413
  columnName: string;
340
414
  columnType: string;
@@ -841,6 +915,44 @@ export default class AnalyticsDatabaseService<
841
915
  return statement;
842
916
  }
843
917
 
918
+ public toExistsStatement(existsBy: ExistsBy<TBaseModel>): Statement {
919
+ if (!this.database) {
920
+ this.useDefaultDatabase();
921
+ }
922
+
923
+ const databaseName: string = this.database.getDatasourceOptions().database!;
924
+
925
+ const whereStatement: Statement = this.statementGenerator.toWhereStatement(
926
+ existsBy.query,
927
+ );
928
+
929
+ /*
930
+ * `SELECT 1 ... LIMIT 1` so ClickHouse stops at the first matching
931
+ * row instead of scanning every match like count() does. The
932
+ * max_execution_time cap is defense in depth; unlike the count and
933
+ * find statements we deliberately do NOT set timeout_overflow_mode
934
+ * = 'break' here, because a partial (empty) result would be read as
935
+ * "does not exist" — a false negative that could, for example, let a
936
+ * caller insert a duplicate. A LIMIT 1 over the sort key never gets
937
+ * near this cap in practice; if it ever did, throwing is the safe
938
+ * outcome.
939
+ */
940
+ /* eslint-disable prettier/prettier */
941
+ const statement: Statement = SQL`
942
+ SELECT 1 as existsFlag
943
+ FROM ${databaseName}.${this.model.tableName}
944
+ WHERE TRUE `.append(whereStatement);
945
+
946
+ statement.append(SQL` LIMIT 1`);
947
+
948
+ statement.append(" SETTINGS max_execution_time = 45");
949
+
950
+ logger.debug(`${this.model.tableName} Exists Statement`, { tableName: this.model.tableName } as LogAttributes);
951
+ logger.debug(statement, { tableName: this.model.tableName } as LogAttributes);
952
+
953
+ return statement;
954
+ }
955
+
844
956
  public toAggregateStatement(aggregateBy: AggregateBy<TBaseModel>): {
845
957
  statement: Statement;
846
958
  columns: Array<string>;
@@ -2796,20 +2796,18 @@ ${incidentSeverity.name}
2796
2796
  * re-emitting, the dashboard Sum stays equal to the true count of
2797
2797
  * distinct incidents.
2798
2798
  */
2799
- const existingIncidentCount: PositiveNumber = await MetricService.countBy({
2799
+ const incidentCountMetricExists: boolean = await MetricService.existsBy({
2800
2800
  query: {
2801
2801
  projectId: incident.projectId,
2802
2802
  serviceId: data.incidentId,
2803
2803
  name: IncidentMetricType.IncidentCount,
2804
2804
  },
2805
- skip: 0,
2806
- limit: 1,
2807
2805
  props: {
2808
2806
  isRoot: true,
2809
2807
  },
2810
2808
  });
2811
2809
 
2812
- if (existingIncidentCount.toNumber() === 0) {
2810
+ if (!incidentCountMetricExists) {
2813
2811
  const incidentCountMetric: Metric = new Metric();
2814
2812
 
2815
2813
  incidentCountMetric.projectId = incident.projectId;
@@ -524,6 +524,15 @@ export class LogAggregationService {
524
524
 
525
525
  statement.append(" ORDER BY bucket ASC");
526
526
 
527
+ /*
528
+ * Defense in depth: cap runtime below the client's 58s request_timeout
529
+ * (matches the histogram / facet paths above). 'break' returns partial
530
+ * aggregated results rather than holding a pool connection.
531
+ */
532
+ statement.append(
533
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
534
+ );
535
+
527
536
  return statement;
528
537
  }
529
538
 
@@ -591,6 +600,14 @@ export class LogAggregationService {
591
600
  }}`,
592
601
  );
593
602
 
603
+ /*
604
+ * Cap runtime below the client's 58s request_timeout; 'break' returns
605
+ * partial results (matches the histogram / facet paths).
606
+ */
607
+ statement.append(
608
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
609
+ );
610
+
594
611
  return statement;
595
612
  }
596
613
 
@@ -647,6 +664,14 @@ export class LogAggregationService {
647
664
  }}`,
648
665
  );
649
666
 
667
+ /*
668
+ * Cap runtime below the client's 58s request_timeout; 'break' returns
669
+ * partial results (matches the histogram / facet paths).
670
+ */
671
+ statement.append(
672
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
673
+ );
674
+
650
675
  return statement;
651
676
  }
652
677
 
@@ -784,6 +809,14 @@ export class LogAggregationService {
784
809
  }}`,
785
810
  );
786
811
 
812
+ /*
813
+ * Cap runtime below the client's 58s request_timeout; 'break' returns
814
+ * partial rows rather than holding a pool connection on a large export.
815
+ */
816
+ statement.append(
817
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
818
+ );
819
+
787
820
  const dbResult: Results = await LogDatabaseService.executeQuery(statement);
788
821
  const response: DbJSONResponse = await dbResult.json<{
789
822
  data?: Array<JSONObject>;
@@ -926,6 +959,14 @@ export class LogAggregationService {
926
959
 
927
960
  LogAggregationService.appendCommonFilters(totalStatement, request);
928
961
 
962
+ /*
963
+ * Cap the count scan below the client's 58s request_timeout; 'break'
964
+ * returns a partial (lower-bound) count, acceptable for an estimate.
965
+ */
966
+ totalStatement.append(
967
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
968
+ );
969
+
929
970
  // Get matching count using the filter query as body search
930
971
  const matchStatement: Statement = SQL`
931
972
  SELECT count() AS cnt
@@ -949,6 +990,14 @@ export class LogAggregationService {
949
990
  bodySearchText: request.filterQuery,
950
991
  });
951
992
 
993
+ /*
994
+ * Cap the count scan below the client's 58s request_timeout; 'break'
995
+ * returns a partial (lower-bound) count, acceptable for an estimate.
996
+ */
997
+ matchStatement.append(
998
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
999
+ );
1000
+
952
1001
  const [totalResult, matchResult] = await Promise.all([
953
1002
  LogDatabaseService.executeQuery(totalStatement),
954
1003
  LogDatabaseService.executeQuery(matchStatement),
@@ -528,6 +528,15 @@ export class ProfileAggregationService {
528
528
  }}`,
529
529
  );
530
530
 
531
+ /*
532
+ * Cap runtime below the client's 58s request_timeout; a flamegraph
533
+ * over a busy project can pull MAX_SAMPLE_FETCH raw samples. 'break'
534
+ * yields a partial flamegraph rather than holding a pool connection.
535
+ */
536
+ statement.append(
537
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
538
+ );
539
+
531
540
  return statement;
532
541
  }
533
542
 
@@ -563,6 +572,14 @@ export class ProfileAggregationService {
563
572
  }}`,
564
573
  );
565
574
 
575
+ /*
576
+ * Cap runtime below the client's 58s request_timeout; 'break' yields
577
+ * a partial function list rather than holding a pool connection.
578
+ */
579
+ statement.append(
580
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
581
+ );
582
+
566
583
  return statement;
567
584
  }
568
585
 
@@ -622,6 +639,14 @@ export class ProfileAggregationService {
622
639
  LIMIT 10000`,
623
640
  );
624
641
 
642
+ /*
643
+ * Cap runtime below the client's 58s request_timeout; 'break' yields
644
+ * partial service activity rather than holding a pool connection.
645
+ */
646
+ statement.append(
647
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
648
+ );
649
+
625
650
  return statement;
626
651
  }
627
652
 
@@ -307,6 +307,16 @@ export class TelemetryAttributeService {
307
307
  );
308
308
  }
309
309
 
310
+ /*
311
+ * Cap runtime below the ClickHouse client's 58s request_timeout so a
312
+ * slow scan on a large project can't hold a pool connection for the
313
+ * full timeout. 'break' returns partial keys, which is fine for an
314
+ * attribute-key picker (matches the findBy / aggregation read paths).
315
+ */
316
+ statement.append(
317
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
318
+ );
319
+
310
320
  return statement;
311
321
  }
312
322
 
@@ -451,6 +461,16 @@ export class TelemetryAttributeService {
451
461
  }}`,
452
462
  );
453
463
 
464
+ /*
465
+ * Cap runtime below the client's 58s request_timeout. This value
466
+ * autocomplete runs per keystroke and scans a Map subscript, so a
467
+ * pathological key/project must not hold a pool connection; 'break'
468
+ * returns partial values, acceptable for autocomplete.
469
+ */
470
+ statement.append(
471
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
472
+ );
473
+
454
474
  return statement;
455
475
  }
456
476
 
@@ -0,0 +1,8 @@
1
+ import Query from "./Query";
2
+ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
3
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
4
+
5
+ export default interface ExistsBy<TBaseModel extends AnalyticsBaseModel> {
6
+ query: Query<TBaseModel>;
7
+ props: DatabaseCommonInteractionProps;
8
+ }