@oneuptime/common 10.0.94 → 10.0.97

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 (134) hide show
  1. package/Models/AnalyticsModels/Log.ts +6 -0
  2. package/Models/AnalyticsModels/Metric.ts +6 -0
  3. package/Models/AnalyticsModels/Profile.ts +6 -0
  4. package/Models/AnalyticsModels/Span.ts +6 -0
  5. package/Models/DatabaseModels/Alert.ts +52 -0
  6. package/Models/DatabaseModels/DockerHost.ts +3 -10
  7. package/Models/DatabaseModels/Host.ts +1015 -0
  8. package/Models/DatabaseModels/HostOwnerTeam.ts +462 -0
  9. package/Models/DatabaseModels/HostOwnerUser.ts +461 -0
  10. package/Models/DatabaseModels/Incident.ts +52 -0
  11. package/Models/DatabaseModels/Index.ts +6 -0
  12. package/Models/DatabaseModels/KubernetesCluster.ts +0 -7
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1778006035712-AddHostTables.ts +201 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1778013317872-AddHostIpAddresses.ts +15 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1778066346303-WidenHostOsVersionToLongText.ts +42 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1778070278986-MigrationName.ts +79 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  18. package/Server/Services/HostOwnerTeamService.ts +10 -0
  19. package/Server/Services/HostOwnerUserService.ts +10 -0
  20. package/Server/Services/HostService.ts +227 -0
  21. package/Server/Services/LogAggregationService.ts +10 -3
  22. package/Server/Services/MetricService.ts +200 -0
  23. package/Server/Services/MonitorTemplateService.ts +11 -3
  24. package/Server/Services/TraceAggregationService.ts +8 -3
  25. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +46 -18
  26. package/Server/Utils/Monitor/MonitorAlert.ts +31 -0
  27. package/Server/Utils/Monitor/MonitorIncident.ts +31 -0
  28. package/Server/Utils/VM/VMRunner.ts +62 -0
  29. package/Tests/Server/Services/LogAggregationService.test.ts +25 -0
  30. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +145 -0
  31. package/Types/Metrics/MetricQueryConfigData.ts +9 -0
  32. package/Types/Permission.ts +134 -0
  33. package/UI/Components/Charts/Area/AreaChart.tsx +1 -1
  34. package/UI/Components/Charts/Bar/BarChart.tsx +1 -1
  35. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +15 -8
  36. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +12 -9
  37. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +17 -10
  38. package/UI/Components/Charts/Line/LineChart.tsx +1 -1
  39. package/UI/Components/ExpandableText/ExpandableText.tsx +29 -7
  40. package/UI/Components/JSONTable/JSONTable.tsx +27 -1
  41. package/UI/Components/LogsViewer/LogsViewer.tsx +3 -0
  42. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +109 -23
  43. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +11 -4
  44. package/UI/Components/Navbar/NavBarMenu.tsx +17 -2
  45. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +10 -3
  46. package/Utils/ValueFormatter.ts +57 -3
  47. package/build/dist/Models/AnalyticsModels/Log.js +6 -0
  48. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  49. package/build/dist/Models/AnalyticsModels/Metric.js +6 -0
  50. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  51. package/build/dist/Models/AnalyticsModels/Profile.js +6 -0
  52. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  53. package/build/dist/Models/AnalyticsModels/Span.js +6 -0
  54. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  55. package/build/dist/Models/DatabaseModels/Alert.js +51 -0
  56. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  57. package/build/dist/Models/DatabaseModels/DockerHost.js +3 -10
  58. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  59. package/build/dist/Models/DatabaseModels/Host.js +1041 -0
  60. package/build/dist/Models/DatabaseModels/Host.js.map +1 -0
  61. package/build/dist/Models/DatabaseModels/HostOwnerTeam.js +480 -0
  62. package/build/dist/Models/DatabaseModels/HostOwnerTeam.js.map +1 -0
  63. package/build/dist/Models/DatabaseModels/HostOwnerUser.js +479 -0
  64. package/build/dist/Models/DatabaseModels/HostOwnerUser.js.map +1 -0
  65. package/build/dist/Models/DatabaseModels/Incident.js +51 -0
  66. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  67. package/build/dist/Models/DatabaseModels/Index.js +6 -0
  68. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  69. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +0 -7
  70. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778006035712-AddHostTables.js +76 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778006035712-AddHostTables.js.map +1 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778013317872-AddHostIpAddresses.js +12 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778013317872-AddHostIpAddresses.js.map +1 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778066346303-WidenHostOsVersionToLongText.js +31 -0
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778066346303-WidenHostOsVersionToLongText.js.map +1 -0
  77. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778070278986-MigrationName.js +34 -0
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778070278986-MigrationName.js.map +1 -0
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  81. package/build/dist/Server/Services/HostOwnerTeamService.js +9 -0
  82. package/build/dist/Server/Services/HostOwnerTeamService.js.map +1 -0
  83. package/build/dist/Server/Services/HostOwnerUserService.js +9 -0
  84. package/build/dist/Server/Services/HostOwnerUserService.js.map +1 -0
  85. package/build/dist/Server/Services/HostService.js +206 -0
  86. package/build/dist/Server/Services/HostService.js.map +1 -0
  87. package/build/dist/Server/Services/LogAggregationService.js +10 -3
  88. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  89. package/build/dist/Server/Services/MetricService.js +160 -0
  90. package/build/dist/Server/Services/MetricService.js.map +1 -1
  91. package/build/dist/Server/Services/MonitorTemplateService.js +7 -0
  92. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  93. package/build/dist/Server/Services/TraceAggregationService.js +8 -3
  94. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  95. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +46 -18
  96. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  97. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +26 -0
  98. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  99. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +26 -0
  100. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  101. package/build/dist/Server/Utils/VM/VMRunner.js +61 -0
  102. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  103. package/build/dist/Tests/Server/Services/LogAggregationService.test.js +13 -0
  104. package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
  105. package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js +123 -0
  106. package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js.map +1 -1
  107. package/build/dist/Types/Permission.js +120 -0
  108. package/build/dist/Types/Permission.js.map +1 -1
  109. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  110. package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
  111. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +13 -7
  112. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  113. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +11 -9
  114. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  115. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +16 -10
  116. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  117. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  118. package/build/dist/UI/Components/ExpandableText/ExpandableText.js +10 -5
  119. package/build/dist/UI/Components/ExpandableText/ExpandableText.js.map +1 -1
  120. package/build/dist/UI/Components/JSONTable/JSONTable.js +8 -1
  121. package/build/dist/UI/Components/JSONTable/JSONTable.js.map +1 -1
  122. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +1 -1
  123. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  124. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +40 -14
  125. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  126. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +10 -4
  127. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  128. package/build/dist/UI/Components/Navbar/NavBarMenu.js +15 -2
  129. package/build/dist/UI/Components/Navbar/NavBarMenu.js.map +1 -1
  130. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +10 -3
  131. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  132. package/build/dist/Utils/ValueFormatter.js +47 -3
  133. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  134. package/package.json +1 -1
