@oneuptime/common 10.0.80 → 10.0.83
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/Metric.ts +296 -2
- package/Server/Services/MetricService.ts +228 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +43 -3
- package/Server/Utils/Express.ts +5 -0
- package/Types/AnalyticsDatabase/TableColumnType.ts +1 -0
- package/Types/BaseDatabase/AggregationType.ts +35 -0
- package/Types/Monitor/IncomingMonitor/IncomingMonitorRequest.ts +1 -0
- package/UI/Components/Banner/Banner.tsx +7 -2
- package/UI/Components/Breadcrumbs/Breadcrumbs.tsx +6 -2
- package/UI/Components/Button/Button.tsx +10 -4
- package/UI/Components/Card/Card.tsx +11 -4
- package/UI/Components/EmptyState/EmptyState.tsx +6 -2
- package/UI/Components/EventItem/EventItem.tsx +9 -5
- package/UI/Components/Modal/ConfirmModal.tsx +5 -1
- package/UI/Components/Modal/Modal.tsx +21 -5
- package/UI/Components/ModelTable/BaseModelTable.tsx +7 -1
- package/UI/Components/Navbar/NavBar.tsx +6 -3
- package/UI/Components/Page/Page.tsx +6 -2
- package/UI/Components/SideMenu/SideMenu.tsx +18 -6
- package/UI/Components/SideMenu/SideMenuItem.tsx +9 -2
- package/UI/Components/SideMenu/SideMenuSection.tsx +4 -1
- package/UI/Utils/Translation.tsx +53 -0
- package/UI/esbuild-config.js +8 -0
- package/build/dist/Models/AnalyticsModels/Metric.js +250 -2
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +183 -2
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +36 -2
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Express.js +3 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/TableColumnType.js +1 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumnType.js.map +1 -1
- package/build/dist/Types/BaseDatabase/AggregationType.js +30 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
- package/build/dist/UI/Components/Banner/Banner.js +6 -2
- package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
- package/build/dist/UI/Components/Breadcrumbs/Breadcrumbs.js +5 -2
- package/build/dist/UI/Components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +10 -4
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/Card/Card.js +6 -2
- package/build/dist/UI/Components/Card/Card.js.map +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js +4 -2
- package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
- package/build/dist/UI/Components/EventItem/EventItem.js +9 -7
- package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
- package/build/dist/UI/Components/Modal/ConfirmModal.js +4 -1
- package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +13 -3
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +7 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +6 -3
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +5 -2
- package/build/dist/UI/Components/Page/Page.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js +12 -6
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js +8 -2
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js +4 -1
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
- package/build/dist/UI/Utils/Translation.js +36 -0
- package/build/dist/UI/Utils/Translation.js.map +1 -0
- package/package.json +3 -1
|
@@ -20,6 +20,7 @@ export enum MetricPointType {
|
|
|
20
20
|
Gauge = "Gauge",
|
|
21
21
|
Histogram = "Histogram",
|
|
22
22
|
ExponentialHistogram = "ExponentialHistogram",
|
|
23
|
+
Summary = "Summary",
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export enum ServiceType {
|
|
@@ -501,8 +502,116 @@ export default class Metric extends AnalyticsBaseModel {
|
|
|
501
502
|
const explicitBoundsColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
|
|
502
503
|
{
|
|
503
504
|
key: "explicitBounds",
|
|
504
|
-
title: "Explicit
|
|
505
|
-
description:
|
|
505
|
+
title: "Explicit Bounds",
|
|
506
|
+
description:
|
|
507
|
+
"Upper bounds (exclusive of the +inf overflow bucket) for each explicit-bucket histogram bucket. Stored as Float64 so sub-integer boundaries (e.g. 0.005, 0.01) survive ingest — the previous Array(Int64) representation silently truncated those to 0.",
|
|
508
|
+
required: true,
|
|
509
|
+
defaultValue: [],
|
|
510
|
+
type: TableColumnType.ArrayDecimal,
|
|
511
|
+
accessControl: {
|
|
512
|
+
read: [
|
|
513
|
+
Permission.ProjectOwner,
|
|
514
|
+
Permission.ProjectAdmin,
|
|
515
|
+
Permission.ProjectMember,
|
|
516
|
+
Permission.ReadTelemetryServiceLog,
|
|
517
|
+
],
|
|
518
|
+
create: [
|
|
519
|
+
Permission.ProjectOwner,
|
|
520
|
+
Permission.ProjectAdmin,
|
|
521
|
+
Permission.ProjectMember,
|
|
522
|
+
Permission.CreateTelemetryServiceLog,
|
|
523
|
+
],
|
|
524
|
+
update: [],
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
/*
|
|
530
|
+
* --- ExponentialHistogram-only columns ----------------------------------
|
|
531
|
+
* These are populated only when metricPointType = ExponentialHistogram.
|
|
532
|
+
* For other metric types they are left at their defaults (0 / []).
|
|
533
|
+
*/
|
|
534
|
+
|
|
535
|
+
const scaleColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
|
|
536
|
+
key: "scale",
|
|
537
|
+
title: "Scale",
|
|
538
|
+
description:
|
|
539
|
+
"ExponentialHistogram resolution. base = 2^(2^-scale); bucket index `i` covers (base^i, base^(i+1)].",
|
|
540
|
+
required: false,
|
|
541
|
+
type: TableColumnType.Number,
|
|
542
|
+
accessControl: {
|
|
543
|
+
read: [
|
|
544
|
+
Permission.ProjectOwner,
|
|
545
|
+
Permission.ProjectAdmin,
|
|
546
|
+
Permission.ProjectMember,
|
|
547
|
+
Permission.ReadTelemetryServiceLog,
|
|
548
|
+
],
|
|
549
|
+
create: [
|
|
550
|
+
Permission.ProjectOwner,
|
|
551
|
+
Permission.ProjectAdmin,
|
|
552
|
+
Permission.ProjectMember,
|
|
553
|
+
Permission.CreateTelemetryServiceLog,
|
|
554
|
+
],
|
|
555
|
+
update: [],
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const zeroCountColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
|
|
560
|
+
key: "zeroCount",
|
|
561
|
+
title: "Zero Count",
|
|
562
|
+
description:
|
|
563
|
+
"ExponentialHistogram count of values within the zero region (|v| <= zeroThreshold).",
|
|
564
|
+
required: false,
|
|
565
|
+
type: TableColumnType.BigNumber,
|
|
566
|
+
accessControl: {
|
|
567
|
+
read: [
|
|
568
|
+
Permission.ProjectOwner,
|
|
569
|
+
Permission.ProjectAdmin,
|
|
570
|
+
Permission.ProjectMember,
|
|
571
|
+
Permission.ReadTelemetryServiceLog,
|
|
572
|
+
],
|
|
573
|
+
create: [
|
|
574
|
+
Permission.ProjectOwner,
|
|
575
|
+
Permission.ProjectAdmin,
|
|
576
|
+
Permission.ProjectMember,
|
|
577
|
+
Permission.CreateTelemetryServiceLog,
|
|
578
|
+
],
|
|
579
|
+
update: [],
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const positiveOffsetColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
|
|
584
|
+
{
|
|
585
|
+
key: "positiveOffset",
|
|
586
|
+
title: "Positive Bucket Offset",
|
|
587
|
+
description:
|
|
588
|
+
"Bucket index of the first entry in positiveBucketCounts (ExponentialHistogram).",
|
|
589
|
+
required: false,
|
|
590
|
+
type: TableColumnType.Number,
|
|
591
|
+
accessControl: {
|
|
592
|
+
read: [
|
|
593
|
+
Permission.ProjectOwner,
|
|
594
|
+
Permission.ProjectAdmin,
|
|
595
|
+
Permission.ProjectMember,
|
|
596
|
+
Permission.ReadTelemetryServiceLog,
|
|
597
|
+
],
|
|
598
|
+
create: [
|
|
599
|
+
Permission.ProjectOwner,
|
|
600
|
+
Permission.ProjectAdmin,
|
|
601
|
+
Permission.ProjectMember,
|
|
602
|
+
Permission.CreateTelemetryServiceLog,
|
|
603
|
+
],
|
|
604
|
+
update: [],
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const positiveBucketCountsColumn: AnalyticsTableColumn =
|
|
610
|
+
new AnalyticsTableColumn({
|
|
611
|
+
key: "positiveBucketCounts",
|
|
612
|
+
title: "Positive Bucket Counts",
|
|
613
|
+
description:
|
|
614
|
+
"Counts for the positive range of an ExponentialHistogram, indexed from positiveOffset.",
|
|
506
615
|
required: true,
|
|
507
616
|
defaultValue: [],
|
|
508
617
|
type: TableColumnType.ArrayBigNumber,
|
|
@@ -521,9 +630,118 @@ export default class Metric extends AnalyticsBaseModel {
|
|
|
521
630
|
],
|
|
522
631
|
update: [],
|
|
523
632
|
},
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const negativeOffsetColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
|
|
636
|
+
{
|
|
637
|
+
key: "negativeOffset",
|
|
638
|
+
title: "Negative Bucket Offset",
|
|
639
|
+
description:
|
|
640
|
+
"Bucket index of the first entry in negativeBucketCounts (ExponentialHistogram).",
|
|
641
|
+
required: false,
|
|
642
|
+
type: TableColumnType.Number,
|
|
643
|
+
accessControl: {
|
|
644
|
+
read: [
|
|
645
|
+
Permission.ProjectOwner,
|
|
646
|
+
Permission.ProjectAdmin,
|
|
647
|
+
Permission.ProjectMember,
|
|
648
|
+
Permission.ReadTelemetryServiceLog,
|
|
649
|
+
],
|
|
650
|
+
create: [
|
|
651
|
+
Permission.ProjectOwner,
|
|
652
|
+
Permission.ProjectAdmin,
|
|
653
|
+
Permission.ProjectMember,
|
|
654
|
+
Permission.CreateTelemetryServiceLog,
|
|
655
|
+
],
|
|
656
|
+
update: [],
|
|
657
|
+
},
|
|
524
658
|
},
|
|
525
659
|
);
|
|
526
660
|
|
|
661
|
+
const negativeBucketCountsColumn: AnalyticsTableColumn =
|
|
662
|
+
new AnalyticsTableColumn({
|
|
663
|
+
key: "negativeBucketCounts",
|
|
664
|
+
title: "Negative Bucket Counts",
|
|
665
|
+
description:
|
|
666
|
+
"Counts for the negative range of an ExponentialHistogram, indexed from negativeOffset.",
|
|
667
|
+
required: true,
|
|
668
|
+
defaultValue: [],
|
|
669
|
+
type: TableColumnType.ArrayBigNumber,
|
|
670
|
+
accessControl: {
|
|
671
|
+
read: [
|
|
672
|
+
Permission.ProjectOwner,
|
|
673
|
+
Permission.ProjectAdmin,
|
|
674
|
+
Permission.ProjectMember,
|
|
675
|
+
Permission.ReadTelemetryServiceLog,
|
|
676
|
+
],
|
|
677
|
+
create: [
|
|
678
|
+
Permission.ProjectOwner,
|
|
679
|
+
Permission.ProjectAdmin,
|
|
680
|
+
Permission.ProjectMember,
|
|
681
|
+
Permission.CreateTelemetryServiceLog,
|
|
682
|
+
],
|
|
683
|
+
update: [],
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
/*
|
|
688
|
+
* --- Summary-only columns -----------------------------------------------
|
|
689
|
+
* Populated only when metricPointType = Summary. Two parallel arrays
|
|
690
|
+
* keyed by index (mirrors the bucketCounts/explicitBounds convention):
|
|
691
|
+
* summaryQuantiles[i] in [0,1], summaryValues[i] is value at that quantile.
|
|
692
|
+
*/
|
|
693
|
+
|
|
694
|
+
const summaryQuantilesColumn: AnalyticsTableColumn =
|
|
695
|
+
new AnalyticsTableColumn({
|
|
696
|
+
key: "summaryQuantiles",
|
|
697
|
+
title: "Summary Quantiles",
|
|
698
|
+
description:
|
|
699
|
+
"Quantile percentages in [0,1] for a Summary metric (parallel to summaryValues).",
|
|
700
|
+
required: true,
|
|
701
|
+
defaultValue: [],
|
|
702
|
+
type: TableColumnType.ArrayDecimal,
|
|
703
|
+
accessControl: {
|
|
704
|
+
read: [
|
|
705
|
+
Permission.ProjectOwner,
|
|
706
|
+
Permission.ProjectAdmin,
|
|
707
|
+
Permission.ProjectMember,
|
|
708
|
+
Permission.ReadTelemetryServiceLog,
|
|
709
|
+
],
|
|
710
|
+
create: [
|
|
711
|
+
Permission.ProjectOwner,
|
|
712
|
+
Permission.ProjectAdmin,
|
|
713
|
+
Permission.ProjectMember,
|
|
714
|
+
Permission.CreateTelemetryServiceLog,
|
|
715
|
+
],
|
|
716
|
+
update: [],
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const summaryValuesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
|
|
721
|
+
key: "summaryValues",
|
|
722
|
+
title: "Summary Values",
|
|
723
|
+
description:
|
|
724
|
+
"Values corresponding to each quantile in summaryQuantiles for a Summary metric.",
|
|
725
|
+
required: true,
|
|
726
|
+
defaultValue: [],
|
|
727
|
+
type: TableColumnType.ArrayDecimal,
|
|
728
|
+
accessControl: {
|
|
729
|
+
read: [
|
|
730
|
+
Permission.ProjectOwner,
|
|
731
|
+
Permission.ProjectAdmin,
|
|
732
|
+
Permission.ProjectMember,
|
|
733
|
+
Permission.ReadTelemetryServiceLog,
|
|
734
|
+
],
|
|
735
|
+
create: [
|
|
736
|
+
Permission.ProjectOwner,
|
|
737
|
+
Permission.ProjectAdmin,
|
|
738
|
+
Permission.ProjectMember,
|
|
739
|
+
Permission.CreateTelemetryServiceLog,
|
|
740
|
+
],
|
|
741
|
+
update: [],
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
|
|
527
745
|
const traceIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
|
|
528
746
|
key: "traceId",
|
|
529
747
|
title: "Trace ID",
|
|
@@ -647,6 +865,14 @@ export default class Metric extends AnalyticsBaseModel {
|
|
|
647
865
|
maxColumn,
|
|
648
866
|
bucketCountsColumn,
|
|
649
867
|
explicitBoundsColumn,
|
|
868
|
+
scaleColumn,
|
|
869
|
+
zeroCountColumn,
|
|
870
|
+
positiveOffsetColumn,
|
|
871
|
+
positiveBucketCountsColumn,
|
|
872
|
+
negativeOffsetColumn,
|
|
873
|
+
negativeBucketCountsColumn,
|
|
874
|
+
summaryQuantilesColumn,
|
|
875
|
+
summaryValuesColumn,
|
|
650
876
|
traceIdColumn,
|
|
651
877
|
spanIdColumn,
|
|
652
878
|
retentionDateColumn,
|
|
@@ -846,4 +1072,72 @@ export default class Metric extends AnalyticsBaseModel {
|
|
|
846
1072
|
public set retentionDate(v: Date | undefined) {
|
|
847
1073
|
this.setColumnValue("retentionDate", v);
|
|
848
1074
|
}
|
|
1075
|
+
|
|
1076
|
+
public get scale(): number | undefined {
|
|
1077
|
+
return this.getColumnValue("scale") as number | undefined;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
public set scale(v: number | undefined) {
|
|
1081
|
+
this.setColumnValue("scale", v);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
public get zeroCount(): number | undefined {
|
|
1085
|
+
return this.getColumnValue("zeroCount") as number | undefined;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
public set zeroCount(v: number | undefined) {
|
|
1089
|
+
this.setColumnValue("zeroCount", v);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
public get positiveOffset(): number | undefined {
|
|
1093
|
+
return this.getColumnValue("positiveOffset") as number | undefined;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
public set positiveOffset(v: number | undefined) {
|
|
1097
|
+
this.setColumnValue("positiveOffset", v);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
public get positiveBucketCounts(): Array<number> | undefined {
|
|
1101
|
+
return this.getColumnValue("positiveBucketCounts") as
|
|
1102
|
+
| Array<number>
|
|
1103
|
+
| undefined;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
public set positiveBucketCounts(v: Array<number> | undefined) {
|
|
1107
|
+
this.setColumnValue("positiveBucketCounts", v);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
public get negativeOffset(): number | undefined {
|
|
1111
|
+
return this.getColumnValue("negativeOffset") as number | undefined;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
public set negativeOffset(v: number | undefined) {
|
|
1115
|
+
this.setColumnValue("negativeOffset", v);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
public get negativeBucketCounts(): Array<number> | undefined {
|
|
1119
|
+
return this.getColumnValue("negativeBucketCounts") as
|
|
1120
|
+
| Array<number>
|
|
1121
|
+
| undefined;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
public set negativeBucketCounts(v: Array<number> | undefined) {
|
|
1125
|
+
this.setColumnValue("negativeBucketCounts", v);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
public get summaryQuantiles(): Array<number> | undefined {
|
|
1129
|
+
return this.getColumnValue("summaryQuantiles") as Array<number> | undefined;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
public set summaryQuantiles(v: Array<number> | undefined) {
|
|
1133
|
+
this.setColumnValue("summaryQuantiles", v);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
public get summaryValues(): Array<number> | undefined {
|
|
1137
|
+
return this.getColumnValue("summaryValues") as Array<number> | undefined;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
public set summaryValues(v: Array<number> | undefined) {
|
|
1141
|
+
this.setColumnValue("summaryValues", v);
|
|
1142
|
+
}
|
|
849
1143
|
}
|
|
@@ -1,10 +1,235 @@
|
|
|
1
1
|
import ClickhouseDatabase from "../Infrastructure/ClickhouseDatabase";
|
|
2
2
|
import AnalyticsDatabaseService from "./AnalyticsDatabaseService";
|
|
3
|
-
import
|
|
3
|
+
import Metric from "../../Models/AnalyticsModels/Metric";
|
|
4
|
+
import AggregateBy, {
|
|
5
|
+
AggregateUtil,
|
|
6
|
+
} from "../Types/AnalyticsDatabase/AggregateBy";
|
|
7
|
+
import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
|
|
8
|
+
import {
|
|
9
|
+
getPercentileLevel,
|
|
10
|
+
isPercentileAggregation,
|
|
11
|
+
} from "../../Types/BaseDatabase/AggregationType";
|
|
12
|
+
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
|
|
13
|
+
import logger, { LogAttributes } from "../Utils/Logger";
|
|
4
14
|
|
|
5
|
-
export class MetricService extends AnalyticsDatabaseService<
|
|
15
|
+
export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
6
16
|
public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
|
|
7
|
-
super({ modelType:
|
|
17
|
+
super({ modelType: Metric, database: clickhouseDatabase });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Histogram-aware aggregation override.
|
|
22
|
+
*
|
|
23
|
+
* For non-percentile aggregations (Sum/Avg/Min/Max/Count) we delegate
|
|
24
|
+
* to the base implementation. For percentile aggregations
|
|
25
|
+
* (P50/P90/P95/P99) we build a subquery that fans each metric row out
|
|
26
|
+
* into one or more `(midpoint, weight)` samples — derived from
|
|
27
|
+
* histogram buckets when present — and then runs
|
|
28
|
+
* `quantileExactWeighted` over the fanned-out distribution. This means
|
|
29
|
+
* a P95 of `http.server.request.duration` returns a real
|
|
30
|
+
* bucket-derived 95th percentile of observed values, not the 95th
|
|
31
|
+
* percentile of per-row `sum`s.
|
|
32
|
+
*
|
|
33
|
+
* Per `metricPointType` the fanout is:
|
|
34
|
+
*
|
|
35
|
+
* Histogram -> one (midpoint, count) per explicit bucket;
|
|
36
|
+
* midpoint = (lower + upper) / 2, with the
|
|
37
|
+
* implicit -inf/+inf buckets approximated
|
|
38
|
+
* against the nearest bound.
|
|
39
|
+
* ExponentialHistogram -> one (geomean, count) per positive bucket;
|
|
40
|
+
* base = 2^(2^-scale); bucket index `k` is
|
|
41
|
+
* `positiveOffset + i - 1` (1-indexed) and
|
|
42
|
+
* we use the geometric midpoint
|
|
43
|
+
* base^(k + 0.5). Negative buckets are
|
|
44
|
+
* currently ignored (rare in practice and
|
|
45
|
+
* would require a separate fanout).
|
|
46
|
+
* Summary -> exactly one sample: the value at the
|
|
47
|
+
* stored quantile closest to (and >=) the
|
|
48
|
+
* target `p`, weighted 1; falls back to the
|
|
49
|
+
* highest stored quantile when nothing
|
|
50
|
+
* covers `p`.
|
|
51
|
+
* Sum / Gauge / unknown -> raw `value` weighted 1 (same as the
|
|
52
|
+
* generic `quantile(p)(value)` path).
|
|
53
|
+
*/
|
|
54
|
+
public override toAggregateStatement(aggregateBy: AggregateBy<Metric>): {
|
|
55
|
+
statement: Statement;
|
|
56
|
+
columns: Array<string>;
|
|
57
|
+
} {
|
|
58
|
+
if (!isPercentileAggregation(aggregateBy.aggregationType)) {
|
|
59
|
+
return super.toAggregateStatement(aggregateBy);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const percentileLevel: number | null = getPercentileLevel(
|
|
63
|
+
aggregateBy.aggregationType,
|
|
64
|
+
);
|
|
65
|
+
if (percentileLevel === null) {
|
|
66
|
+
return super.toAggregateStatement(aggregateBy);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!this.database) {
|
|
70
|
+
this.useDefaultDatabase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const databaseName: string = this.database.getDatasourceOptions().database!;
|
|
74
|
+
|
|
75
|
+
const aggregationColumn: string =
|
|
76
|
+
aggregateBy.aggregateColumnName.toString();
|
|
77
|
+
const aggregationTimestampColumn: string =
|
|
78
|
+
aggregateBy.aggregationTimestampColumnName.toString();
|
|
79
|
+
const aggregationInterval: string = AggregateUtil.getAggregationInterval({
|
|
80
|
+
startDate: aggregateBy.startTimestamp!,
|
|
81
|
+
endDate: aggregateBy.endTimestamp!,
|
|
82
|
+
}).toLowerCase();
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* Group-by columns from the caller need to be carried through the
|
|
86
|
+
* inner subquery so the outer GROUP BY can reference them. Only
|
|
87
|
+
* columns that exist on the model are accepted (matches the base
|
|
88
|
+
* generator's safety net).
|
|
89
|
+
*/
|
|
90
|
+
const groupByKeys: Array<string> = [];
|
|
91
|
+
if (aggregateBy.groupBy) {
|
|
92
|
+
for (const key of Object.keys(aggregateBy.groupBy)) {
|
|
93
|
+
if (!this.model.getTableColumn(key)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
groupByKeys.push(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const whereStatement: Statement = this.statementGenerator.toWhereStatement(
|
|
101
|
+
aggregateBy.query,
|
|
102
|
+
);
|
|
103
|
+
const sortStatement: Statement = this.statementGenerator.toSortStatement(
|
|
104
|
+
aggregateBy.sort!,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* Per-row fanout. The result of multiIf is `Array(Tuple(Float64,
|
|
109
|
+
* Float64))` — element 1 is the sample midpoint, element 2 is the
|
|
110
|
+
* weight (rounded to UInt64 in the outer SELECT for
|
|
111
|
+
* quantileExactWeighted). Each branch is guarded by a presence
|
|
112
|
+
* check so a row missing its expected payload (e.g. a zero-bucket
|
|
113
|
+
* histogram) silently drops to the scalar fallback rather than
|
|
114
|
+
* exploding.
|
|
115
|
+
*/
|
|
116
|
+
const fanoutExpression: string = `
|
|
117
|
+
multiIf(
|
|
118
|
+
metricPointType = 'ExponentialHistogram' AND notEmpty(positiveBucketCounts),
|
|
119
|
+
arrayMap(
|
|
120
|
+
i -> tuple(
|
|
121
|
+
pow(
|
|
122
|
+
pow(2.0, pow(2.0, -toFloat64(coalesce(scale, 0)))),
|
|
123
|
+
toFloat64(coalesce(positiveOffset, 0)) + toFloat64(i) - 0.5
|
|
124
|
+
),
|
|
125
|
+
toFloat64(positiveBucketCounts[i])
|
|
126
|
+
),
|
|
127
|
+
arrayEnumerate(positiveBucketCounts)
|
|
128
|
+
),
|
|
129
|
+
metricPointType = 'Histogram' AND notEmpty(bucketCounts),
|
|
130
|
+
arrayMap(
|
|
131
|
+
i -> tuple(
|
|
132
|
+
multiIf(
|
|
133
|
+
length(explicitBounds) = 0,
|
|
134
|
+
toFloat64(coalesce(value, sum, 0)),
|
|
135
|
+
i = 1,
|
|
136
|
+
toFloat64(explicitBounds[1]) / 2.0,
|
|
137
|
+
i > length(explicitBounds),
|
|
138
|
+
toFloat64(explicitBounds[length(explicitBounds)]) * 1.5,
|
|
139
|
+
(toFloat64(explicitBounds[i - 1]) + toFloat64(explicitBounds[i])) / 2.0
|
|
140
|
+
),
|
|
141
|
+
toFloat64(bucketCounts[i])
|
|
142
|
+
),
|
|
143
|
+
arrayEnumerate(bucketCounts)
|
|
144
|
+
),
|
|
145
|
+
metricPointType = 'Summary' AND notEmpty(summaryValues),
|
|
146
|
+
[tuple(
|
|
147
|
+
if(
|
|
148
|
+
arrayFirstIndex(q -> q >= ${percentileLevel}, summaryQuantiles) > 0,
|
|
149
|
+
summaryValues[arrayFirstIndex(q -> q >= ${percentileLevel}, summaryQuantiles)],
|
|
150
|
+
summaryValues[length(summaryValues)]
|
|
151
|
+
),
|
|
152
|
+
1.0
|
|
153
|
+
)],
|
|
154
|
+
[tuple(toFloat64(coalesce(value, sum, 0)), 1.0)]
|
|
155
|
+
)
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
/*
|
|
159
|
+
* Inner subquery: keeps the row's timestamp and any group-by
|
|
160
|
+
* columns, then fans the row into per-sample rows via arrayJoin.
|
|
161
|
+
* We use `__pcl_pair` so the column name doesn't collide with any
|
|
162
|
+
* model column should ClickHouse ever surface it through a tooling
|
|
163
|
+
* layer.
|
|
164
|
+
*/
|
|
165
|
+
const innerSelectColumns: Array<string> = [aggregationTimestampColumn];
|
|
166
|
+
for (const key of groupByKeys) {
|
|
167
|
+
if (!innerSelectColumns.includes(key)) {
|
|
168
|
+
innerSelectColumns.push(key);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const innerSelectClause: string = `${innerSelectColumns.join(", ")}, arrayJoin(${fanoutExpression}) AS __pcl_pair`;
|
|
173
|
+
|
|
174
|
+
const statement: Statement = SQL``;
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* Outer SELECT: time bucket + weighted quantile + carry-forward
|
|
178
|
+
* group-by columns. Quantile weight must be UInt for
|
|
179
|
+
* quantileExactWeighted; we round to nearest integer (a count of
|
|
180
|
+
* 0.5 rounds to 0 which drops the sample, but bucket counts are
|
|
181
|
+
* always whole numbers in practice).
|
|
182
|
+
*/
|
|
183
|
+
statement.append(
|
|
184
|
+
`SELECT quantileExactWeighted(${percentileLevel})(__pcl_pair.1, toUInt64(greatest(0, round(__pcl_pair.2)))) as ${aggregationColumn}, date_trunc('${aggregationInterval}', toStartOfInterval(${aggregationTimestampColumn}, INTERVAL 1 ${aggregationInterval})) as ${aggregationTimestampColumn}`,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
for (const key of groupByKeys) {
|
|
188
|
+
statement.append(`, ${key}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
statement.append(SQL` FROM (`);
|
|
192
|
+
statement.append(`SELECT ${innerSelectClause}`);
|
|
193
|
+
statement.append(
|
|
194
|
+
` FROM ${databaseName}.${this.model.tableName} WHERE TRUE `,
|
|
195
|
+
);
|
|
196
|
+
statement.append(whereStatement);
|
|
197
|
+
statement.append(SQL`) `);
|
|
198
|
+
|
|
199
|
+
statement.append(SQL` GROUP BY `).append(`${aggregationTimestampColumn}`);
|
|
200
|
+
for (const key of groupByKeys) {
|
|
201
|
+
statement.append(`, ${key}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
statement.append(SQL` ORDER BY `).append(sortStatement);
|
|
205
|
+
|
|
206
|
+
statement.append(
|
|
207
|
+
SQL` LIMIT ${{
|
|
208
|
+
value: Number(aggregateBy.limit),
|
|
209
|
+
type: TableColumnType.Number,
|
|
210
|
+
}}`,
|
|
211
|
+
);
|
|
212
|
+
statement.append(
|
|
213
|
+
SQL` OFFSET ${{
|
|
214
|
+
value: Number(aggregateBy.skip),
|
|
215
|
+
type: TableColumnType.Number,
|
|
216
|
+
}} `,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const columns: Array<string> = [
|
|
220
|
+
aggregationColumn,
|
|
221
|
+
aggregationTimestampColumn,
|
|
222
|
+
...groupByKeys,
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
logger.debug(`${this.model.tableName} Percentile Aggregate Statement`, {
|
|
226
|
+
tableName: this.model.tableName,
|
|
227
|
+
} as LogAttributes);
|
|
228
|
+
logger.debug(statement, {
|
|
229
|
+
tableName: this.model.tableName,
|
|
230
|
+
} as LogAttributes);
|
|
231
|
+
|
|
232
|
+
return { statement, columns };
|
|
8
233
|
}
|
|
9
234
|
}
|
|
10
235
|
|
|
@@ -35,6 +35,7 @@ import AggregateBy, {
|
|
|
35
35
|
AggregateUtil,
|
|
36
36
|
} from "../../Types/AnalyticsDatabase/AggregateBy";
|
|
37
37
|
import CaptureSpan from "../Telemetry/CaptureSpan";
|
|
38
|
+
import { getPercentileLevel } from "../../../Types/BaseDatabase/AggregationType";
|
|
38
39
|
|
|
39
40
|
export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
40
41
|
public model!: TBaseModel;
|
|
@@ -275,6 +276,25 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
275
276
|
.join(", ")}]`;
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
if (column.type === TableColumnType.ArrayDecimal) {
|
|
280
|
+
value = `[${(value as Array<number>)
|
|
281
|
+
.map((v: number) => {
|
|
282
|
+
if (v && typeof v !== "number") {
|
|
283
|
+
v = parseFloat(v);
|
|
284
|
+
return isNaN(v) ? "NULL" : v;
|
|
285
|
+
}
|
|
286
|
+
/*
|
|
287
|
+
* Filter non-finite (NaN/+Inf/-Inf) -> NULL so ClickHouse Float64
|
|
288
|
+
* serialization succeeds (mirrors `toNumberOrNull` in OTLP ingest).
|
|
289
|
+
*/
|
|
290
|
+
if (typeof v === "number" && !Number.isFinite(v)) {
|
|
291
|
+
return "NULL";
|
|
292
|
+
}
|
|
293
|
+
return v;
|
|
294
|
+
})
|
|
295
|
+
.join(", ")}]`;
|
|
296
|
+
}
|
|
297
|
+
|
|
278
298
|
if (column.type === TableColumnType.MapStringString) {
|
|
279
299
|
const mapObj: Record<string, string> = value as Record<string, string>;
|
|
280
300
|
const entries: Array<string> = Object.entries(mapObj)
|
|
@@ -594,19 +614,37 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
594
614
|
/*
|
|
595
615
|
* EXAMPLE:
|
|
596
616
|
* SELECT sum(Metric.value) as avg_value, date_trunc('hour', toStartOfInterval(createdAt, INTERVAL 1 hour)) as createdAt
|
|
617
|
+
*
|
|
618
|
+
* Percentile aggregations (P50/P90/P95/P99) compile to ClickHouse's
|
|
619
|
+
* `quantile(level)(column)`. This is the right thing for scalar
|
|
620
|
+
* columns (Span.duration, Metric.value when the metric is a Sum or
|
|
621
|
+
* Gauge, etc.). MetricService overrides this method when it has
|
|
622
|
+
* histogram bucket data so the percentile is computed from the
|
|
623
|
+
* actual sample distribution rather than from the per-row aggregated
|
|
624
|
+
* value.
|
|
597
625
|
*/
|
|
598
626
|
|
|
599
627
|
const selectStatement: Statement = new Statement();
|
|
600
628
|
|
|
601
|
-
const aggregationMethod: string =
|
|
602
|
-
aggregateBy.aggregationType.toLocaleLowerCase();
|
|
603
629
|
const aggregationInterval: string = AggregateUtil.getAggregationInterval({
|
|
604
630
|
startDate: aggregateBy.startTimestamp!,
|
|
605
631
|
endDate: aggregateBy.endTimestamp!,
|
|
606
632
|
});
|
|
633
|
+
const aggregationColumn: string =
|
|
634
|
+
aggregateBy.aggregateColumnName.toString();
|
|
635
|
+
const aggregationTimestampColumn: string =
|
|
636
|
+
aggregateBy.aggregationTimestampColumnName.toString();
|
|
637
|
+
|
|
638
|
+
const percentileLevel: number | null = getPercentileLevel(
|
|
639
|
+
aggregateBy.aggregationType,
|
|
640
|
+
);
|
|
641
|
+
const aggregationExpression: string =
|
|
642
|
+
percentileLevel !== null
|
|
643
|
+
? `quantile(${percentileLevel})(${aggregationColumn})`
|
|
644
|
+
: `${aggregateBy.aggregationType.toLocaleLowerCase()}(${aggregationColumn})`;
|
|
607
645
|
|
|
608
646
|
selectStatement.append(
|
|
609
|
-
`${
|
|
647
|
+
`${aggregationExpression} as ${aggregationColumn}, date_trunc('${aggregationInterval.toLowerCase()}', toStartOfInterval(${aggregationTimestampColumn}, INTERVAL 1 ${aggregationInterval.toLowerCase()})) as ${aggregationTimestampColumn}`,
|
|
610
648
|
);
|
|
611
649
|
|
|
612
650
|
const columns: Array<string> = [
|
|
@@ -745,6 +783,7 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
745
783
|
"Array(String)": TableColumnType.ArrayText,
|
|
746
784
|
"Array(Int32)": TableColumnType.ArrayNumber,
|
|
747
785
|
"Array(Int64)": TableColumnType.ArrayBigNumber,
|
|
786
|
+
"Array(Float64)": TableColumnType.ArrayDecimal,
|
|
748
787
|
"Map(String, String)": TableColumnType.MapStringString,
|
|
749
788
|
JSON: TableColumnType.JSON, //JSONArray is also JSON
|
|
750
789
|
Bool: TableColumnType.Boolean,
|
|
@@ -766,6 +805,7 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
766
805
|
[TableColumnType.JSONArray]: SQL`String`, // we use JSON as a string because ClickHouse has really good JSON support for string types
|
|
767
806
|
[TableColumnType.ArrayNumber]: SQL`Array(Int32)`,
|
|
768
807
|
[TableColumnType.ArrayBigNumber]: SQL`Array(Int64)`,
|
|
808
|
+
[TableColumnType.ArrayDecimal]: SQL`Array(Float64)`,
|
|
769
809
|
[TableColumnType.ArrayText]: SQL`Array(String)`,
|
|
770
810
|
[TableColumnType.LongNumber]: SQL`Int128`,
|
|
771
811
|
[TableColumnType.BigNumber]: SQL`Int64`,
|
package/Server/Utils/Express.ts
CHANGED
|
@@ -22,6 +22,11 @@ export const ExpressJson: GenericFunction = express.json;
|
|
|
22
22
|
export const ExpressUrlEncoded: GenericFunction = express.urlencoded;
|
|
23
23
|
export const ExpressRaw: GenericFunction = express.raw;
|
|
24
24
|
|
|
25
|
+
export const createExpressApp: () => express.Application =
|
|
26
|
+
(): express.Application => {
|
|
27
|
+
return express();
|
|
28
|
+
};
|
|
29
|
+
|
|
25
30
|
export type ProbeRequest = {
|
|
26
31
|
id: ObjectID;
|
|
27
32
|
};
|