@oneuptime/common 10.5.8 → 10.5.17

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 (118) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +1 -1
  2. package/Models/AnalyticsModels/Log.ts +1 -1
  3. package/Models/AnalyticsModels/Metric.ts +1 -1
  4. package/Models/AnalyticsModels/Profile.ts +1 -1
  5. package/Models/AnalyticsModels/ProfileSample.ts +1 -1
  6. package/Models/AnalyticsModels/Span.ts +1 -1
  7. package/Models/DatabaseModels/SmsLog.ts +111 -0
  8. package/Models/DatabaseModels/TelemetryException.ts +46 -34
  9. package/Models/DatabaseModels/TelemetryUsageBilling.ts +35 -2
  10. package/Server/API/AIAgentDataAPI.ts +25 -7
  11. package/Server/API/DashboardAPI.ts +616 -0
  12. package/Server/API/TelemetryExceptionAPI.ts +6 -2
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.ts +39 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.ts +28 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.ts +24 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.ts +47 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.ts +34 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  19. package/Server/Infrastructure/QueueWorker.ts +40 -1
  20. package/Server/Services/AnalyticsDatabaseService.ts +87 -0
  21. package/Server/Services/DatabaseService.ts +73 -0
  22. package/Server/Services/TelemetryExceptionService.ts +24 -49
  23. package/Server/Services/TelemetryUsageBillingService.ts +289 -166
  24. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +102 -72
  25. package/Server/Types/Database/Permissions/OwnedScopePermission.ts +81 -60
  26. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +67 -0
  27. package/Server/Utils/Logger.ts +12 -1
  28. package/Server/Utils/StartServer.ts +13 -5
  29. package/Server/Utils/Telemetry/ContextSpanProcessor.ts +48 -0
  30. package/Server/Utils/Telemetry/SpanUtil.ts +16 -35
  31. package/Server/Utils/Telemetry/TelemetryContext.ts +190 -0
  32. package/Server/Utils/Telemetry.ts +18 -2
  33. package/Types/Database/AccessControl/OwnedThrough.ts +31 -3
  34. package/Types/SmsStatus.ts +16 -0
  35. package/Types/Telemetry/ServiceType.ts +10 -0
  36. package/UI/Components/LogsViewer/LogsViewer.tsx +16 -0
  37. package/UI/Utils/Project.ts +6 -0
  38. package/UI/Utils/Telemetry/Telemetry.ts +65 -0
  39. package/UI/Utils/TelemetryService.ts +150 -0
  40. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -1
  41. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  42. package/build/dist/Models/AnalyticsModels/Log.js +1 -1
  43. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  44. package/build/dist/Models/AnalyticsModels/Metric.js +1 -1
  45. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  46. package/build/dist/Models/AnalyticsModels/Profile.js +1 -1
  47. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  48. package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -1
  49. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  50. package/build/dist/Models/AnalyticsModels/Span.js +1 -1
  51. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  52. package/build/dist/Models/DatabaseModels/SmsLog.js +112 -0
  53. package/build/dist/Models/DatabaseModels/SmsLog.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/TelemetryException.js +47 -33
  55. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  56. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js +36 -2
  57. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js.map +1 -1
  58. package/build/dist/Server/API/AIAgentDataAPI.js +24 -8
  59. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  60. package/build/dist/Server/API/DashboardAPI.js +459 -2
  61. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  62. package/build/dist/Server/API/TelemetryExceptionAPI.js +6 -2
  63. package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.js +20 -0
  65. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.js.map +1 -0
  66. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js +23 -0
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js.map +1 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js +19 -0
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js.map +1 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js +22 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js.map +1 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js +25 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js.map +1 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  76. package/build/dist/Server/Infrastructure/QueueWorker.js +31 -1
  77. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  78. package/build/dist/Server/Services/AnalyticsDatabaseService.js +59 -0
  79. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  80. package/build/dist/Server/Services/DatabaseService.js +62 -0
  81. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  82. package/build/dist/Server/Services/TelemetryExceptionService.js +16 -41
  83. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  84. package/build/dist/Server/Services/TelemetryUsageBillingService.js +211 -147
  85. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  86. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +84 -63
  87. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  88. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js +67 -49
  89. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js.map +1 -1
  90. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +51 -0
  91. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  92. package/build/dist/Server/Utils/Logger.js +8 -1
  93. package/build/dist/Server/Utils/Logger.js.map +1 -1
  94. package/build/dist/Server/Utils/StartServer.js +12 -4
  95. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  96. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js +37 -0
  97. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js.map +1 -0
  98. package/build/dist/Server/Utils/Telemetry/SpanUtil.js +15 -24
  99. package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
  100. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js +124 -0
  101. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js.map +1 -0
  102. package/build/dist/Server/Utils/Telemetry.js +12 -1
  103. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  104. package/build/dist/Types/Database/AccessControl/OwnedThrough.js +7 -2
  105. package/build/dist/Types/Database/AccessControl/OwnedThrough.js.map +1 -1
  106. package/build/dist/Types/SmsStatus.js +15 -0
  107. package/build/dist/Types/SmsStatus.js.map +1 -1
  108. package/build/dist/Types/Telemetry/ServiceType.js +10 -0
  109. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  110. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -0
  111. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  112. package/build/dist/UI/Utils/Project.js +5 -0
  113. package/build/dist/UI/Utils/Project.js.map +1 -1
  114. package/build/dist/UI/Utils/Telemetry/Telemetry.js +44 -0
  115. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  116. package/build/dist/UI/Utils/TelemetryService.js +113 -0
  117. package/build/dist/UI/Utils/TelemetryService.js.map +1 -0
  118. package/package.json +2 -2