@@ -57,6 +57,20 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
57
57
  columns: Array<string>;
58
58
  } {
59
59
  if (!isPercentileAggregation(aggregateBy.aggregationType)) {
60
+ /*
61
+ * Try the per-host MV first — host detail pages are the
62
+ * dominant attribute-filtered path and the per-host MV is
63
+ * the only one that can serve them. If it doesn't apply
64
+ * (no host filter, or extra attrs/groupBy), fall through
65
+ * to the project/serviceId MV, then to the base table.
66
+ */
67
+ const hostMvStatement: {
68
+ statement: Statement;
69
+ columns: Array<string>;
70
+ } | null = this.tryBuildHostAggregateMVStatement(aggregateBy);
71
+ if (hostMvStatement) {
72
+ return hostMvStatement;
73
+ }
60
74
  const mvStatement: {
61
75
  statement: Statement;
62
76
  columns: Array<string>;
@@ -401,6 +415,192 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
401
415
  };
402
416
  }
403
417
 
418
+ /*
419
+ * Per-host materialized-view fast path.
420
+ *
421
+ * Returns a statement that reads from MetricItemAggMV1mByHost
422
+ * (created by AddMetricMinuteAggregateByHostMaterializedView)
423
+ * when:
424
+ *
425
+ * - The aggregation is Sum/Avg/Min/Max/Count over `value`.
426
+ * - The only attribute filter is `resource.host.name` as a
427
+ * bare-string equality (the dashboard's host detail page
428
+ * pattern).
429
+ * - The query carries no group-by other than the time
430
+ * bucket — the MV is keyed by hostIdentifier and does not
431
+ * preserve other attribute breakdowns.
432
+ *
433
+ * Returns `null` if any condition fails so the caller falls
434
+ * through to the next fast path / base table. The result row
435
+ * shape (columns: aggregateColumn, timestampColumn) matches
436
+ * the base statement so downstream code needs no changes.
437
+ */
438
+ private tryBuildHostAggregateMVStatement(
439
+ aggregateBy: AggregateBy<Metric>,
440
+ ): { statement: Statement; columns: Array<string> } | null {
441
+ const aggType: AggregationType = aggregateBy.aggregationType;
442
+ const supported: ReadonlyArray<AggregationType> = [
443
+ AggregationType.Sum,
444
+ AggregationType.Avg,
445
+ AggregationType.Min,
446
+ AggregationType.Max,
447
+ AggregationType.Count,
448
+ ];
449
+ if (!supported.includes(aggType)) {
450
+ return null;
451
+ }
452
+
453
+ if (
454
+ aggregateBy.aggregateColumnName.toString() !== "value" ||
455
+ aggregateBy.aggregationTimestampColumnName.toString() !== "time"
456
+ ) {
457
+ return null;
458
+ }
459
+
460
+ if (aggregateBy.groupBy && Object.keys(aggregateBy.groupBy).length > 0) {
461
+ return null;
462
+ }
463
+
464
+ /*
465
+ * Inspect the attribute filter. This MV is only safe when
466
+ * the user is filtering by exactly one attribute,
467
+ * `resource.host.name`, with a bare-string value (the
468
+ * canonical Overview/Metrics-page pattern). Anything else —
469
+ * extra attribute filters, NotEqual, Search, etc. — has to
470
+ * fall back so the result stays correct.
471
+ */
472
+ const queryRecord: Record<string, unknown> =
473
+ (aggregateBy.query as unknown as Record<string, unknown>) || {};
474
+ const attrs: unknown = queryRecord["attributes"];
475
+ if (!attrs || typeof attrs !== "object") {
476
+ return null;
477
+ }
478
+ const attrEntries: Array<[string, unknown]> = Object.entries(
479
+ attrs as Record<string, unknown>,
480
+ );
481
+ if (attrEntries.length !== 1) {
482
+ return null;
483
+ }
484
+ const [attrKey, attrValue] = attrEntries[0]!;
485
+ if (attrKey !== "resource.host.name") {
486
+ return null;
487
+ }
488
+ if (attrValue === undefined || attrValue === null) {
489
+ return null;
490
+ }
491
+ const hostIdentifier: string =
492
+ typeof attrValue === "string" ? attrValue : "";
493
+ if (!hostIdentifier) {
494
+ return null;
495
+ }
496
+
497
+ const interval: AggregationInterval = AggregateUtil.getAggregationInterval({
498
+ startDate: aggregateBy.startTimestamp!,
499
+ endDate: aggregateBy.endTimestamp!,
500
+ });
501
+ void interval;
502
+
503
+ if (!this.database) {
504
+ this.useDefaultDatabase();
505
+ }
506
+ const databaseName: string = this.database.getDatasourceOptions().database!;
507
+
508
+ const intervalLower: string = interval.toLowerCase();
509
+
510
+ let mergedExpr: string;
511
+ if (aggType === AggregationType.Sum) {
512
+ mergedExpr = `sumMerge(valueSumState)`;
513
+ } else if (aggType === AggregationType.Count) {
514
+ mergedExpr = `countMerge(valueCountState)`;
515
+ } else if (aggType === AggregationType.Min) {
516
+ mergedExpr = `minMerge(valueMinState)`;
517
+ } else if (aggType === AggregationType.Max) {
518
+ mergedExpr = `maxMerge(valueMaxState)`;
519
+ } else {
520
+ mergedExpr = `if(countMerge(valueCountState) = 0, 0, sumMerge(valueSumState) / countMerge(valueCountState))`;
521
+ }
522
+
523
+ /*
524
+ * Strip both `time` (column doesn't exist on the MV; we
525
+ * inject an explicit bucketTime range below) and
526
+ * `attributes` (the attribute filter is now an explicit
527
+ * `hostIdentifier =` predicate against an MV column).
528
+ */
529
+ const filteredQuery: typeof aggregateBy.query =
530
+ this.stripAttributesAndTimeFromQuery(
531
+ aggregateBy.query,
532
+ ) as typeof aggregateBy.query;
533
+ const nonTimeWhere: Statement =
534
+ this.statementGenerator.toWhereStatement(filteredQuery);
535
+ const sortStatement: Statement = this.statementGenerator.toSortStatement(
536
+ aggregateBy.sort!,
537
+ );
538
+
539
+ const statement: Statement = SQL``;
540
+
541
+ statement.append(
542
+ `SELECT ${mergedExpr} as value, date_trunc('${intervalLower}', toStartOfInterval(bucketTime, INTERVAL 1 ${intervalLower})) as time`,
543
+ );
544
+ statement.append(SQL` FROM ${databaseName}.MetricItemAggMV1mByHost`);
545
+ statement.append(
546
+ ` WHERE bucketTime >= toDateTime('${this.formatDateTime(aggregateBy.startTimestamp!)}') AND bucketTime <= toDateTime('${this.formatDateTime(aggregateBy.endTimestamp!)}')`,
547
+ );
548
+ statement.append(
549
+ SQL` AND hostIdentifier = ${{
550
+ value: hostIdentifier,
551
+ type: TableColumnType.Text,
552
+ }}`,
553
+ );
554
+ statement.append(SQL` `).append(nonTimeWhere);
555
+
556
+ statement.append(SQL` GROUP BY `).append(`time`);
557
+ statement.append(SQL` ORDER BY `).append(sortStatement);
558
+ statement.append(
559
+ SQL` LIMIT ${{
560
+ value: Number(aggregateBy.limit),
561
+ type: TableColumnType.Number,
562
+ }}`,
563
+ );
564
+ statement.append(
565
+ SQL` OFFSET ${{
566
+ value: Number(aggregateBy.skip),
567
+ type: TableColumnType.Number,
568
+ }} `,
569
+ );
570
+ statement.append(
571
+ ` SETTINGS optimize_aggregation_in_order=1, optimize_move_to_prewhere=1, max_threads=4`,
572
+ );
573
+
574
+ logger.debug(`${this.model.tableName} Host MV Aggregate Statement`, {
575
+ tableName: this.model.tableName,
576
+ } as LogAttributes);
577
+ logger.debug(statement, {
578
+ tableName: this.model.tableName,
579
+ } as LogAttributes);
580
+
581
+ return {
582
+ statement,
583
+ columns: [
584
+ aggregateBy.aggregateColumnName.toString(),
585
+ aggregateBy.aggregationTimestampColumnName.toString(),
586
+ ],
587
+ };
588
+ }
589
+
590
+ private stripAttributesAndTimeFromQuery(query: unknown): typeof query {
591
+ if (!query || typeof query !== "object") {
592
+ return query;
593
+ }
594
+ const out: Record<string, unknown> = {};
595
+ for (const [k, v] of Object.entries(query as Record<string, unknown>)) {
596
+ if (k === "time" || k === "attributes") {
597
+ continue;
598
+ }
599
+ out[k] = v;
600
+ }
601
+ return out as typeof query;
602
+ }
603
+
404
604
  private stripTimeFromQuery(query: unknown): typeof query {
405
605
  if (!query || typeof query !== "object") {
406
606
  return query;
@@ -16,18 +16,20 @@ export interface SyncLinkedMonitorsResult {
16
16
 
17
17
  /**
18
18
  * Subset of Monitor fields that a template push can overwrite. Anything
19
- * outside this set (name, description, labels, monitorType, etc.) is
20
- * intentionally never touched by sync — those are per-monitor concerns.
19
+ * outside this set (name, description, monitorType, etc.) is intentionally
20
+ * never touched by sync — those are per-monitor concerns.
21
21
  */
22
22
  export type SyncableTemplateField =
23
23
  | "monitorSteps"
24
24
  | "monitoringInterval"
25
- | "minimumProbeAgreement";
25
+ | "minimumProbeAgreement"
26
+ | "labels";
26
27
 
27
28
  const ALL_SYNCABLE_FIELDS: ReadonlyArray<SyncableTemplateField> = [
28
29
  "monitorSteps",
29
30
  "monitoringInterval",
30
31
  "minimumProbeAgreement",
32
+ "labels",
31
33
  ];
32
34
 
33
35
  export class Service extends DatabaseService<Model> {
@@ -125,6 +127,9 @@ export class Service extends DatabaseService<Model> {
125
127
  monitorSteps: true,
126
128
  monitoringInterval: true,
127
129
  minimumProbeAgreement: true,
130
+ labels: {
131
+ _id: true,
132
+ },
128
133
  },
129
134
  props: data.props,
130
135
  });
@@ -203,6 +208,9 @@ export class Service extends DatabaseService<Model> {
203
208
  monitorSteps: true,
204
209
  monitoringInterval: true,
205
210
  minimumProbeAgreement: true,
211
+ labels: {
212
+ _id: true,
213
+ },
206
214
  },
207
215
  props: data.props,
208
216
  });
@@ -503,14 +503,19 @@ export class TraceAggregationService {
503
503
  for (const [attrKey, attrValue] of Object.entries(request.attributes)) {
504
504
  TraceAggregationService.validateFacetKey(attrKey);
505
505
 
506
+ /*
507
+ * Match attribute keys case-insensitively — see the matching note in
508
+ * LogAggregationService.appendCommonFilters. Casings vary across
509
+ * OTEL conventions and app-emitted attributes.
510
+ */
506
511
  statement.append(
507
- SQL` AND attributes[${{
512
+ SQL` AND arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
508
513
  type: TableColumnType.Text,
509
514
  value: attrKey,
510
- }}] = ${{
515
+ }}) AND v = ${{
511
516
  type: TableColumnType.Text,
512
517
  value: attrValue,
513
- }}`,
518
+ }}, mapKeys(attributes), mapValues(attributes))`,
514
519
  );
