@oneuptime/common 11.2.3 → 11.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/Models/AnalyticsModels/Span.ts +285 -0
  2. package/Server/Infrastructure/Queue.ts +20 -0
  3. package/Server/Services/ProjectService.ts +429 -300
  4. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +26 -4
  5. package/Server/Utils/Monitor/MonitorResource.ts +19 -1
  6. package/Server/Utils/Telemetry/LlmSpan.ts +251 -0
  7. package/Server/Utils/Telemetry.ts +154 -7
  8. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +25 -12
  9. package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +164 -0
  10. package/Tests/Server/Utils/Telemetry.test.ts +251 -0
  11. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +8 -3
  12. package/UI/Components/Navbar/NavBar.tsx +13 -2
  13. package/Utils/Dashboard/Components/DashboardTraceChartComponent.ts +6 -58
  14. package/build/dist/Models/AnalyticsModels/Span.js +231 -0
  15. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  16. package/build/dist/Server/Infrastructure/Queue.js +7 -1
  17. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  18. package/build/dist/Server/Services/ProjectService.js +371 -277
  19. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  20. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +26 -4
  21. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  22. package/build/dist/Server/Utils/Monitor/MonitorResource.js +17 -1
  23. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  24. package/build/dist/Server/Utils/Telemetry/LlmSpan.js +153 -0
  25. package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -0
  26. package/build/dist/Server/Utils/Telemetry.js +128 -7
  27. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  28. package/build/dist/UI/Components/Navbar/NavBar.js +4 -1
  29. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  30. package/build/dist/Utils/Dashboard/Components/DashboardTraceChartComponent.js +6 -49
  31. package/build/dist/Utils/Dashboard/Components/DashboardTraceChartComponent.js.map +1 -1
  32. package/package.json +1 -1
@@ -893,6 +893,192 @@ export default class Span extends AnalyticsBaseModel {
893
893
  },
894
894
  });
895
895
 
