@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.
- package/Models/AnalyticsModels/Log.ts +6 -0
- package/Models/AnalyticsModels/Metric.ts +6 -0
- package/Models/AnalyticsModels/Profile.ts +6 -0
- package/Models/AnalyticsModels/Span.ts +6 -0
- package/Models/DatabaseModels/Alert.ts +52 -0
- package/Models/DatabaseModels/DockerHost.ts +3 -10
- package/Models/DatabaseModels/Host.ts +1015 -0
- package/Models/DatabaseModels/HostOwnerTeam.ts +462 -0
- package/Models/DatabaseModels/HostOwnerUser.ts +461 -0
- package/Models/DatabaseModels/Incident.ts +52 -0
- package/Models/DatabaseModels/Index.ts +6 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +0 -7
- package/Server/Infrastructure/Postgres/SchemaMigrations/1778006035712-AddHostTables.ts +201 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1778013317872-AddHostIpAddresses.ts +15 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1778066346303-WidenHostOsVersionToLongText.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1778070278986-MigrationName.ts +79 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Services/HostOwnerTeamService.ts +10 -0
- package/Server/Services/HostOwnerUserService.ts +10 -0
- package/Server/Services/HostService.ts +227 -0
- package/Server/Services/LogAggregationService.ts +10 -3
- package/Server/Services/MetricService.ts +200 -0
- package/Server/Services/MonitorTemplateService.ts +11 -3
- package/Server/Services/TraceAggregationService.ts +8 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +46 -18
- package/Server/Utils/Monitor/MonitorAlert.ts +31 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +31 -0
- package/Server/Utils/VM/VMRunner.ts +62 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +25 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +145 -0
- package/Types/Metrics/MetricQueryConfigData.ts +9 -0
- package/Types/Permission.ts +134 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +1 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +1 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +15 -8
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +12 -9
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +17 -10
- package/UI/Components/Charts/Line/LineChart.tsx +1 -1
- package/UI/Components/ExpandableText/ExpandableText.tsx +29 -7
- package/UI/Components/JSONTable/JSONTable.tsx +27 -1
- package/UI/Components/LogsViewer/LogsViewer.tsx +3 -0
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +109 -23
- package/UI/Components/LogsViewer/components/LogSearchBar.tsx +11 -4
- package/UI/Components/Navbar/NavBarMenu.tsx +17 -2
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +10 -3
- package/Utils/ValueFormatter.ts +57 -3
- package/build/dist/Models/AnalyticsModels/Log.js +6 -0
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +6 -0
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Profile.js +6 -0
- package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +6 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Alert.js +51 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerHost.js +3 -10
- package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Host.js +1041 -0
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -0
- package/build/dist/Models/DatabaseModels/HostOwnerTeam.js +480 -0
- package/build/dist/Models/DatabaseModels/HostOwnerTeam.js.map +1 -0
- package/build/dist/Models/DatabaseModels/HostOwnerUser.js +479 -0
- package/build/dist/Models/DatabaseModels/HostOwnerUser.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Incident.js +51 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +6 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +0 -7
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778006035712-AddHostTables.js +76 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778006035712-AddHostTables.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778013317872-AddHostIpAddresses.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778013317872-AddHostIpAddresses.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778066346303-WidenHostOsVersionToLongText.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778066346303-WidenHostOsVersionToLongText.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778070278986-MigrationName.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778070278986-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/HostOwnerTeamService.js +9 -0
- package/build/dist/Server/Services/HostOwnerTeamService.js.map +1 -0
- package/build/dist/Server/Services/HostOwnerUserService.js +9 -0
- package/build/dist/Server/Services/HostOwnerUserService.js.map +1 -0
- package/build/dist/Server/Services/HostService.js +206 -0
- package/build/dist/Server/Services/HostService.js.map +1 -0
- package/build/dist/Server/Services/LogAggregationService.js +10 -3
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +160 -0
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +7 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +8 -3
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +46 -18
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +26 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +26 -0
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +61 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Tests/Server/Services/LogAggregationService.test.js +13 -0
- package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
- package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js +123 -0
- package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js.map +1 -1
- package/build/dist/Types/Permission.js +120 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +13 -7
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +11 -9
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +16 -10
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/ExpandableText/ExpandableText.js +10 -5
- package/build/dist/UI/Components/ExpandableText/ExpandableText.js.map +1 -1
- package/build/dist/UI/Components/JSONTable/JSONTable.js +8 -1
- package/build/dist/UI/Components/JSONTable/JSONTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +40 -14
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +10 -4
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenu.js +15 -2
- package/build/dist/UI/Components/Navbar/NavBarMenu.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +10 -3
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
- package/build/dist/Utils/ValueFormatter.js +47 -3
- package/build/dist/Utils/ValueFormatter.js.map +1 -1
- 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,
|
|
20
|
-
*
|
|
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
|
|
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
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
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 ${
|
|
524
|
+
SQL`AND arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
|
|
500
525
|
value: mapKey,
|
|
501
526
|
type: TableColumnType.Text,
|
|
502
|
-
}}
|
|
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 ${
|
|
538
|
+
SQL`AND NOT arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
|
|
514
539
|
value: mapKey,
|
|
515
540
|
type: TableColumnType.Text,
|
|
516
|
-
}}
|
|
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 ${
|
|
552
|
+
SQL`AND arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
|
|
528
553
|
value: mapKey,
|
|
529
554
|
type: TableColumnType.Text,
|
|
530
|
-
}}
|
|
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 ${
|
|
566
|
+
SQL`AND arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
|
|
542
567
|
value: mapKey,
|
|
543
568
|
type: TableColumnType.Text,
|
|
544
|
-
}}
|
|
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
|
|
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 —
|
|
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
|
});
|