515
520
  }
516
521
  }
@@ -463,10 +463,35 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
463
463
  }
464
464
 
465
465
  /*
466
- * ClickHouse Map columns return the value type's default for
467
- * missing keys (empty string for String values), so to express
468
- * "is empty" we have to cover both the missing-key and the
469
- * empty-string case explicitly.
466
+ * Map filters split into two paths:
467
+ *
468
+ * 1. Programmatic equality / null / numeric comparisons
469
+ * EqualTo, NotEqual, IsNull, NotNull, GreaterThan, etc.,
470
+ * or bare string/number values. Callers are dashboard
471
+ * pages and services that pass canonical keys already
472
+ * matching the stored casing, so we use ClickHouse's
473
+ * direct Map subscript `attributes['k']`. That's an O(1)
474
+ * hash lookup per row and lets the query planner push the
475
+ * predicate into PREWHERE, instead of paying the
476
+ * `arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(...))`
477
+ * cost which materializes mapKeys/mapValues per row and
478
+ * lowercases every stored key on every query. Restoring
479
+ * this fast path is the single biggest performance fix
480
+ * for Host / Logs / Traces detail pages.
481
+ *
482
+ * 2. User-typed substring/wildcard operators — Search,
483
+ * StartsWith, EndsWith, NotContains. These come from the
484
+ * search bar where users shouldn't have to remember
485
+ * whether the attribute key is `requestId` or `requestid`,
486
+ * so we keep the case-insensitive `arrayExists` form. The
487
+ * cost is acceptable because a search-bar query is
488
+ * bounded (one user, one click) and these operators
489
+ * already imply a row scan.
490
+ *
491
+ * ClickHouse Map subscripts return the value type's default
492
+ * for missing keys (empty string for String values), which
493
+ * is what the IsNull / NotNull / NotEqual branches below
494
+ * mirror to preserve the previous semantics.
470
495
  */