896
+ /*
897
+ * First-class LLM / GenAI / AI-agent columns. Denormalized at ingest from
898
+ * gen_ai.* (and OpenLLMetry/OpenInference) span attributes so LLM calls can
899
+ * be listed, filtered, and aggregated (tokens/cost/latency) cheaply without
900
+ * scanning the attributes map. See Common/Server/Utils/Telemetry/LlmSpan.ts.
901
+ */
902
+ const llmColumnAccessControl: {
903
+ read: Array<Permission>;
904
+ create: Array<Permission>;
905
+ update: Array<Permission>;
906
+ } = {
907
+ read: [
908
+ Permission.ProjectOwner,
909
+ Permission.ProjectAdmin,
910
+ Permission.ProjectMember,
911
+ Permission.Viewer,
912
+ Permission.TelemetryAdmin,
913
+ Permission.TelemetryMember,
914
+ Permission.TelemetryViewer,
915
+ Permission.ReadTelemetryServiceTraces,
916
+ ],
917
+ create: [
918
+ Permission.ProjectOwner,
919
+ Permission.ProjectAdmin,
920
+ Permission.ProjectMember,
921
+ Permission.TelemetryAdmin,
922
+ Permission.TelemetryMember,
923
+ Permission.CreateTelemetryServiceTraces,
924
+ ],
925
+ update: [],
926
+ };
927
+
928
+ const isLlmSpanColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
929
+ key: "isLlmSpan",
930
+ title: "Is LLM Span",
931
+ description:
932
+ "Whether this span is an LLM / GenAI / AI-agent operation, populated at ingest time for fast filtering of AI calls",
933
+ required: true,
934
+ defaultValue: false,
935
+ type: TableColumnType.Boolean,
936
+ skipIndex: {
937
+ name: "idx_is_llm_span",
938
+ type: SkipIndexType.Set,
939
+ params: [2],
940
+ granularity: 4,
941
+ },
942
+ accessControl: llmColumnAccessControl,
943
+ });
944
+
945
+ const llmSystemColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
946
+ key: "llmSystem",
947
+ isLowCardinality: true,
948
+ title: "LLM System",
949
+ description:
950
+ "GenAI provider / system, e.g. openai, anthropic, aws.bedrock (gen_ai.system)",
951
+ required: false,
952
+ type: TableColumnType.Text,
953
+ skipIndex: {
954
+ name: "idx_llm_system",
955
+ type: SkipIndexType.Set,
956
+ params: [100],
957
+ granularity: 4,
958
+ },
959
+ accessControl: llmColumnAccessControl,
960
+ });
961
+
962
+ const llmOperationColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
963
+ key: "llmOperation",
964
+ isLowCardinality: true,
965
+ title: "LLM Operation",
966
+ description:
967
+ "GenAI operation, e.g. chat, embeddings, execute_tool, invoke_agent (gen_ai.operation.name)",
968
+ required: false,
969
+ type: TableColumnType.Text,
970
+ skipIndex: {
971
+ name: "idx_llm_operation",
972
+ type: SkipIndexType.Set,
973
+ params: [100],
974
+ granularity: 4,
975
+ },
976
+ accessControl: llmColumnAccessControl,
977
+ });
978
+
979
+ const llmRequestModelColumn: AnalyticsTableColumn =
980
+ new AnalyticsTableColumn({
981
+ key: "llmRequestModel",
982
+ isLowCardinality: true,
983
+ title: "LLM Request Model",
984
+ description: "Model requested by the caller (gen_ai.request.model)",
985
+ required: false,
986
+ type: TableColumnType.Text,
987
+ skipIndex: {
988
+ name: "idx_llm_request_model",
989
+ type: SkipIndexType.Set,
990
+ params: [1000],
991
+ granularity: 4,
992
+ },
993
+ accessControl: llmColumnAccessControl,
994
+ });
995
+
996
+ const llmResponseModelColumn: AnalyticsTableColumn =
997
+ new AnalyticsTableColumn({
998
+ key: "llmResponseModel",
999
+ isLowCardinality: true,
1000
+ title: "LLM Response Model",
1001
+ description:
1002
+ "Model the provider actually served (gen_ai.response.model)",
1003
+ required: false,
1004
+ type: TableColumnType.Text,
1005
+ accessControl: llmColumnAccessControl,
1006
+ });
1007
+
1008
+ const llmAgentNameColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
1009
+ key: "llmAgentName",
1010
+ codec: { codec: "ZSTD", level: 1 },
1011
+ title: "LLM Agent Name",
1012
+ description:
1013
+ "Name of the agent for agent-framework spans (gen_ai.agent.name)",
1014
+ required: false,
1015
+ type: TableColumnType.Text,
1016
+ accessControl: llmColumnAccessControl,
1017
+ });
1018
+
1019
+ const llmToolNameColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
1020
+ key: "llmToolName",
1021
+ codec: { codec: "ZSTD", level: 1 },
1022
+ title: "LLM Tool Name",
1023
+ description:
1024
+ "Name of the tool invoked for tool-call spans (gen_ai.tool.name)",
1025
+ required: false,
1026
+ type: TableColumnType.Text,
1027
+ accessControl: llmColumnAccessControl,
1028
+ });
1029
+
1030
+ const llmInputTokensColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
1031
+ {
1032
+ key: "llmInputTokens",
1033
+ title: "LLM Input Tokens",
1034
+ description: "Input/prompt token count (gen_ai.usage.input_tokens)",
1035
+ required: true,
1036
+ defaultValue: 0,
1037
+ type: TableColumnType.Number,
1038
+ accessControl: llmColumnAccessControl,
1039
+ },
1040
+ );
1041
+
1042
+ const llmOutputTokensColumn: AnalyticsTableColumn =
1043
+ new AnalyticsTableColumn({
1044
+ key: "llmOutputTokens",
1045
+ title: "LLM Output Tokens",
1046
+ description:
1047
+ "Output/completion token count (gen_ai.usage.output_tokens)",
1048
+ required: true,
1049
+ defaultValue: 0,
1050
+ type: TableColumnType.Number,
1051
+ accessControl: llmColumnAccessControl,
1052
+ });
1053
+
1054
+ const llmTotalTokensColumn: AnalyticsTableColumn = new AnalyticsTableColumn(
1055
+ {
1056
+ key: "llmTotalTokens",
1057
+ title: "LLM Total Tokens",
1058
+ description:
1059
+ "Total token count (gen_ai.usage.total_tokens, or input + output)",
1060
+ required: true,
1061
+ defaultValue: 0,
1062
+ type: TableColumnType.Number,
1063
+ accessControl: llmColumnAccessControl,
1064
+ },
1065
+ );
1066
+
1067
+ const llmCostColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
1068
+ key: "llmCost",
1069
+ title: "LLM Cost (USD)",
1070
+ description:
1071
+ "Cost of this LLM call in USD. Only populated when the instrumentation reports it (gen_ai.usage.cost); OneUptime does not compute pricing.",
1072
+ required: true,
1073
+ defaultValue: 0,
1074
+ /*
1075
+ * Decimal (ClickHouse Double) — cost is fractional; an Int column would
1076
+ * reject the fractional JSONEachRow value and poison the whole batch.
1077
+ */
1078
+ type: TableColumnType.Decimal,
1079
+ accessControl: llmColumnAccessControl,
1080
+ });
1081
+
896
1082
  const retentionDateColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
