@oneuptime/common 10.0.96 → 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/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/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/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/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;
|
|
@@ -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 =
|
|
@@ -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
|
});
|
|
@@ -13,6 +13,13 @@ import AnalyticsTableEngine from "../../../../Types/AnalyticsDatabase/AnalyticsT
|
|
|
13
13
|
import AnalyticsTableColumn from "../../../../Types/AnalyticsDatabase/TableColumn";
|
|
14
14
|
import TableColumnType from "../../../../Types/AnalyticsDatabase/TableColumnType";
|
|
15
15
|
import OneUptimeDate from "../../../../Types/Date";
|
|
16
|
+
import EqualTo from "../../../../Types/BaseDatabase/EqualTo";
|
|
17
|
+
import NotEqual from "../../../../Types/BaseDatabase/NotEqual";
|
|
18
|
+
import IsNull from "../../../../Types/BaseDatabase/IsNull";
|
|
19
|
+
import NotNull from "../../../../Types/BaseDatabase/NotNull";
|
|
20
|
+
import GreaterThan from "../../../../Types/BaseDatabase/GreaterThan";
|
|
21
|
+
import Search from "../../../../Types/BaseDatabase/Search";
|
|
22
|
+
import StartsWith from "../../../../Types/BaseDatabase/StartsWith";
|
|
16
23
|
|
|
17
24
|
function expectStatement(actual: Statement, expected: Statement): void {
|
|
18
25
|
expect(actual.query).toBe(expected.query);
|
|
@@ -179,6 +186,144 @@ describe("StatementGenerator", () => {
|
|
|
179
186
|
p3: OneUptimeDate.toClickhouseDateTime(date),
|
|
180
187
|
});
|
|
181
188
|
});
|
|
189
|
+
|
|
190
|
+
describe("MapStringString columns", () => {
|
|
191
|
+
class MapModel extends AnalyticsBaseModel {
|
|
192
|
+
public constructor() {
|
|
193
|
+
super({
|
|
194
|
+
tableName: "<map-table>",
|
|
195
|
+
singularName: "<singular>",
|
|
196
|
+
pluralName: "<plural>",
|
|
197
|
+
tableColumns: [
|
|
198
|
+
new AnalyticsTableColumn({
|
|
199
|
+
key: "_id",
|
|
200
|
+
title: "<title>",
|
|
201
|
+
description: "<description>",
|
|
202
|
+
required: true,
|
|
203
|
+
type: TableColumnType.ObjectID,
|
|
204
|
+
}),
|
|
205
|
+
new AnalyticsTableColumn({
|
|
206
|
+
key: "attributes",
|
|
207
|
+
title: "<title>",
|
|
208
|
+
description: "<description>",
|
|
209
|
+
required: true,
|
|
210
|
+
defaultValue: {},
|
|
211
|
+
type: TableColumnType.MapStringString,
|
|
212
|
+
}),
|
|
213
|
+
],
|
|
214
|
+
crudApiPath: new Route("route"),
|
|
215
|
+
primaryKeys: ["_id"],
|
|
216
|
+
sortKeys: ["_id"],
|
|
217
|
+
partitionKey: "_id",
|
|
218
|
+
tableEngine: AnalyticsTableEngine.MergeTree,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let mapGenerator: StatementGenerator<MapModel>;
|
|
224
|
+
beforeEach(() => {
|
|
225
|
+
mapGenerator = new StatementGenerator<MapModel>({
|
|
226
|
+
modelType: MapModel,
|
|
227
|
+
database: ClickhouseAppInstance,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("uses direct map subscript for bare-value equality", () => {
|
|
232
|
+
const statement: Statement = mapGenerator.toWhereStatement({
|
|
233
|
+
attributes: { requestId: "uuid-123" },
|
|
234
|
+
} as any);
|
|
235
|
+
/*
|
|
236
|
+
* Programmatic callers pass canonical keys, so bare-value
|
|
237
|
+
* equality compiles to `attributes['k'] = v` — an O(1) Map
|
|
238
|
+
* subscript that the planner can push into PREWHERE. The
|
|
239
|
+
* slower case-insensitive arrayExists form is reserved for
|
|
240
|
+
* the user-typed Search/StartsWith/EndsWith/NotContains
|
|
241
|
+
* operators below.
|
|
242
|
+
*/
|
|
243
|
+
expect(statement.query).toBe(
|
|
244
|
+
"AND {p0:Identifier}[{p1:String}] = {p2:String}",
|
|
245
|
+
);
|
|
246
|
+
expect(statement.query_params).toStrictEqual({
|
|
247
|
+
p0: "attributes",
|
|
248
|
+
p1: "requestId",
|
|
249
|
+
p2: "uuid-123",
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("uses direct map subscript for EqualTo wrapper", () => {
|
|
254
|
+
const statement: Statement = mapGenerator.toWhereStatement({
|
|
255
|
+
attributes: { requestId: new EqualTo("uuid-123") },
|
|
256
|
+
} as any);
|
|
257
|
+
expect(statement.query).toBe(
|
|
258
|
+
"AND {p0:Identifier}[{p1:String}] = {p2:String}",
|
|
259
|
+
);
|
|
260
|
+
expect(statement.query_params).toStrictEqual({
|
|
261
|
+
p0: "attributes",
|
|
262
|
+
p1: "requestId",
|
|
263
|
+
p2: "uuid-123",
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("uses direct map subscript for NotEqual wrapper", () => {
|
|
268
|
+
const statement: Statement = mapGenerator.toWhereStatement({
|
|
269
|
+
attributes: { requestId: new NotEqual("uuid-123") },
|
|
270
|
+
} as any);
|
|
271
|
+
expect(statement.query).toBe(
|
|
272
|
+
"AND {p0:Identifier}[{p1:String}] != {p2:String}",
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("uses mapContains+subscript for IsNull wrapper", () => {
|
|
277
|
+
const statement: Statement = mapGenerator.toWhereStatement({
|
|
278
|
+
attributes: { requestId: new IsNull() },
|
|
279
|
+
} as any);
|
|
280
|
+
expect(statement.query).toBe(
|
|
281
|
+
"AND ((NOT mapContains({p0:Identifier}, {p1:String})) OR {p2:Identifier}[{p3:String}] = '')",
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("uses mapContains+subscript for NotNull wrapper", () => {
|
|
286
|
+
const statement: Statement = mapGenerator.toWhereStatement({
|
|
287
|
+
attributes: { requestId: new NotNull() },
|
|
288
|
+
} as any);
|
|
289
|
+
expect(statement.query).toBe(
|
|
290
|
+
"AND mapContains({p0:Identifier}, {p1:String}) AND {p2:Identifier}[{p3:String}] != ''",
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("uses direct map subscript for numeric GreaterThan wrapper", () => {
|
|
295
|
+
const statement: Statement = mapGenerator.toWhereStatement({
|
|
296
|
+
attributes: { httpStatus: new GreaterThan(500) },
|
|
297
|
+
} as any);
|
|
298
|
+
expect(statement.query).toBe(
|
|
299
|
+
"AND toFloat64OrNull({p0:Identifier}[{p1:String}]) > {p2:Int32}",
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("keeps case-insensitive arrayExists for Search wrapper", () => {
|
|
304
|
+
const statement: Statement = mapGenerator.toWhereStatement({
|
|
305
|
+
attributes: { requestId: new Search("uuid") },
|
|
306
|
+
} as any);
|
|
307
|
+
/*
|
|
308
|
+
* Search comes from the user-typed search bar — keep the
|
|
309
|
+
* case-insensitive ILIKE form so the user doesn't have to
|
|
310
|
+
* remember whether the stored key is `requestId` or
|
|
311
|
+
* `requestid`.
|
|
312
|
+
*/
|
|
313
|
+
expect(statement.query).toContain("arrayExists");
|
|
314
|
+
expect(statement.query).toContain("lowerUTF8");
|
|
315
|
+
expect(statement.query).toContain("ILIKE");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("keeps case-insensitive arrayExists for StartsWith wrapper", () => {
|
|
319
|
+
const statement: Statement = mapGenerator.toWhereStatement({
|
|
320
|
+
attributes: { requestId: new StartsWith("uuid") },
|
|
321
|
+
} as any);
|
|
322
|
+
expect(statement.query).toContain("arrayExists");
|
|
323
|
+
expect(statement.query).toContain("lowerUTF8");
|
|
324
|
+
expect(statement.query).toContain("ILIKE");
|
|
325
|
+
});
|
|
326
|
+
});
|
|
182
327
|
});
|
|
183
328
|
|
|
184
329
|
describe("toSelectStatement", () => {
|
|
@@ -20,4 +20,13 @@ export default interface MetricQueryConfigData {
|
|
|
20
20
|
yAxisValueFormatter?: ((value: number) => string) | undefined;
|
|
21
21
|
warningThreshold?: number | undefined;
|
|
22
22
|
criticalThreshold?: number | undefined;
|
|
23
|
+
/*
|
|
24
|
+
* When true, the post-aggregation series points are transformed into
|
|
25
|
+
* a per-second rate of change: `(value - previousValue) / Δt`. This is
|
|
26
|
+
* the right view for OTel cumulative counters (e.g. `system.disk.io`,
|
|
27
|
+
* `system.network.io`) — without it, the chart plots monotonically
|
|
28
|
+
* growing bytes-since-process-start. Negative deltas (counter resets
|
|
29
|
+
* on agent restart) are clamped to 0.
|
|
30
|
+
*/
|
|
31
|
+
transformAsRate?: boolean | undefined;
|
|
23
32
|
}
|