471
496
  if (mapEntry instanceof IsNull) {
472
497
  whereStatement.append(
@@ -496,13 +521,13 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
496
521
 
497
522
  if (mapEntry instanceof Search) {
498
523
  whereStatement.append(
499
- SQL`AND ${key}[${{
524
+ SQL`AND arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
500
525
  value: mapKey,
501
526
  type: TableColumnType.Text,
502
- }}] ILIKE ${{
527
+ }}) AND v ILIKE ${{
503
528
  value: mapEntry as Search<string>,
504
529
  type: TableColumnType.Text,
505
- }}`,
530
+ }}, mapKeys(${key}), mapValues(${key}))`,
506
531
  );
507
532
  continue;
508
533
  }
@@ -510,13 +535,13 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
510
535
  if (mapEntry instanceof NotContains) {
511
536
  const literalValue: string = `%${(mapEntry.value as string) || ""}%`;
512
537
  whereStatement.append(
513
- SQL`AND ${key}[${{
538
+ SQL`AND NOT arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
514
539
  value: mapKey,
515
540
  type: TableColumnType.Text,
516
- }}] NOT ILIKE ${{
541
+ }}) AND v ILIKE ${{
517
542
  value: literalValue,
518
543
  type: TableColumnType.Text,
519
- }}`,
544
+ }}, mapKeys(${key}), mapValues(${key}))`,
520
545
  );