@@ -0,0 +1,28 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ /*
4
+ * Drop the foreign key from TelemetryUsageBilling.serviceId -> Service.
5
+ *
6
+ * Telemetry ingested without an OTel service.name is metered against a
7
+ * synthetic "unattributed" bucket whose serviceId is the projectId
8
+ * (ServiceType.Unknown). That id has no matching Service row, so the FK
9
+ * would reject the billing row. serviceId stays a required column; it is
10
+ * just no longer constrained to the Service table (it is polymorphic,
11
+ * mirroring the analytics telemetry rows). The TypeORM relation is kept
12
+ * for read-side joins and resolves to null for the unattributed bucket.
13
+ */
14
+ export class MigrationName1780381124553 implements MigrationInterface {
15
+ public name = "MigrationName1780381124553";
16
+
17
+ public async up(queryRunner: QueryRunner): Promise<void> {
18
+ await queryRunner.query(
19
+ `ALTER TABLE "TelemetryUsageBilling" DROP CONSTRAINT IF EXISTS "FK_b9f49cd8318a35757fc843ee900"`,
20
+ );
21
+ }
22
+
23
+ public async down(queryRunner: QueryRunner): Promise<void> {
24
+ await queryRunner.query(
25
+ `ALTER TABLE "TelemetryUsageBilling" ADD CONSTRAINT "FK_b9f49cd8318a35757fc843ee900" FOREIGN KEY ("serviceId") REFERENCES "Service"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
26
+ );
27
+ }
28
+ }
@@ -0,0 +1,24 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ /*
4
+ * Add TelemetryUsageBilling.serviceType — the resource-type discriminator
5
+ * for a usage row (OpenTelemetry / Host / DockerHost / KubernetesCluster /
6
+ * Unknown). Lets the usage breakdown attribute ingestion to the kind of
7
+ * resource that produced it, now that non-Service telemetry is metered.
8
+ * Nullable; legacy rows (all real Services) are treated as OpenTelemetry.
9
+ */
10
+ export class MigrationName1780382837019 implements MigrationInterface {
11
+ public name = "MigrationName1780382837019";
12
+
13
+ public async up(queryRunner: QueryRunner): Promise<void> {
14
+ await queryRunner.query(
15
+ `ALTER TABLE "TelemetryUsageBilling" ADD COLUMN IF NOT EXISTS "serviceType" character varying(100)`,
16
+ );
17
+ }
18
+
19
+ public async down(queryRunner: QueryRunner): Promise<void> {
20
+ await queryRunner.query(
21
+ `ALTER TABLE "TelemetryUsageBilling" DROP COLUMN IF EXISTS "serviceType"`,
22
+ );
23
+ }
24
+ }
@@ -0,0 +1,47 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1780387560604 implements MigrationInterface {
4
+ public name = "MigrationName1780387560604";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "SmsLog" DROP CONSTRAINT "FK_SmsLog_userOnCallLogTimelineId"`,
9
+ );
10
+ await queryRunner.query(
11
+ `DROP INDEX "public"."IDX_SmsLog_userOnCallLogTimelineId"`,
12
+ );
13
+ await queryRunner.query(
14
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
15
+ );
16
+ await queryRunner.query(
17
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
18
+ );
19
+ await queryRunner.query(
20
+ `CREATE INDEX "IDX_77bd357cbaad40a136a36f5f1e" ON "SmsLog" ("userOnCallLogTimelineId") `,
21
+ );
22
+ await queryRunner.query(
23
+ `ALTER TABLE "SmsLog" ADD CONSTRAINT "FK_77bd357cbaad40a136a36f5f1ed" FOREIGN KEY ("userOnCallLogTimelineId") REFERENCES "UserOnCallLogTimeline"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
24
+ );
25
+ }
26
+
27
+ public async down(queryRunner: QueryRunner): Promise<void> {
28
+ await queryRunner.query(
29
+ `ALTER TABLE "SmsLog" DROP CONSTRAINT "FK_77bd357cbaad40a136a36f5f1ed"`,
30
+ );
31
+ await queryRunner.query(
32
+ `DROP INDEX "public"."IDX_77bd357cbaad40a136a36f5f1e"`,
33
+ );
34
+ await queryRunner.query(
35
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
36
+ );
37
+ await queryRunner.query(
38
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
39
+ );
40
+ await queryRunner.query(
41
+ `CREATE INDEX "IDX_SmsLog_userOnCallLogTimelineId" ON "SmsLog" ("userOnCallLogTimelineId") `,
42
+ );
43
+ await queryRunner.query(
44
+ `ALTER TABLE "SmsLog" ADD CONSTRAINT "FK_SmsLog_userOnCallLogTimelineId" FOREIGN KEY ("userOnCallLogTimelineId") REFERENCES "UserOnCallLogTimeline"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,34 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ /*
4
+ * Make TelemetryException.serviceId polymorphic:
5
+ * - Drop the foreign key to Service. An exception's serviceId can be a
6
+ * Service, a Host / DockerHost / KubernetesCluster id, or the projectId
7
+ * for unattributed (Unknown) telemetry — a FK to Service rejected every
8
+ * non-Service exception and forced ingest to skip them.
9
+ * - Add a serviceType discriminator (OpenTelemetry / Host / DockerHost /
10
+ * KubernetesCluster / Unknown) so the Issues UI can attribute each issue
11
+ * to the kind of resource that produced it. Nullable; legacy rows (all
12
+ * real Services) are treated as OpenTelemetry on read.
13
+ */
14
+ export class MigrationName1780388219225 implements MigrationInterface {
15
+ public name = "MigrationName1780388219225";
16
+
17
+ public async up(queryRunner: QueryRunner): Promise<void> {
18
+ await queryRunner.query(
19
+ `ALTER TABLE "TelemetryException" DROP CONSTRAINT IF EXISTS "FK_08a0cfa9f184257b1e57da4cf50"`,
20
+ );
21
+ await queryRunner.query(
22
+ `ALTER TABLE "TelemetryException" ADD COLUMN IF NOT EXISTS "serviceType" character varying(100)`,
23
+ );
24
+ }
25
+
26
+ public async down(queryRunner: QueryRunner): Promise<void> {
27
+ await queryRunner.query(
28
+ `ALTER TABLE "TelemetryException" DROP COLUMN IF EXISTS "serviceType"`,
29
+ );
30
+ await queryRunner.query(
31
+ `ALTER TABLE "TelemetryException" ADD CONSTRAINT "FK_08a0cfa9f184257b1e57da4cf50" FOREIGN KEY ("serviceId") REFERENCES "Service"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
32
+ );
33
+ }
34
+ }
@@ -360,6 +360,11 @@ import { AddLabelGroupByToGroupingRules1779971548393 } from "./1779971548393-Add
360
360
  import { AddTransportTypeToProjectSmtpConfig1779975064262 } from "./1779975064262-AddTransportTypeToProjectSmtpConfig";
361
361
  import { AddSmtpTransportTypeToGlobalConfig1779976190561 } from "./1779976190561-AddSmtpTransportTypeToGlobalConfig";
362
362
  import { MigrationName1779980428744 } from "./1779980428744-MigrationName";
363
+ import { AddDeliveryTrackingToSmsLog1780317745887 } from "./1780317745887-AddDeliveryTrackingToSmsLog";
364
+ import { MigrationName1780381124553 } from "./1780381124553-MigrationName";
365
+ import { MigrationName1780382837019 } from "./1780382837019-MigrationName";
366
+ import { MigrationName1780387560604 } from "./1780387560604-MigrationName";
367
+ import { MigrationName1780388219225 } from "./1780388219225-MigrationName";
363
368
 
364
369
  export default [
365
370
  InitialMigration,
@@ -724,4 +729,9 @@ export default [
724
729
  AddTransportTypeToProjectSmtpConfig1779975064262,
725
730
  AddSmtpTransportTypeToGlobalConfig1779976190561,
726
731
  MigrationName1779980428744,
732
+ AddDeliveryTrackingToSmsLog1780317745887,
733
+ MigrationName1780381124553,
734
+ MigrationName1780382837019,
735
+ MigrationName1780387560604,
736
+ MigrationName1780388219225,
727
737
  ];
@@ -8,6 +8,12 @@ import {
8
8
  import { Worker } from "bullmq";
9
9
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
10
10
  import AppMetrics from "../Utils/Telemetry/AppMetrics";
11
+ import TelemetryContext from "../Utils/Telemetry/TelemetryContext";
12
+ import Telemetry, {
13
+ Span,
14
+ SpanException,
15
+ SpanStatusCode,
16
+ } from "../Utils/Telemetry";
11
17
  import Redis from "./Redis";
12
18
 
13
19
  export default class QueueWorker {
@@ -45,7 +51,40 @@ export default class QueueWorker {
45
51
  let outcome: "success" | "failure" | "timeout" = "success";
46
52
 
47
53
  try {
48
- await onJobInQueue(job);
54
+ /*
55
+ * Seed a telemetry-context scope for this job so every span and log it
56
+ * produces inherits the queue/job name plus any tenant identifiers
57
+ * carried in the job payload (projectId, monitorId, incidentId, ...).
58
+ */
59
+ await TelemetryContext.runWithContext(
60
+ {
61
+ queueName: queueName,
62
+ jobName: job.name || "unknown",
63
+ ...TelemetryContext.pickKnownAttributes(job.data),
64
+ },
65
+ () => {
66
+ /*
67
+ * Wrap the job in an explicit root span so every background job has
68
+ * a consistent, named trace root that carries the seeded context —
69
+ * the @CaptureSpan service calls it makes become children of this.
70
+ */
71
+ return Telemetry.startActiveSpan<Promise<void>>({
72
+ name: `worker.job ${queueName}/${job.name || "unknown"}`,
73
+ fn: async (span: Span): Promise<void> => {
74
+ try {
75
+ await onJobInQueue(job);
76
+ span.setStatus({ code: SpanStatusCode.OK });
77
+ } catch (err) {
78
+ span.recordException(err as SpanException);
79
+ span.setStatus({ code: SpanStatusCode.ERROR });
80
+ throw err;
81
+ } finally {
82
+ span.end();
83
+ }
84
+ },
85
+ });
86
+ },
87
+ );
49
88
  } catch (err) {
50
89
  outcome =
51
90
  err instanceof TimeoutException ||
@@ -366,6 +366,93 @@ export default class AnalyticsDatabaseService<
366
366
  return await this._findBy(findBy);
367
367
  }
368
368
 
369
+ /**
370
+ * Group telemetry rows by (serviceId, serviceType) for a project over a
371
+ * time window, returning the row count and an estimate of the ingested
372
+ * byte size (ClickHouse `byteSize(*)`, the uncompressed in-memory size of
373
+ * each row's columns). This is the enumeration source for usage billing:
374
+ * a single aggregation scan surfaces EVERY resource that emitted
375
+ * telemetry — real Services, Hosts, Docker hosts, Kubernetes clusters,
376
+ * Monitors and unattributed (serviceId = projectId) — without needing a
377
+ * Postgres row per resource. The caller decides which serviceTypes to
378
+ * bill and how to attribute retention.
379
+ */
380
+ @CaptureSpan()
381
+ public async groupTelemetryUsageByService(data: {
382
+ projectId: ObjectID;
383
+ timestampColumnName: keyof TBaseModel | string;
384
+ startDate: Date;
385
+ endDate: Date;
386
+ }): Promise<
387
+ Array<{
388
+ serviceId: string;
389
+ serviceType: string | null;
390
+ rowCount: number;
391
+ estimatedBytes: number;
392
+ }>
393
+ > {
394
+ const timestampColumnName: string = data.timestampColumnName.toString();
395
+
396
+ if (!this.model.getTableColumn(timestampColumnName)) {
397
+ throw new BadDataException(
398
+ `Invalid timestampColumnName: ${timestampColumnName}`,
399
+ );
400
+ }
401
+
402
+ if (!this.database) {
403
+ this.useDefaultDatabase();
404
+ }
405
+ const databaseName: string =
406
+ this.database!.getDatasourceOptions().database!;
407
+
408
+ const statement: Statement = SQL`SELECT serviceId AS serviceId, serviceType AS serviceType, count() AS rowCount, sum(byteSize(*)) AS estimatedBytes FROM ${databaseName}.${this.model.tableName} WHERE projectId = ${{
409
+ type: TableColumnType.ObjectID,
410
+ value: data.projectId,
411
+ }} AND ${timestampColumnName} >= ${{
412
+ type: TableColumnType.DateTime64,
413
+ value: data.startDate,
414
+ }} AND ${timestampColumnName} <= ${{
415
+ type: TableColumnType.DateTime64,
416
+ value: data.endDate,
417
+ }} GROUP BY serviceId, serviceType`;
418
+
419
+ statement.append(
420
+ " SETTINGS max_execution_time = 60, timeout_overflow_mode = 'break'",
421
+ );
422
+
423
+ const dbResult: ResultSet<"JSON"> = await this.executeQuery(statement);
424
+ const responseJSON: ResponseJSON<JSONObject> =
425
+ await dbResult.json<JSONObject>();
426
+ const items: Array<JSONObject> = responseJSON.data ? responseJSON.data : [];
427
+
428
+ const results: Array<{
429
+ serviceId: string;
430
+ serviceType: string | null;
431
+ rowCount: number;
432
+ estimatedBytes: number;
433
+ }> = [];
434
+
435
+ for (const item of items) {
436
+ const serviceId: string = (item["serviceId"] as string) || "";
437
+ if (!serviceId) {
438
+ continue;
439
+ }
440
+ const serviceTypeRaw: unknown = item["serviceType"];
441
+ const serviceType: string | null =
442
+ typeof serviceTypeRaw === "string" && serviceTypeRaw.trim()
443
+ ? serviceTypeRaw
444
+ : null;
445
+ results.push({
446
+ serviceId,
447
+ serviceType,
448
+ rowCount: Number(item["rowCount"]) || 0,
449
+ estimatedBytes: Number(item["estimatedBytes"]) || 0,
450
+ });
451
+ }
452
+
453
+ return results;
454
+ }
455
+
369
456
  @CaptureSpan()
370
457
  public async aggregateBy(
371
458
  aggregateBy: AggregateBy<TBaseModel>,
@@ -54,6 +54,7 @@ import HashedString from "../../Types/HashedString";
54
54
  import { JSONObject, JSONValue } from "../../Types/JSON";
55
55
  import JSONFunctions from "../../Types/JSONFunctions";
56
56
  import ObjectID from "../../Types/ObjectID";
57
+ import TelemetryContext from "../Utils/Telemetry/TelemetryContext";
57
58
  import PositiveNumber from "../../Types/PositiveNumber";
58
59
  import Text from "../../Types/Text";
59
60
  import Typeof from "../../Types/Typeof";
@@ -681,6 +682,69 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
681
682
  }
682
683
  }
683
684
 
685
+ /**
686
+ * Derive the telemetry attribute key for this model's primary id, e.g.
687
+ * `Incident` -> `incidentId`, `Monitor` -> `monitorId`. Matches the keys in
688
+ * TelemetryContextAttributes so dashboards/queries stay consistent.
689
+ */
690
+ private getTelemetryEntityIdKey(): string {
691
+ const name: string = this.modelName || "entity";
692
+ return name.charAt(0).toLowerCase() + name.slice(1) + "Id";
693
+ }
694
+
695
+ /**
696
+ * Seed the ambient telemetry context with the project (tenant) of an
697
+ * operation so worker/cron/service spans and logs — which run outside the
698
+ * HTTP request scope — still carry projectId. Best-effort and safe to call
699
+ * anywhere.
700
+ */
701
+ protected setTelemetryContextFromProps(
702
+ props: DatabaseCommonInteractionProps | undefined,
703
+ ): void {
704
+ try {
705
+ if (props?.tenantId) {
706
+ TelemetryContext.setAttributes({
707
+ projectId: props.tenantId.toString(),
708
+ });
709
+ }
710
+ } catch {
711
+ // Telemetry must never break a database operation.
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Seed the ambient telemetry context from a concrete model instance: the
717
+ * project (tenant) and the entity's own id (e.g. incidentId, monitorId).
718
+ * Used on create, where a new entity id is minted. Best-effort and safe.
719
+ */
720
+ protected setTelemetryContextFromItem(item: TBaseModel | undefined): void {
721
+ try {
722
+ if (!item) {
723
+ return;
724
+ }
725
+
726
+ const attributes: { [key: string]: string } = {};
727
+
728
+ const tenantColumn: string | null = this.model.getTenantColumn();
729
+ if (tenantColumn) {
730
+ const tenantValue: unknown = item.getColumnValue(tenantColumn);
731
+ if (tenantValue) {
732
+ attributes["projectId"] = String(tenantValue);
733
+ }
734
+ }
735
+
736
+ if (item.id) {
737
+ attributes[this.getTelemetryEntityIdKey()] = item.id.toString();
738
+ }
739
+
740
+ if (Object.keys(attributes).length > 0) {
741
+ TelemetryContext.setAttributes(attributes);
742
+ }
743
+ } catch {
744
+ // Telemetry must never break a database operation.
745
+ }
746
+ }
747
+
684
748
  @CaptureSpan()
685
749
  public async create(createBy: CreateBy<TBaseModel>): Promise<TBaseModel> {
686
750
  const onCreate: OnCreate<TBaseModel> = createBy.props.ignoreHooks
@@ -742,6 +806,9 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
742
806
  try {
743
807
  createBy.data = await this.getRepository().save(createBy.data);
744
808
 
809
+ // Seed telemetry context with projectId + <model>Id for this create.
810
+ this.setTelemetryContextFromItem(createBy.data);
811
+
745
812
  if (!createBy.props.ignoreHooks) {
746
813
  createBy.data = await this.onCreateSuccess(
747
814
  {
@@ -1216,6 +1283,8 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1216
1283
 
1217
1284
  private async _deleteBy(deleteBy: DeleteBy<TBaseModel>): Promise<number> {
1218
1285
  try {
1286
+ this.setTelemetryContextFromProps(deleteBy.props);
1287
+
1219
1288
  if (this.doNotAllowDelete && !deleteBy.props.isRoot) {
1220
1289
  throw new BadDataException("Delete not allowed");
1221
1290
  }
@@ -1444,6 +1513,8 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1444
1513
  withDeleted?: boolean | undefined,
1445
1514
  ): Promise<Array<TBaseModel>> {
1446
1515
  try {
1516
+ this.setTelemetryContextFromProps(findBy.props);
1517
+
1447
1518
  let automaticallyAddedCreatedAtInSelect: boolean = false;
1448
1519
 
1449
1520
  if (!findBy.sort || Object.keys(findBy.sort).length === 0) {
@@ -1638,6 +1709,8 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1638
1709
 
1639
1710
  private async _updateBy(updateBy: UpdateBy<TBaseModel>): Promise<number> {
1640
1711
  try {
1712
+ this.setTelemetryContextFromProps(updateBy.props);
1713
+
1641
1714
  const onUpdate: OnUpdate<TBaseModel> = updateBy.props.ignoreHooks
1642
1715
  ? { updateBy, carryForward: [] }
1643
1716
  : await this.onBeforeUpdate(updateBy);
@@ -1,10 +1,9 @@
1
1
  import DatabaseService from "./DatabaseService";
2
2
  import Model from "../../Models/DatabaseModels/TelemetryException";
3
- import TelemetryServiceModel from "../../Models/DatabaseModels/Service";
3
+ import ServiceType from "../../Types/Telemetry/ServiceType";
4
4
  import AIAgentTask from "../../Models/DatabaseModels/AIAgentTask";
5
5
  import AIAgentTaskTelemetryException from "../../Models/DatabaseModels/AIAgentTaskTelemetryException";
6
6
  import ObjectID from "../../Types/ObjectID";
7
- import PositiveNumber from "../../Types/PositiveNumber";
8
7
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
9
8
  import BadDataException from "../../Types/Exception/BadDataException";
10
9
  import AIAgentTaskType from "../../Types/AI/AIAgentTaskType";
@@ -13,7 +12,6 @@ import { FixExceptionTaskMetadata } from "../../Types/AI/AIAgentTaskMetadata";
13
12
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
14
13
  import AIAgentTaskService from "./AIAgentTaskService";
15
14
  import AIAgentTaskTelemetryExceptionService from "./AIAgentTaskTelemetryExceptionService";
16
- import ServiceService from "./ServiceService";
17
15
  import QueryHelper from "../Types/Database/QueryHelper";
18
16
  import ModelPermission from "../Types/Database/Permissions/Index";
19
17
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
@@ -24,7 +22,13 @@ export interface CreateAIAgentTaskForExceptionParams {
24
22
  }
25
23
 
26
24
  export interface DashboardServiceSummary {
27
- service: TelemetryServiceModel;
25
+ /*
26
+ * Polymorphic: a real Service, a Host/DockerHost/KubernetesCluster id, or
27
+ * the projectId for unattributed telemetry. The client resolves the
28
+ * display name per serviceType.
29
+ */
30
+ serviceId: string;
31
+ serviceType: ServiceType | null;
28
32
  unresolvedCount: number;
29
33
  totalOccurrences: number;
30
34
  }
@@ -182,11 +186,9 @@ export class Service extends DatabaseService<Model> {
182
186
  lastSeenAt: true,
183
187
  firstSeenAt: true,
184
188
  environment: true,
185
- service: {
186
- _id: true,
187
- name: true,
188
- serviceColor: true,
189
- },
189
+ // serviceId is polymorphic; the client resolves it per serviceType.
190
+ serviceId: true,
191
+ serviceType: true,
190
192
  };
191
193
 
192
194
  const [
@@ -284,6 +286,7 @@ export class Service extends DatabaseService<Model> {
284
286
 
285
287
  interface AggregateRow {
286
288
  serviceId: string | null;
289
+ serviceType: string | null;
287
290
  unresolvedCount: string;
288
291
  totalOccurrences: string | null;
289
292
  }
@@ -292,6 +295,7 @@ export class Service extends DatabaseService<Model> {
292
295
  "TelemetryException",
293
296
  )
294
297
  .select(`"TelemetryException"."serviceId"`, "serviceId")
298
+ .addSelect(`"TelemetryException"."serviceType"`, "serviceType")
295
299
  .addSelect(`COUNT(*)`, "unresolvedCount")
296
300
  .addSelect(
297
301
  `COALESCE(SUM("TelemetryException"."occuranceCount"), 0)`,
@@ -305,6 +309,7 @@ export class Service extends DatabaseService<Model> {
305
309
  .andWhere(`"TelemetryException"."deletedAt" IS NULL`)
306
310
  .andWhere(`"TelemetryException"."serviceId" IS NOT NULL`)
307
311
  .groupBy(`"TelemetryException"."serviceId"`)
312
+ .addGroupBy(`"TelemetryException"."serviceType"`)
308
313
  .orderBy(`"unresolvedCount"`, "DESC")
309
314
  .getRawMany()) as Array<AggregateRow>;
310
315
 
@@ -312,52 +317,22 @@ export class Service extends DatabaseService<Model> {
312
317
  return [];
313
318
  }
314
319
 
315
- const serviceIds: Array<string> = [];
316
- for (const row of rows) {
317
- if (row.serviceId) {
318
- serviceIds.push(row.serviceId);
319
- }
320
- }
321
-
322
- if (serviceIds.length === 0) {
323
- return [];
324
- }
325
-
326
- const services: Array<TelemetryServiceModel> = await ServiceService.findBy({
327
- query: {
328
- projectId,
329
- _id: QueryHelper.any(serviceIds),
330
- },
331
- select: {
332
- _id: true,
333
- name: true,
334
- serviceColor: true,
335
- },
336
- limit: new PositiveNumber(serviceIds.length),
337
- skip: new PositiveNumber(0),
338
- props,
339
- });
340
-
341
- const serviceById: Map<string, TelemetryServiceModel> = new Map();
342
- for (const service of services) {
343
- if (service._id) {
344
- serviceById.set(service._id, service);
345
- }
346
- }
347
-
320
+ /*
321
+ * serviceId is polymorphic do NOT resolve it to a Service here. The
322
+ * old code looked each serviceId up in the Service table and dropped
323
+ * any that didn't match, which silently excluded Host / DockerHost /
324
+ * KubernetesCluster and unattributed (Unknown) telemetry. Return the
325
+ * raw (serviceId, serviceType) + counts; the client resolves the
326
+ * display name per serviceType.
327
+ */
348
328
  const summaries: Array<DashboardServiceSummary> = [];
349
329
  for (const row of rows) {
350
330
  if (!row.serviceId) {
351
331
  continue;
352
332
  }
353
- const service: TelemetryServiceModel | undefined = serviceById.get(
354
- row.serviceId,
355
- );
356
- if (!service) {
357
- continue;
358
- }
359
333
  summaries.push({
360
- service,
334
+ serviceId: row.serviceId,
335
+ serviceType: (row.serviceType as ServiceType | null) ?? null,
361
336
  unresolvedCount: parseInt(row.unresolvedCount, 10) || 0,
362
337
  totalOccurrences: parseInt(row.totalOccurrences || "0", 10) || 0,
363
338
  });