@oneuptime/common 11.5.1 → 11.5.3
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/Server/Infrastructure/Postgres/SchemaMigrations/1783937343400-MigrationName.ts +1129 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +3 -1
- package/Server/Services/AnalyticsDatabaseService.ts +59 -7
- package/Server/Services/MetricService.ts +124 -28
- package/Server/Services/SpanService.ts +11 -0
- package/Server/Types/AnalyticsDatabase/AggregateBy.ts +39 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +8 -5
- package/Server/Utils/Telemetry/LlmSpan.ts +38 -64
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +84 -0
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +167 -0
- package/Tests/Types/BaseDatabase/AggregationIntervalUtil.test.ts +40 -0
- package/Types/BaseDatabase/AggregateBy.ts +11 -1
- package/Types/BaseDatabase/AggregationInterval.ts +10 -0
- package/Types/BaseDatabase/AggregationIntervalUtil.ts +23 -0
- package/Types/Metrics/MetricFormulaConfigData.ts +5 -0
- package/Types/Metrics/MetricQueryConfigData.ts +19 -0
- package/Types/Telemetry/LlmConventions.ts +182 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +15 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +16 -2
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +31 -32
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +41 -17
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +38 -34
- package/UI/Components/Charts/ChartLibrary/SparkChart/SparkChart.tsx +4 -3
- package/UI/Components/Charts/ChartLibrary/Utils/ChartColors.ts +48 -10
- package/UI/Components/Charts/Line/LineChart.tsx +15 -2
- package/UI/Components/Detail/PlaceholderText.tsx +1 -14
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783937343400-MigrationName.js +384 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783937343400-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +3 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +43 -5
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +110 -23
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/SpanService.js +10 -0
- package/build/dist/Server/Services/SpanService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js +32 -0
- package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +2 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js +14 -54
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
- package/build/dist/Types/BaseDatabase/AggregationInterval.js +10 -0
- package/build/dist/Types/BaseDatabase/AggregationInterval.js.map +1 -1
- package/build/dist/Types/BaseDatabase/AggregationIntervalUtil.js +13 -0
- package/build/dist/Types/BaseDatabase/AggregationIntervalUtil.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmConventions.js +149 -0
- package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +3 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +3 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +13 -9
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +19 -7
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +21 -9
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/SparkChart/SparkChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/ChartColors.js +26 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/ChartColors.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +3 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/PlaceholderText.js +1 -4
- package/build/dist/UI/Components/Detail/PlaceholderText.js.map +1 -1
- package/package.json +1 -1
|
@@ -433,6 +433,7 @@ import { AddTelemetryEntityRelationshipMetrics1783762505482 } from "./1783762505
|
|
|
433
433
|
import { AddIoTDeviceCredentialTable1783780000000 } from "./1783780000000-AddIoTDeviceCredentialTable";
|
|
434
434
|
import { AddSnmpV3AuthColumnsToNetworkDevice1783790000000 } from "./1783790000000-AddSnmpV3AuthColumnsToNetworkDevice";
|
|
435
435
|
import { AddAdditionalParamsToLlmProvider1783650000000 } from "./1783650000000-AddAdditionalParamsToLlmProvider";
|
|
436
|
+
import { MigrationName1783937343400 } from "./1783937343400-MigrationName";
|
|
436
437
|
|
|
437
438
|
export default [
|
|
438
439
|
InitialMigration,
|
|
@@ -869,5 +870,6 @@ export default [
|
|
|
869
870
|
AddTelemetryEntityRelationshipMetrics1783762505482,
|
|
870
871
|
AddIoTDeviceCredentialTable1783780000000,
|
|
871
872
|
AddSnmpV3AuthColumnsToNetworkDevice1783790000000,
|
|
872
|
-
AddAdditionalParamsToLlmProvider1783650000000
|
|
873
|
+
AddAdditionalParamsToLlmProvider1783650000000,
|
|
874
|
+
MigrationName1783937343400,
|
|
873
875
|
];
|
|
@@ -64,7 +64,10 @@ import Text from "../../Types/Text";
|
|
|
64
64
|
import Typeof from "../../Types/Typeof";
|
|
65
65
|
import API from "../../Utils/API";
|
|
66
66
|
import { Stream } from "node:stream";
|
|
67
|
-
import AggregateBy
|
|
67
|
+
import AggregateBy, {
|
|
68
|
+
AggregateUtil,
|
|
69
|
+
} from "../Types/AnalyticsDatabase/AggregateBy";
|
|
70
|
+
import AggregationInterval from "../../Types/BaseDatabase/AggregationInterval";
|
|
68
71
|
import AggregatedResult from "../../Types/BaseDatabase/AggregatedResult";
|
|
69
72
|
import AggregationType from "../../Types/BaseDatabase/AggregationType";
|
|
70
73
|
import Sort from "../Types/AnalyticsDatabase/Sort";
|
|
@@ -744,6 +747,17 @@ export default class AnalyticsDatabaseService<
|
|
|
744
747
|
);
|
|
745
748
|
}
|
|
746
749
|
|
|
750
|
+
if (
|
|
751
|
+
aggregateBy.aggregationInterval !== undefined &&
|
|
752
|
+
!Object.values(AggregationInterval).includes(
|
|
753
|
+
aggregateBy.aggregationInterval,
|
|
754
|
+
)
|
|
755
|
+
) {
|
|
756
|
+
throw new BadDataException(
|
|
757
|
+
`Invalid aggregationInterval: ${aggregateBy.aggregationInterval}. Allowed values: ${Object.values(AggregationInterval).join(", ")}`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
747
761
|
if (!aggregateBy.aggregationTimestampColumnName) {
|
|
748
762
|
throw new BadDataException(
|
|
749
763
|
"aggregationTimestampColumnName is required",
|
|
@@ -1224,18 +1238,56 @@ export default class AnalyticsDatabaseService<
|
|
|
1224
1238
|
.append(whereStatement)
|
|
1225
1239
|
.append(this.getRetentionReadFilter());
|
|
1226
1240
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1241
|
+
/*
|
|
1242
|
+
* The time bucket is a grouping key only when we ARE bucketing by
|
|
1243
|
+
* time. For a `Total` (whole-window) aggregation the timestamp is
|
|
1244
|
+
* emitted as `min(...)` — an aggregate, not a group key — so it must
|
|
1245
|
+
* be left out of GROUP BY. When there is no group-by column either,
|
|
1246
|
+
* the GROUP BY clause is omitted entirely (a single global row).
|
|
1247
|
+
*/
|
|
1248
|
+
const resolvedInterval: AggregationInterval =
|
|
1249
|
+
AggregateUtil.getAggregationInterval({
|
|
1250
|
+
startDate: aggregateBy.startTimestamp!,
|
|
1251
|
+
endDate: aggregateBy.endTimestamp!,
|
|
1252
|
+
aggregationInterval: aggregateBy.aggregationInterval,
|
|
1253
|
+
});
|
|
1254
|
+
const bucketByTime: boolean =
|
|
1255
|
+
!AggregateUtil.isTotalAggregation(resolvedInterval);
|
|
1256
|
+
const hasGroupBy: boolean = Boolean(
|
|
1257
|
+
aggregateBy.groupBy && Object.keys(aggregateBy.groupBy).length > 0,
|
|
1258
|
+
);
|
|
1230
1259
|
|
|
1231
|
-
if (
|
|
1260
|
+
if (bucketByTime) {
|
|
1261
|
+
statement
|
|
1262
|
+
.append(SQL` GROUP BY `)
|
|
1263
|
+
.append(`${aggregateBy.aggregationTimestampColumnName.toString()}`);
|
|
1264
|
+
if (hasGroupBy) {
|
|
1265
|
+
statement
|
|
1266
|
+
.append(SQL` , `)
|
|
1267
|
+
.append(
|
|
1268
|
+
this.statementGenerator.toGroupByStatement(aggregateBy.groupBy!),
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
} else if (hasGroupBy) {
|
|
1232
1272
|
statement
|
|
1233
|
-
.append(SQL`
|
|
1273
|
+
.append(SQL` GROUP BY `)
|
|
1234
1274
|
.append(
|
|
1235
|
-
this.statementGenerator.toGroupByStatement(aggregateBy.groupBy),
|
|
1275
|
+
this.statementGenerator.toGroupByStatement(aggregateBy.groupBy!),
|
|
1236
1276
|
);
|
|
1237
1277
|
}
|
|
1238
1278
|
|
|
1279
|
+
/*
|
|
1280
|
+
* A group-less Total aggregation (`SELECT agg(...), min(ts) ...` with no
|
|
1281
|
+
* GROUP BY) returns exactly one row even over an empty window —
|
|
1282
|
+
* ClickHouse fills the aggregates with type defaults, so min(ts)
|
|
1283
|
+
* surfaces as a 1970 epoch point. Suppress that phantom row. This is a
|
|
1284
|
+
* no-op for every bucketed/grouped query (they only ever emit rows for
|
|
1285
|
+
* groups that actually have data).
|
|
1286
|
+
*/
|
|
1287
|
+
if (!bucketByTime && !hasGroupBy) {
|
|
1288
|
+
statement.append(SQL` HAVING count() > 0`);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1239
1291
|
statement.append(SQL` ORDER BY `).append(sortStatement);
|
|
1240
1292
|
|
|
1241
1293
|
statement.append(
|
|
@@ -560,10 +560,14 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
560
560
|
aggregateBy.aggregateColumnName.toString();
|
|
561
561
|
const aggregationTimestampColumn: string =
|
|
562
562
|
aggregateBy.aggregationTimestampColumnName.toString();
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
563
|
+
const resolvedInterval: AggregationInterval =
|
|
564
|
+
AggregateUtil.getAggregationInterval({
|
|
565
|
+
startDate: aggregateBy.startTimestamp!,
|
|
566
|
+
endDate: aggregateBy.endTimestamp!,
|
|
567
|
+
aggregationInterval: aggregateBy.aggregationInterval,
|
|
568
|
+
});
|
|
569
|
+
const bucketByTime: boolean =
|
|
570
|
+
!AggregateUtil.isTotalAggregation(resolvedInterval);
|
|
567
571
|
|
|
568
572
|
/*
|
|
569
573
|
* Group-by columns from the caller need to be carried through the
|
|
@@ -671,7 +675,7 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
671
675
|
* always whole numbers in practice).
|
|
672
676
|
*/
|
|
673
677
|
statement.append(
|
|
674
|
-
`SELECT quantileExactWeighted(${percentileLevel})(__pcl_pair.1, toUInt64(greatest(0, round(__pcl_pair.2)))) as ${aggregationColumn},
|
|
678
|
+
`SELECT quantileExactWeighted(${percentileLevel})(__pcl_pair.1, toUInt64(greatest(0, round(__pcl_pair.2)))) as ${aggregationColumn}, ${AggregateUtil.buildBucketTimestampSelect(resolvedInterval, aggregationTimestampColumn)}`,
|
|
675
679
|
);
|
|
676
680
|
|
|
677
681
|
for (const key of groupByKeys) {
|
|
@@ -699,13 +703,33 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
699
703
|
statement.append(this.getRetentionReadFilter());
|
|
700
704
|
statement.append(SQL`) `);
|
|
701
705
|
|
|
702
|
-
|
|
706
|
+
/*
|
|
707
|
+
* `Total` collapses the window into one row per group: the timestamp
|
|
708
|
+
* becomes an aggregate (`min(...)`) and drops out of GROUP BY, and
|
|
709
|
+
* when nothing else is grouped the clause is omitted entirely.
|
|
710
|
+
*/
|
|
711
|
+
const percentileGroupByTerms: Array<string> = [];
|
|
712
|
+
if (bucketByTime) {
|
|
713
|
+
percentileGroupByTerms.push(aggregationTimestampColumn);
|
|
714
|
+
}
|
|
703
715
|
for (const key of groupByKeys) {
|
|
704
|
-
|
|
716
|
+
percentileGroupByTerms.push(key);
|
|
705
717
|
}
|
|
706
718
|
attributeGroupKeys.forEach((_key: string, index: number) => {
|
|
707
|
-
|
|
719
|
+
percentileGroupByTerms.push(`__attr_grp_${index}`);
|
|
708
720
|
});
|
|
721
|
+
if (percentileGroupByTerms.length > 0) {
|
|
722
|
+
statement
|
|
723
|
+
.append(SQL` GROUP BY `)
|
|
724
|
+
.append(percentileGroupByTerms.join(", "));
|
|
725
|
+
} else {
|
|
726
|
+
/*
|
|
727
|
+
* Group-less Total: suppress the single phantom row an empty window
|
|
728
|
+
* would otherwise return (min(time) → 1970 epoch). Only reachable
|
|
729
|
+
* under Total — a bucketed query always groups by the time column.
|
|
730
|
+
*/
|
|
731
|
+
statement.append(SQL` HAVING count() > 0`);
|
|
732
|
+
}
|
|
709
733
|
|
|
710
734
|
statement.append(SQL` ORDER BY `).append(sortStatement);
|
|
711
735
|
|
|
@@ -832,10 +856,14 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
832
856
|
aggregateBy.aggregateColumnName.toString();
|
|
833
857
|
const aggregationTimestampColumn: string =
|
|
834
858
|
aggregateBy.aggregationTimestampColumnName.toString();
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
859
|
+
const resolvedInterval: AggregationInterval =
|
|
860
|
+
AggregateUtil.getAggregationInterval({
|
|
861
|
+
startDate: aggregateBy.startTimestamp!,
|
|
862
|
+
endDate: aggregateBy.endTimestamp!,
|
|
863
|
+
aggregationInterval: aggregateBy.aggregationInterval,
|
|
864
|
+
});
|
|
865
|
+
const bucketByTime: boolean =
|
|
866
|
+
!AggregateUtil.isTotalAggregation(resolvedInterval);
|
|
839
867
|
|
|
840
868
|
const groupByKeys: Array<string> = [];
|
|
841
869
|
if (aggregateBy.groupBy) {
|
|
@@ -871,7 +899,7 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
871
899
|
const statement: Statement = SQL``;
|
|
872
900
|
|
|
873
901
|
statement.append(
|
|
874
|
-
`SELECT ${aggregationExpression} as ${aggregationColumn},
|
|
902
|
+
`SELECT ${aggregationExpression} as ${aggregationColumn}, ${AggregateUtil.buildBucketTimestampSelect(resolvedInterval, aggregationTimestampColumn)}`,
|
|
875
903
|
);
|
|
876
904
|
for (const key of groupByKeys) {
|
|
877
905
|
statement.append(`, ${key}`);
|
|
@@ -916,13 +944,32 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
916
944
|
statement.append(this.getRetentionReadFilter());
|
|
917
945
|
}
|
|
918
946
|
|
|
919
|
-
|
|
947
|
+
/*
|
|
948
|
+
* `Total` drops the time bucket from GROUP BY (it becomes an
|
|
949
|
+
* aggregate `min(...)`), leaving one row per group — or a single
|
|
950
|
+
* global row when nothing else is grouped, in which case the GROUP
|
|
951
|
+
* BY clause is omitted entirely.
|
|
952
|
+
*/
|
|
953
|
+
const scalarGroupByTerms: Array<string> = [];
|
|
954
|
+
if (bucketByTime) {
|
|
955
|
+
scalarGroupByTerms.push(aggregationTimestampColumn);
|
|
956
|
+
}
|
|
920
957
|
for (const key of groupByKeys) {
|
|
921
|
-
|
|
958
|
+
scalarGroupByTerms.push(key);
|
|
922
959
|
}
|
|
923
960
|
attributeGroupKeys.forEach((_key: string, index: number) => {
|
|
924
|
-
|
|
961
|
+
scalarGroupByTerms.push(`__attr_grp_${index}`);
|
|
925
962
|
});
|
|
963
|
+
if (scalarGroupByTerms.length > 0) {
|
|
964
|
+
statement.append(SQL` GROUP BY `).append(scalarGroupByTerms.join(", "));
|
|
965
|
+
} else {
|
|
966
|
+
/*
|
|
967
|
+
* Group-less Total: suppress the single phantom row an empty window
|
|
968
|
+
* would otherwise return (min(time) → 1970 epoch). Only reachable
|
|
969
|
+
* under Total — a bucketed query always groups by the time column.
|
|
970
|
+
*/
|
|
971
|
+
statement.append(SQL` HAVING count() > 0`);
|
|
972
|
+
}
|
|
926
973
|
|
|
927
974
|
statement.append(SQL` ORDER BY `).append(sortStatement);
|
|
928
975
|
|
|
@@ -1044,18 +1091,54 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1044
1091
|
.append(outerWhereStatement)
|
|
1045
1092
|
.append(SQL` AND retentionDate >= now()`);
|
|
1046
1093
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1094
|
+
/*
|
|
1095
|
+
* Mirror the base builder's Total handling: the SELECT above (shared
|
|
1096
|
+
* toAggregateSelectStatement) already emits `min(time)` for a `Total`
|
|
1097
|
+
* interval, so the time column must NOT appear in GROUP BY — grouping
|
|
1098
|
+
* by an aggregate alias is a ClickHouse error. Omit the clause
|
|
1099
|
+
* entirely when nothing else is grouped.
|
|
1100
|
+
*/
|
|
1101
|
+
const mutableResolvedInterval: AggregationInterval =
|
|
1102
|
+
AggregateUtil.getAggregationInterval({
|
|
1103
|
+
startDate: aggregateBy.startTimestamp!,
|
|
1104
|
+
endDate: aggregateBy.endTimestamp!,
|
|
1105
|
+
aggregationInterval: aggregateBy.aggregationInterval,
|
|
1106
|
+
});
|
|
1107
|
+
const mutableBucketByTime: boolean = !AggregateUtil.isTotalAggregation(
|
|
1108
|
+
mutableResolvedInterval,
|
|
1109
|
+
);
|
|
1110
|
+
const mutableHasGroupBy: boolean = Boolean(
|
|
1111
|
+
aggregateBy.groupBy && Object.keys(aggregateBy.groupBy).length > 0,
|
|
1112
|
+
);
|
|
1050
1113
|
|
|
1051
|
-
if (
|
|
1114
|
+
if (mutableBucketByTime) {
|
|
1115
|
+
statement
|
|
1116
|
+
.append(SQL` GROUP BY `)
|
|
1117
|
+
.append(`${aggregateBy.aggregationTimestampColumnName.toString()}`);
|
|
1118
|
+
if (mutableHasGroupBy) {
|
|
1119
|
+
statement
|
|
1120
|
+
.append(SQL` , `)
|
|
1121
|
+
.append(
|
|
1122
|
+
this.statementGenerator.toGroupByStatement(aggregateBy.groupBy!),
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
} else if (mutableHasGroupBy) {
|
|
1052
1126
|
statement
|
|
1053
|
-
.append(SQL`
|
|
1127
|
+
.append(SQL` GROUP BY `)
|
|
1054
1128
|
.append(
|
|
1055
|
-
this.statementGenerator.toGroupByStatement(aggregateBy.groupBy),
|
|
1129
|
+
this.statementGenerator.toGroupByStatement(aggregateBy.groupBy!),
|
|
1056
1130
|
);
|
|
1057
1131
|
}
|
|
1058
1132
|
|
|
1133
|
+
/*
|
|
1134
|
+
* Group-less Total: suppress the single phantom row an empty window
|
|
1135
|
+
* would otherwise return (min(time) → 1970 epoch). No-op for bucketed
|
|
1136
|
+
* or grouped queries.
|
|
1137
|
+
*/
|
|
1138
|
+
if (!mutableBucketByTime && !mutableHasGroupBy) {
|
|
1139
|
+
statement.append(SQL` HAVING count() > 0`);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1059
1142
|
statement.append(SQL` ORDER BY `).append(sortStatement);
|
|
1060
1143
|
|
|
1061
1144
|
statement.append(
|
|
@@ -1236,14 +1319,18 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1236
1319
|
const interval: AggregationInterval = AggregateUtil.getAggregationInterval({
|
|
1237
1320
|
startDate: aggregateBy.startTimestamp!,
|
|
1238
1321
|
endDate: aggregateBy.endTimestamp!,
|
|
1322
|
+
aggregationInterval: aggregateBy.aggregationInterval,
|
|
1239
1323
|
});
|
|
1240
1324
|
/*
|
|
1241
|
-
* The MV is bucketed at 1 minute, so
|
|
1242
|
-
* (Minute / Hour / Day / Week / Month / Year)
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1325
|
+
* The MV is bucketed at 1 minute, so every time-bucketed interval
|
|
1326
|
+
* (Minute / Hour / Day / Week / Month / Year) is >= MV resolution and
|
|
1327
|
+
* acceptable — the honored interval flows into the date_trunc below.
|
|
1328
|
+
* `Total` (whole-window, no bucketing) is the one exception: fall back
|
|
1329
|
+
* to the raw-table builder, which knows how to collapse the window.
|
|
1245
1330
|
*/
|
|
1246
|
-
|
|
1331
|
+
if (AggregateUtil.isTotalAggregation(interval)) {
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1247
1334
|
|
|
1248
1335
|
const queryRecord: Record<string, unknown> =
|
|
1249
1336
|
(aggregateBy.query as unknown as Record<string, unknown>) || {};
|
|
@@ -1510,8 +1597,17 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
|
|
|
1510
1597
|
const interval: AggregationInterval = AggregateUtil.getAggregationInterval({
|
|
1511
1598
|
startDate: aggregateBy.startTimestamp!,
|
|
1512
1599
|
endDate: aggregateBy.endTimestamp!,
|
|
1600
|
+
aggregationInterval: aggregateBy.aggregationInterval,
|
|
1513
1601
|
});
|
|
1514
|
-
|
|
1602
|
+
/*
|
|
1603
|
+
* `Total` (whole-window, no bucketing) is not served by this
|
|
1604
|
+
* time-bucketed per-host MV — fall back to the raw-table builder.
|
|
1605
|
+
* Every other interval is >= the MV's 1-minute resolution and flows
|
|
1606
|
+
* into the date_trunc below.
|
|
1607
|
+
*/
|
|
1608
|
+
if (AggregateUtil.isTotalAggregation(interval)) {
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1515
1611
|
|
|
1516
1612
|
if (!this.database) {
|
|
1517
1613
|
this.useDefaultDatabase();
|
|
@@ -245,6 +245,17 @@ export class SpanService extends AnalyticsDatabaseService<Span> {
|
|
|
245
245
|
return null;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
/*
|
|
249
|
+
* This projection path derives its bucket interval from the window
|
|
250
|
+
* and buckets by minute; it does not implement the explicit
|
|
251
|
+
* `aggregationInterval` override (nor the `Total` whole-window mode).
|
|
252
|
+
* When a caller pins an interval, fall back to the generic base
|
|
253
|
+
* builder, which honors it.
|
|
254
|
+
*/
|
|
255
|
+
if (aggregateBy.aggregationInterval !== undefined) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
248
259
|
/*
|
|
249
260
|
* Only the aggregates whose states the projection stores. P95 etc.
|
|
250
261
|
* must fall back: the projection has no quantile(0.95) state and
|
|
@@ -16,12 +16,51 @@ export class AggregateUtil {
|
|
|
16
16
|
* code reconstructing the server's bucket grid (heartbeat
|
|
17
17
|
* availability charts) always agrees with the interval the
|
|
18
18
|
* statement generator compiles into `toStartOfInterval(...)`.
|
|
19
|
+
*
|
|
20
|
+
* `aggregationInterval`, when provided, pins the bucket size
|
|
21
|
+
* independent of the window (including `Total` for a single
|
|
22
|
+
* whole-window bucket per group).
|
|
19
23
|
*/
|
|
20
24
|
@CaptureSpan()
|
|
21
25
|
public static getAggregationInterval(data: {
|
|
22
26
|
startDate: Date;
|
|
23
27
|
endDate: Date;
|
|
28
|
+
aggregationInterval?: AggregationInterval | undefined;
|
|
24
29
|
}): AggregationInterval {
|
|
25
30
|
return AggregationIntervalUtil.getAggregationIntervalForWindow(data);
|
|
26
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True when the resolved interval means "no time bucketing" — the
|
|
35
|
+
* entire window collapses into one aggregate per group.
|
|
36
|
+
*/
|
|
37
|
+
public static isTotalAggregation(interval: AggregationInterval): boolean {
|
|
38
|
+
return interval === AggregationInterval.Total;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The SELECT fragment that produces the bucket timestamp column,
|
|
43
|
+
* already aliased to `timestampColumn`. Shared by every aggregate SQL
|
|
44
|
+
* builder so the `Total` (whole-window) shape stays identical across
|
|
45
|
+
* paths:
|
|
46
|
+
*
|
|
47
|
+
* - normal interval → `date_trunc('<i>', toStartOfInterval(col, INTERVAL 1 <i>)) as col`
|
|
48
|
+
* - Total → `min(col) as col` (one row per group; the
|
|
49
|
+
* earliest sample timestamp in the window is the bucket label)
|
|
50
|
+
*
|
|
51
|
+
* `timestampColumn` is a model-validated identifier (see
|
|
52
|
+
* AnalyticsDatabaseService._aggregateBy), so raw interpolation here is
|
|
53
|
+
* consistent with the surrounding builders.
|
|
54
|
+
*/
|
|
55
|
+
public static buildBucketTimestampSelect(
|
|
56
|
+
resolvedInterval: AggregationInterval,
|
|
57
|
+
timestampColumn: string,
|
|
58
|
+
): string {
|
|
59
|
+
if (AggregateUtil.isTotalAggregation(resolvedInterval)) {
|
|
60
|
+
return `min(${timestampColumn}) as ${timestampColumn}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const interval: string = resolvedInterval.toLowerCase();
|
|
64
|
+
return `date_trunc('${interval}', toStartOfInterval(${timestampColumn}, INTERVAL 1 ${interval})) as ${timestampColumn}`;
|
|
65
|
+
}
|
|
27
66
|
}
|
|
@@ -51,6 +51,7 @@ import JSONFunctions from "../../../Types/JSONFunctions";
|
|
|
51
51
|
import AggregateBy, {
|
|
52
52
|
AggregateUtil,
|
|
53
53
|
} from "../../Types/AnalyticsDatabase/AggregateBy";
|
|
54
|
+
import AggregationInterval from "../../../Types/BaseDatabase/AggregationInterval";
|
|
54
55
|
import CaptureSpan from "../Telemetry/CaptureSpan";
|
|
55
56
|
import { getPercentileLevel } from "../../../Types/BaseDatabase/AggregationType";
|
|
56
57
|
|
|
@@ -1194,10 +1195,12 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1194
1195
|
|
|
1195
1196
|
const selectStatement: Statement = new Statement();
|
|
1196
1197
|
|
|
1197
|
-
const aggregationInterval:
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1198
|
+
const aggregationInterval: AggregationInterval =
|
|
1199
|
+
AggregateUtil.getAggregationInterval({
|
|
1200
|
+
startDate: aggregateBy.startTimestamp!,
|
|
1201
|
+
endDate: aggregateBy.endTimestamp!,
|
|
1202
|
+
aggregationInterval: aggregateBy.aggregationInterval,
|
|
1203
|
+
});
|
|
1201
1204
|
const aggregationColumn: string =
|
|
1202
1205
|
aggregateBy.aggregateColumnName.toString();
|
|
1203
1206
|
const aggregationTimestampColumn: string =
|
|
@@ -1212,7 +1215,7 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
1212
1215
|
: `${aggregateBy.aggregationType.toLocaleLowerCase()}(${aggregationColumn})`;
|
|
1213
1216
|
|
|
1214
1217
|
selectStatement.append(
|
|
1215
|
-
`${aggregationExpression} as ${aggregationColumn},
|
|
1218
|
+
`${aggregationExpression} as ${aggregationColumn}, ${AggregateUtil.buildBucketTimestampSelect(aggregationInterval, aggregationTimestampColumn)}`,
|
|
1216
1219
|
);
|
|
1217
1220
|
|
|
1218
1221
|
const columns: Array<string> = [
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import Dictionary from "../../../Types/Dictionary";
|
|
2
|
+
import {
|
|
3
|
+
LlmAgentNameAttributeKeys,
|
|
4
|
+
LlmAttributeNamespacePrefixes,
|
|
5
|
+
LlmCostAttributeKeys,
|
|
6
|
+
LlmInputTokenAttributeKeys,
|
|
7
|
+
LlmOperationAttributeKeys,
|
|
8
|
+
LlmOutputTokenAttributeKeys,
|
|
9
|
+
LlmRequestModelAttributeKeys,
|
|
10
|
+
LlmResponseModelAttributeKeys,
|
|
11
|
+
LlmSystemAttributeKeys,
|
|
12
|
+
LlmToolNameAttributeKeys,
|
|
13
|
+
LlmTotalTokenAttributeKeys,
|
|
14
|
+
} from "../../../Types/Telemetry/LlmConventions";
|
|
2
15
|
import { AttributeType } from "./Telemetry";
|
|
3
16
|
|
|
4
17
|
/*
|
|
@@ -7,11 +20,9 @@ import { AttributeType } from "./Telemetry";
|
|
|
7
20
|
* OneUptime ingests OpenTelemetry spans generically. To make LLM and agent
|
|
8
21
|
* telemetry a first-class signal (filterable lists, token/cost/latency
|
|
9
22
|
* rollups) we denormalize a small set of values out of the span attributes at
|
|
10
|
-
* ingest time.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - OpenLLMetry / Traceloop (gen_ai.* + traceloop.*)
|
|
14
|
-
* - OpenInference / Arize (llm.* + openinference.span.kind)
|
|
23
|
+
* ingest time. The set of recognized attribute keys lives in the shared
|
|
24
|
+
* Common/Types/Telemetry/LlmConventions module so this server-side extractor
|
|
25
|
+
* and the client-side display parser cannot drift out of sync.
|
|
15
26
|
*
|
|
16
27
|
* Prompt/completion CONTENT is intentionally NOT denormalized here — it stays
|
|
17
28
|
* in the span's attributes/events map (already captured + scrubbed) and is
|
|
@@ -79,29 +90,19 @@ export default class LlmSpanUtil {
|
|
|
79
90
|
return fields;
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
fields.llmSystem = this.getString(attributes,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
fields.llmRequestModel = this.getString(attributes, [
|
|
96
|
-
"gen_ai.request.model",
|
|
97
|
-
"llm.model_name",
|
|
98
|
-
"llm.request.model",
|
|
99
|
-
]);
|
|
100
|
-
|
|
101
|
-
fields.llmResponseModel = this.getString(attributes, [
|
|
102
|
-
"gen_ai.response.model",
|
|
103
|
-
"llm.response.model",
|
|
104
|
-
]);
|
|
93
|
+
fields.llmSystem = this.getString(attributes, LlmSystemAttributeKeys);
|
|
94
|
+
|
|
95
|
+
fields.llmOperation = this.getString(attributes, LlmOperationAttributeKeys);
|
|
96
|
+
|
|
97
|
+
fields.llmRequestModel = this.getString(
|
|
98
|
+
attributes,
|
|
99
|
+
LlmRequestModelAttributeKeys,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
fields.llmResponseModel = this.getString(
|
|
103
|
+
attributes,
|
|
104
|
+
LlmResponseModelAttributeKeys,
|
|
105
|
+
);
|
|
105
106
|
|
|
106
107
|
// Fall back to the response model when no request model was reported.
|
|
107
108
|
if (!fields.llmRequestModel && fields.llmResponseModel) {
|
|
@@ -114,29 +115,15 @@ export default class LlmSpanUtil {
|
|
|
114
115
|
* reject the row and fail the whole span batch.
|
|
115
116
|
*/
|
|
116
117
|
fields.llmInputTokens = Math.trunc(
|
|
117
|
-
this.getNumber(attributes,
|
|
118
|
-
"gen_ai.usage.input_tokens",
|
|
119
|
-
"gen_ai.usage.prompt_tokens",
|
|
120
|
-
"llm.token_count.prompt",
|
|
121
|
-
"llm.usage.prompt_tokens",
|
|
122
|
-
]),
|
|
118
|
+
this.getNumber(attributes, LlmInputTokenAttributeKeys),
|
|
123
119
|
);
|
|
124
120
|
|
|
125
121
|
fields.llmOutputTokens = Math.trunc(
|
|
126
|
-
this.getNumber(attributes,
|
|
127
|
-
"gen_ai.usage.output_tokens",
|
|
128
|
-
"gen_ai.usage.completion_tokens",
|
|
129
|
-
"llm.token_count.completion",
|
|
130
|
-
"llm.usage.completion_tokens",
|
|
131
|
-
]),
|
|
122
|
+
this.getNumber(attributes, LlmOutputTokenAttributeKeys),
|
|
132
123
|
);
|
|
133
124
|
|
|
134
125
|
fields.llmTotalTokens = Math.trunc(
|
|
135
|
-
this.getNumber(attributes,
|
|
136
|
-
"gen_ai.usage.total_tokens",
|
|
137
|
-
"llm.token_count.total",
|
|
138
|
-
"llm.usage.total_tokens",
|
|
139
|
-
]),
|
|
126
|
+
this.getNumber(attributes, LlmTotalTokenAttributeKeys),
|
|
140
127
|
);
|
|
141
128
|
|
|
142
129
|
// Derive total when only the parts were reported.
|
|
@@ -147,22 +134,11 @@ export default class LlmSpanUtil {
|
|
|
147
134
|
fields.llmTotalTokens = fields.llmInputTokens + fields.llmOutputTokens;
|
|
148
135
|
}
|
|
149
136
|
|
|
150
|
-
fields.llmCost = this.getNumber(attributes,
|
|
151
|
-
"gen_ai.usage.cost",
|
|
152
|
-
"gen_ai.usage.cost_usd",
|
|
153
|
-
"gen_ai.usage.total_cost",
|
|
154
|
-
"llm.usage.total_cost",
|
|
155
|
-
]);
|
|
137
|
+
fields.llmCost = this.getNumber(attributes, LlmCostAttributeKeys);
|
|
156
138
|
|
|
157
|
-
fields.llmAgentName = this.getString(attributes,
|
|
158
|
-
"gen_ai.agent.name",
|
|
159
|
-
"agent.name",
|
|
160
|
-
]);
|
|
139
|
+
fields.llmAgentName = this.getString(attributes, LlmAgentNameAttributeKeys);
|
|
161
140
|
|
|
162
|
-
fields.llmToolName = this.getString(attributes,
|
|
163
|
-
"gen_ai.tool.name",
|
|
164
|
-
"tool.name",
|
|
165
|
-
]);
|
|
141
|
+
fields.llmToolName = this.getString(attributes, LlmToolNameAttributeKeys);
|
|
166
142
|
|
|
167
143
|
fields.isLlmSpan = this.detectIsLlmSpan(keys, fields);
|
|
168
144
|
|
|
@@ -187,11 +163,9 @@ export default class LlmSpanUtil {
|
|
|
187
163
|
|
|
188
164
|
// Last-resort: any GenAI/LLM-namespaced attribute at all.
|
|
189
165
|
return keys.some((key: string) => {
|
|
190
|
-
return (
|
|
191
|
-
key.startsWith(
|
|
192
|
-
|
|
193
|
-
key.startsWith("traceloop.")
|
|
194
|
-
);
|
|
166
|
+
return LlmAttributeNamespacePrefixes.some((prefix: string) => {
|
|
167
|
+
return key.startsWith(prefix);
|
|
168
|
+
});
|
|
195
169
|
});
|
|
196
170
|
}
|
|
197
171
|
|