521
546
  continue;
522
547
  }
@@ -524,13 +549,13 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
524
549
  if (mapEntry instanceof StartsWith) {
525
550
  const literalValue: string = `${(mapEntry.value as string) || ""}%`;
526
551
  whereStatement.append(
527
- SQL`AND ${key}[${{
552
+ SQL`AND arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
528
553
  value: mapKey,
529
554
  type: TableColumnType.Text,
530
- }}] ILIKE ${{
555
+ }}) AND v ILIKE ${{
531
556
  value: literalValue,
532
557
  type: TableColumnType.Text,
533
- }}`,
558
+ }}, mapKeys(${key}), mapValues(${key}))`,
534
559
  );
535
560
  continue;
536
561
  }
@@ -538,13 +563,13 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
538
563
  if (mapEntry instanceof EndsWith) {
539
564
  const literalValue: string = `%${(mapEntry.value as string) || ""}`;
540
565
  whereStatement.append(
541
- SQL`AND ${key}[${{
566
+ SQL`AND arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
542
567
  value: mapKey,
543
568
  type: TableColumnType.Text,
544
- }}] ILIKE ${{
569
+ }}) AND v ILIKE ${{
545
570
  value: literalValue,
546
571
  type: TableColumnType.Text,
547
- }}`,
572
+ }}, mapKeys(${key}), mapValues(${key}))`,
548
573
  );