897
1083
  key: "retentionDate",
898
1084
  codec: [{ codec: "DoubleDelta" }, { codec: "ZSTD", level: 1 }],
@@ -974,6 +1160,17 @@ export default class Span extends AnalyticsBaseModel {
974
1160
  kindColumn,
975
1161
  hasExceptionColumn,
976
1162
  isRootSpanColumn,
1163
+ isLlmSpanColumn,
1164
+ llmSystemColumn,
1165
+ llmOperationColumn,
1166
+ llmRequestModelColumn,
1167
+ llmResponseModelColumn,
1168
+ llmAgentNameColumn,
1169
+ llmToolNameColumn,
1170
+ llmInputTokensColumn,
1171
+ llmOutputTokensColumn,
1172
+ llmTotalTokensColumn,
1173
+ llmCostColumn,
977
1174
  retentionDateColumn,
978
1175
  ],
979
1176
  projections: [
@@ -1199,4 +1396,92 @@ export default class Span extends AnalyticsBaseModel {
1199
1396
  public set retentionDate(v: Date | undefined) {
1200
1397
  this.setColumnValue("retentionDate", v);
1201
1398
  }
1399
+
1400
+ public get isLlmSpan(): boolean | undefined {
1401
+ return this.getColumnValue("isLlmSpan") as boolean | undefined;
1402
+ }
1403
+
1404
+ public set isLlmSpan(v: boolean | undefined) {
1405
+ this.setColumnValue("isLlmSpan", v);
1406
+ }
1407
+
1408
+ public get llmSystem(): string | undefined {
1409
+ return this.getColumnValue("llmSystem") as string | undefined;
1410
+ }
1411
+
1412
+ public set llmSystem(v: string | undefined) {
1413
+ this.setColumnValue("llmSystem", v);
1414
+ }
1415
+
1416
+ public get llmOperation(): string | undefined {
1417
+ return this.getColumnValue("llmOperation") as string | undefined;
1418
+ }
1419
+
1420
+ public set llmOperation(v: string | undefined) {
1421
+ this.setColumnValue("llmOperation", v);
1422
+ }
1423
+
1424
+ public get llmRequestModel(): string | undefined {
1425
+ return this.getColumnValue("llmRequestModel") as string | undefined;
1426
+ }
1427
+
1428
+ public set llmRequestModel(v: string | undefined) {
1429
+ this.setColumnValue("llmRequestModel", v);
1430
+ }
1431
+
1432
+ public get llmResponseModel(): string | undefined {
1433
+ return this.getColumnValue("llmResponseModel") as string | undefined;
1434
+ }
1435
+
1436
+ public set llmResponseModel(v: string | undefined) {
1437
+ this.setColumnValue("llmResponseModel", v);
1438
+ }
1439
+
1440
+ public get llmAgentName(): string | undefined {
1441
+ return this.getColumnValue("llmAgentName") as string | undefined;
1442
+ }
1443
+
1444
+ public set llmAgentName(v: string | undefined) {
1445
+ this.setColumnValue("llmAgentName", v);
1446
+ }
1447
+
1448
+ public get llmToolName(): string | undefined {
1449
+ return this.getColumnValue("llmToolName") as string | undefined;
1450
+ }
1451
+
1452
+ public set llmToolName(v: string | undefined) {
1453
+ this.setColumnValue("llmToolName", v);
1454
+ }
1455
+
1456
+ public get llmInputTokens(): number | undefined {
1457
+ return this.getColumnValue("llmInputTokens") as number | undefined;
1458
+ }
1459
+
1460
+ public set llmInputTokens(v: number | undefined) {
1461
+ this.setColumnValue("llmInputTokens", v);
1462
+ }
1463
+
1464
+ public get llmOutputTokens(): number | undefined {
1465
+ return this.getColumnValue("llmOutputTokens") as number | undefined;
1466
+ }
1467
+
1468
+ public set llmOutputTokens(v: number | undefined) {
1469
+ this.setColumnValue("llmOutputTokens", v);
1470
+ }
1471
+
1472
+ public get llmTotalTokens(): number | undefined {
1473
+ return this.getColumnValue("llmTotalTokens") as number | undefined;
1474
+ }
1475
+
1476
+ public set llmTotalTokens(v: number | undefined) {
1477
+ this.setColumnValue("llmTotalTokens", v);
1478
+ }
1479
+
1480
+ public get llmCost(): number | undefined {
1481
+ return this.getColumnValue("llmCost") as number | undefined;
1482
+ }
1483
+
1484
+ public set llmCost(v: number | undefined) {
1485
+ this.setColumnValue("llmCost", v);
1486
+ }
1202
1487
  }
@@ -219,6 +219,19 @@ export default class Queue {
219
219
  * timestamp.
220
220
  */
221
221
  skipExistenceCheck?: boolean | undefined;
222
+ /**
223
+ * Coalesce same-key jobs so they are never processed in parallel.
224
+ * When set, BullMQ keeps at most one active + one waiting job per
225
+ * `deduplication.id`: additional adds while one is active collapse into
226
+ * the single waiting slot, keeping only the latest job data
227
+ * (keepLastIfActive). Used by the incoming-request ingest path to stop
228
+ * an external sender hammering one monitor's URL from fanning out into
229
+ * many concurrent same-monitor jobs that contend on the per-monitor
230
+ * lock. Independent of `jobId`, which stays unique.
231
+ */
232
+ deduplication?:
233
+ | { id: string; keepLastIfActive?: boolean | undefined }
234
+ | undefined;
222
235
  },
223
236
  ): Promise<Job> {
224
237
  const sanitizedJobId: string = this.sanitizeJobId(jobId.toString());
@@ -242,6 +255,13 @@ export default class Queue {
242
255
  };
243
256
  }
244
257
 
258
+ if (options?.deduplication?.id) {
259
+ optionsObject.deduplication = {
260
+ id: this.sanitizeJobId(options.deduplication.id),
261
+ keepLastIfActive: options.deduplication.keepLastIfActive ?? false,
262
+ };
263
+ }
264
+
245
265
  const queue: BullQueue = this.getQueue(queueName);
246
266
 
247
267
  if (options && options.scheduleAt) {