549
574
  continue;
550
575
  }
@@ -577,7 +602,10 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
577
602
 
578
603
  /*
579
604
  * Map values are stored as text; cast to Float64 for numeric
580
- * comparisons and skip rows where the cast fails (non-numeric).
605
+ * comparisons. toFloat64OrNull yields NULL for non-numeric
606
+ * values (including the empty-string default for missing
607
+ * keys), which compares to false against any numeric
608
+ * threshold and naturally drops those rows.
581
609
  */
582
610
  if (mapEntry instanceof GreaterThan) {
583
611
  whereStatement.append(
@@ -631,7 +659,7 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
631
659
  continue;
632
660
  }
633
661
 
634
- // Bare string/number/boolean — back-compat with existing data.
662
+ // Bare string/number/boolean — direct Map subscript.
635
663
  whereStatement.append(
636
664
  SQL`AND ${key}[${{
637
665
  value: mapKey,
@@ -1,6 +1,7 @@
1
1
  import Alert from "../../../Models/DatabaseModels/Alert";
2
2
  import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
3
3
  import AlertStateTimeline from "../../../Models/DatabaseModels/AlertStateTimeline";
4
+ import Host from "../../../Models/DatabaseModels/Host";
4
5
  import Label from "../../../Models/DatabaseModels/Label";
5
6
  import Monitor from "../../../Models/DatabaseModels/Monitor";
6
7
  import OnCallDutyPolicy from "../../../Models/DatabaseModels/OnCallDutyPolicy";
@@ -17,6 +18,7 @@ import { DisableAutomaticAlertCreation } from "../../EnvironmentConfig";
17
18
  import AlertService from "../../Services/AlertService";
18
19
  import AlertSeverityService from "../../Services/AlertSeverityService";
19
20
  import AlertStateTimelineService from "../../Services/AlertStateTimelineService";
21
+ import HostService from "../../Services/HostService";
20
22
  import logger, { LogAttributes } from "../Logger";
21
23
  import CaptureSpan from "../Telemetry/CaptureSpan";
22
24
  import DataToProcess from "./DataToProcess";
@@ -268,6 +270,35 @@ export default class MonitorAlert {
268
270
  }
269
271
  if (seriesLabels && Object.keys(seriesLabels).length > 0) {
270
272
  alert.seriesLabels = seriesLabels;
273
+
274
+ /*
275
+ * Link the alert to the Host that emitted this series, if
276
+ * the metric carried a `host.name` resource attribute. The
277
+ * Host record was auto-discovered during OTel ingestion.
278
+ */
279
+ const hostName: string | undefined =
280
+ typeof seriesLabels["host.name"] === "string"
281
+ ? (seriesLabels["host.name"] as string)
282
+ : undefined;
283
+
284
+ if (hostName) {
285
+ const host: Host | null = await HostService.findOneBy({
286
+ query: {
287
+ projectId: input.monitor.projectId!,
288
+ hostIdentifier: hostName,
289
+ },
290
+ select: {
291
+ _id: true,
292
+ },
293
+ props: {
294
+ isRoot: true,
295
+ },
296
+ });
297
+
298
+ if (host) {
299
+ alert.hosts = [host];
300
+ }
301
+ }
271
302
  }
272
303
 
273
304
  alert.onCallDutyPolicies =
@@ -1,3 +1,4 @@
1
+ import Host from "../../../Models/DatabaseModels/Host";
1
2
  import Incident from "../../../Models/DatabaseModels/Incident";
2
3
  import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
3
4
  import IncidentStateTimeline from "../../../Models/DatabaseModels/IncidentStateTimeline";
@@ -15,6 +16,7 @@ import ObjectID from "../../../Types/ObjectID";
15
16
  import ProbeMonitorResponse from "../../../Types/Probe/ProbeMonitorResponse";
16
17
  import { TelemetryQuery } from "../../../Types/Telemetry/TelemetryQuery";
17
18
  import { DisableAutomaticIncidentCreation } from "../../EnvironmentConfig";
19
+ import HostService from "../../Services/HostService";
18
20
  import IncidentService from "../../Services/IncidentService";
19
21
  import IncidentSeverityService from "../../Services/IncidentSeverityService";
20
22
  import IncidentStateTimelineService from "../../Services/IncidentStateTimelineService";
@@ -313,6 +315,35 @@ export default class MonitorIncident {
313
315
  }
314
316
  if (seriesLabels && Object.keys(seriesLabels).length > 0) {
315
317
  incident.seriesLabels = seriesLabels;
318
+
319
+ /*
320
+ * Link the incident to the Host that emitted this series, if
321
+ * the metric carried a `host.name` resource attribute. The
322
+ * Host record was auto-discovered during OTel ingestion.
323
+ */
324
+ const hostName: string | undefined =
325
+ typeof seriesLabels["host.name"] === "string"
326
+ ? (seriesLabels["host.name"] as string)
327
+ : undefined;
328
+
329
+ if (hostName) {
330
+ const host: Host | null = await HostService.findOneBy({
331
+ query: {
332
+ projectId: input.monitor.projectId!,
333
+ hostIdentifier: hostName,
334
+ },
335
+ select: {
336
+ _id: true,
337
+ },
338
+ props: {
339
+ isRoot: true,
340
+ },
341
+ });
342
+
343
+ if (host) {
344
+ incident.hosts = [host];
345
+ }
346
+ }
316
347
  }
317
348
 
318
349
  incident.onCallDutyPolicies =
@@ -17,6 +17,67 @@ import vm, { Context } from "vm";
17
17
  */
18
18
  const PROXY_TARGET_SYMBOL: unique symbol = Symbol("sandboxProxyTarget");
19
19
 
20
+ /**
21
+ * Hardening prelude injected before user code in `runCodeInNodeVM`.
22
+ *
23
+ * Node's `vm` module is not a security boundary. The published PoC for
24
+ * GHSA-g9cp-35m2-fjv6 forces a stack-overflow `RangeError`, walks
25
+ * `e.__proto__.__proto__.__proto__` to `Object.prototype`, then reads
26
+ * `.toString.constructor` to obtain a `Function` constructor that compiles
27
+ * code in a realm where `process.binding('spawn_sync')` is reachable.
28
+ *
29
+ * This prelude closes that path by:
30
+ * - severing `Error.prototype`'s link to `Object.prototype` so the 3-level
31
+ * walk lands on `null` instead of `Object.prototype`;
32
+ * - deleting `.constructor` from every built-in prototype, so even a
33
+ * different walk (e.g. `(0).constructor.constructor`) cannot resolve to a
34
+ * function constructor;
35
+ * - clearing `Function` / `eval` from the sandbox global;
36
+ * - freezing the affected prototypes so user code cannot reattach them.
37
+ *
38
+ * This is a hotfix for the public PoC. The durable fix is to drop
39
+ * `runCodeInNodeVM` in favor of running synthetic monitor scripts in an
40
+ * out-of-process sandbox (tracked on the `probe-runner` branch).
41
+ */
42
+ const VM_HARDENING_PRELUDE: string = `(() => {
43
+ const _ctors = [
44
+ Object, Function, Array, String, Number, Boolean, RegExp,
45
+ Error, RangeError, TypeError, SyntaxError, ReferenceError, EvalError, URIError,
46
+ Symbol, Date, Map, Set, WeakMap, WeakSet, Promise, Proxy,
47
+ ArrayBuffer, DataView,
48
+ Int8Array, Uint8Array, Uint8ClampedArray,
49
+ Int16Array, Uint16Array, Int32Array, Uint32Array,
50
+ Float32Array, Float64Array,
51
+ ];
52
+ if (typeof BigInt !== 'undefined') _ctors.push(BigInt);
53
+
54
+ for (const C of _ctors) {
55
+ try { if (C && C.prototype) delete C.prototype.constructor; } catch (_) {}
56
+ }
57
+
58
+ // Generator / async-function prototypes have no named global — reach via syntax.
59
+ try { delete Object.getPrototypeOf(function*(){}).constructor; } catch (_) {}
60
+ try { delete Object.getPrototypeOf(async function(){}).constructor; } catch (_) {}
61
+ try { delete Object.getPrototypeOf(async function*(){}).constructor; } catch (_) {}
62
+
63
+ try { Object.setPrototypeOf(Error.prototype, null); } catch (_) {}
64
+
65
+ try {
66
+ Object.defineProperty(globalThis, 'Function', {
67
+ value: undefined, writable: false, configurable: false,
68
+ });
69
+ } catch (_) {}
70
+ try {
71
+ Object.defineProperty(globalThis, 'eval', {
72
+ value: undefined, writable: false, configurable: false,
73
+ });
74
+ } catch (_) {}
75
+
76
+ for (const C of _ctors) {
77
+ try { if (C && C.prototype) Object.freeze(C.prototype); } catch (_) {}
78
+ }
79
+ })();`;
80
+
20
81
  /** Properties blocked on every host-realm object exposed to the sandbox. */
21
82
  const BLOCKED_SANDBOX_PROPERTIES: ReadonlySet<string> = new Set([
22
83
  "constructor",
@@ -465,6 +526,7 @@ export default class VMRunner {
465
526
  });
466
527
 
467
528
  const script: string = `(async()=>{
529
+ ${VM_HARDENING_PRELUDE}
468
530
  ${code}
469
531
  })()`;
470
532
 
@@ -80,4 +80,29 @@ describe("LogAggregationService", () => {
80
80
  });
81
81
  }).toThrow("Invalid facetKey");
82
82
  });
83
+
84
+ test("histogram attribute filter matches attribute keys case-insensitively", () => {
85
+ /*
86
+ * Users typing `requestid` should still match data stored with the key
87
+ * `requestId` (camelCase). The histogram filter shares the same WHERE
88
+ * clause builder (`appendCommonFilters`) with the list/facet queries, so
89
+ * verifying it on histogram covers all three.
90
+ */
91
+ const statement: Statement = (
92
+ LogAggregationService as any
93
+ ).buildHistogramStatement({
94
+ ...defaultRequest,
95
+ facetKey: undefined,
96
+ attributes: { requestid: "uuid-123" },
97
+ });
98
+
99
+ expect(statement.query).toContain(
100
+ "arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(",
101
+ );
102
+ expect(statement.query).toContain(
103
+ ", mapKeys(attributes), mapValues(attributes))",
104
+ );
105
+ expect(Object.values(statement.query_params)).toContain("requestid");
106
+ expect(Object.values(statement.query_params)).toContain("uuid-123");
107
+ });
83
108
  });