@oneuptime/common 10.5.9 → 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 (106) 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/TelemetryException.ts +46 -34
  8. package/Models/DatabaseModels/TelemetryUsageBilling.ts +35 -2
  9. package/Server/API/AIAgentDataAPI.ts +25 -7
  10. package/Server/API/TelemetryExceptionAPI.ts +6 -2
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.ts +28 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.ts +24 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.ts +47 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.ts +34 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  16. package/Server/Infrastructure/QueueWorker.ts +40 -1
  17. package/Server/Services/AnalyticsDatabaseService.ts +87 -0
  18. package/Server/Services/DatabaseService.ts +73 -0
  19. package/Server/Services/TelemetryExceptionService.ts +24 -49
  20. package/Server/Services/TelemetryUsageBillingService.ts +289 -166
  21. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +102 -72
  22. package/Server/Types/Database/Permissions/OwnedScopePermission.ts +81 -60
  23. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +67 -0
  24. package/Server/Utils/Logger.ts +12 -1
  25. package/Server/Utils/StartServer.ts +13 -5
  26. package/Server/Utils/Telemetry/ContextSpanProcessor.ts +48 -0
  27. package/Server/Utils/Telemetry/SpanUtil.ts +16 -35
  28. package/Server/Utils/Telemetry/TelemetryContext.ts +190 -0
  29. package/Server/Utils/Telemetry.ts +18 -2
  30. package/Types/Database/AccessControl/OwnedThrough.ts +31 -3
  31. package/Types/Telemetry/ServiceType.ts +10 -0
  32. package/UI/Components/LogsViewer/LogsViewer.tsx +16 -0
  33. package/UI/Utils/Project.ts +6 -0
  34. package/UI/Utils/Telemetry/Telemetry.ts +65 -0
  35. package/UI/Utils/TelemetryService.ts +150 -0
  36. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -1
  37. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  38. package/build/dist/Models/AnalyticsModels/Log.js +1 -1
  39. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  40. package/build/dist/Models/AnalyticsModels/Metric.js +1 -1
  41. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  42. package/build/dist/Models/AnalyticsModels/Profile.js +1 -1
  43. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  44. package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -1
  45. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  46. package/build/dist/Models/AnalyticsModels/Span.js +1 -1
  47. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  48. package/build/dist/Models/DatabaseModels/TelemetryException.js +47 -33
  49. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  50. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js +36 -2
  51. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js.map +1 -1
  52. package/build/dist/Server/API/AIAgentDataAPI.js +24 -8
  53. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  54. package/build/dist/Server/API/TelemetryExceptionAPI.js +6 -2
  55. package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
  56. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js +23 -0
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js.map +1 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js +19 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js.map +1 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js +22 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js.map +1 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js +25 -0
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js.map +1 -0
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  65. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  66. package/build/dist/Server/Infrastructure/QueueWorker.js +31 -1
  67. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  68. package/build/dist/Server/Services/AnalyticsDatabaseService.js +59 -0
  69. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  70. package/build/dist/Server/Services/DatabaseService.js +62 -0
  71. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  72. package/build/dist/Server/Services/TelemetryExceptionService.js +16 -41
  73. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  74. package/build/dist/Server/Services/TelemetryUsageBillingService.js +211 -147
  75. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  76. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +84 -63
  77. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  78. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js +67 -49
  79. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js.map +1 -1
  80. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +51 -0
  81. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  82. package/build/dist/Server/Utils/Logger.js +8 -1
  83. package/build/dist/Server/Utils/Logger.js.map +1 -1
  84. package/build/dist/Server/Utils/StartServer.js +12 -4
  85. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  86. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js +37 -0
  87. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js.map +1 -0
  88. package/build/dist/Server/Utils/Telemetry/SpanUtil.js +15 -24
  89. package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
  90. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js +124 -0
  91. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js.map +1 -0
  92. package/build/dist/Server/Utils/Telemetry.js +12 -1
  93. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  94. package/build/dist/Types/Database/AccessControl/OwnedThrough.js +7 -2
  95. package/build/dist/Types/Database/AccessControl/OwnedThrough.js.map +1 -1
  96. package/build/dist/Types/Telemetry/ServiceType.js +10 -0
  97. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  98. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -0
  99. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  100. package/build/dist/UI/Utils/Project.js +5 -0
  101. package/build/dist/UI/Utils/Project.js.map +1 -1
  102. package/build/dist/UI/Utils/Telemetry/Telemetry.js +44 -0
  103. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  104. package/build/dist/UI/Utils/TelemetryService.js +113 -0
  105. package/build/dist/UI/Utils/TelemetryService.js.map +1 -0
  106. package/package.json +1 -1
@@ -15,7 +15,7 @@ import { SpanStatus } from "./Span";
15
15
  import ServiceType from "../../Types/Telemetry/ServiceType";
16
16
 
17
17
  @OperationalResource()
18
- @OwnedThrough("serviceId", Service)
18
+ @OwnedThrough("serviceId", Service, { includeProjectScope: true })
19
19
  export default class ExceptionInstance extends AnalyticsBaseModel {
20
20
  public constructor() {
21
21
  const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
@@ -16,7 +16,7 @@ import Service from "../DatabaseModels/Service";
16
16
  import ServiceType from "../../Types/Telemetry/ServiceType";
17
17
 
18
18
  @OperationalResource()
19
- @OwnedThrough("serviceId", Service)
19
+ @OwnedThrough("serviceId", Service, { includeProjectScope: true })
20
20
  export default class Log extends AnalyticsBaseModel {
21
21
  public constructor() {
22
22
  const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
@@ -28,7 +28,7 @@ export enum MetricPointType {
28
28
  }
29
29
 
30
30
  @OperationalResource()
31
- @OwnedThrough("serviceId", Service)
31
+ @OwnedThrough("serviceId", Service, { includeProjectScope: true })
32
32
  export default class Metric extends AnalyticsBaseModel {
33
33
  public constructor() {
34
34
  const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
@@ -15,7 +15,7 @@ import Service from "../DatabaseModels/Service";
15
15
  import ServiceType from "../../Types/Telemetry/ServiceType";
16
16
 
17
17
  @OperationalResource()
18
- @OwnedThrough("serviceId", Service)
18
+ @OwnedThrough("serviceId", Service, { includeProjectScope: true })
19
19
  export default class Profile extends AnalyticsBaseModel {
20
20
  public constructor() {
21
21
  const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
@@ -15,7 +15,7 @@ import Service from "../DatabaseModels/Service";
15
15
  import ServiceType from "../../Types/Telemetry/ServiceType";
16
16
 
17
17
  @OperationalResource()
18
- @OwnedThrough("serviceId", Service)
18
+ @OwnedThrough("serviceId", Service, { includeProjectScope: true })
19
19
  export default class ProfileSample extends AnalyticsBaseModel {
20
20
  public constructor() {
21
21
  const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
@@ -47,7 +47,7 @@ export interface SpanLink {
47
47
  }
48
48
 
49
49
  @OperationalResource()
50
- @OwnedThrough("serviceId", Service)
50
+ @OwnedThrough("serviceId", Service, { includeProjectScope: true })
51
51
  export default class Span extends AnalyticsBaseModel {
52
52
  public constructor() {
53
53
  const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
@@ -17,9 +17,13 @@ import TenantColumn from "../../Types/Database/TenantColumn";
17
17
  import IconProp from "../../Types/Icon/IconProp";
18
18
  import ObjectID from "../../Types/ObjectID";
19
19
  import Permission from "../../Types/Permission";
20
+ import ServiceType from "../../Types/Telemetry/ServiceType";
20
21
  import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
21
22
  import DatabaseBaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
22
23
  import Service from "./Service";
24
+ import Host from "./Host";
25
+ import DockerHost from "./DockerHost";
26
+ import KubernetesCluster from "./KubernetesCluster";
23
27
 
24
28
  @EnableDocumentation()
25
29
  @CanAccessIfCanReadOn("service")
@@ -60,7 +64,16 @@ import Service from "./Service";
60
64
  tableDescription:
61
65
  "List of all Telemetry Exceptions created for the telemetry service for this OneUptime project and it's status.",
62
66
  })
63
- @OwnedThrough("serviceId", Service)
67
+ /*
68
+ * serviceId is polymorphic (see the column below) — it may reference a
69
+ * Service, Host, DockerHost or KubernetesCluster, or be the projectId for
70
+ * unattributed (Unknown) telemetry. Owned scope unions ownership across all
71
+ * of those resource types, and includeProjectScope lets in-project users
72
+ * see the unattributed bucket (which has no owner resource).
73
+ */
74
+ @OwnedThrough("serviceId", [Service, Host, DockerHost, KubernetesCluster], {
75
+ includeProjectScope: true,
76
+ })
64
77
  @Entity({
65
78
  name: "TelemetryException",
66
79
  })
@@ -150,6 +163,15 @@ export default class TelemetryException extends DatabaseBaseModel {
150
163
  })
151
164
  public projectId?: ObjectID = undefined;
152
165
 
166
+ /*
167
+ * serviceId is polymorphic (disambiguated by serviceType, mirroring the
168
+ * ClickHouse ExceptionInstance rows): it can be a real Service, a Host /
169
+ * DockerHost / KubernetesCluster id, or the projectId for unattributed
170
+ * (Unknown) telemetry. There is intentionally NO @ManyToOne(Service)
171
+ * relation — a Service join would only resolve OpenTelemetry rows and
172
+ * silently null out everything else. The read side resolves serviceId +
173
+ * serviceType to a resource per type (TelemetryServiceUtil) instead.
174
+ */
153
175
  @ColumnAccessControl({
154
176
  create: [
155
177
  Permission.ProjectOwner,
@@ -172,27 +194,21 @@ export default class TelemetryException extends DatabaseBaseModel {
172
194
  Permission.EditTelemetryException,
173
195
  ],
174
196
  })
197
+ @Index()
175
198
  @TableColumn({
176
- manyToOneRelationColumn: "serviceId",
177
- type: TableColumnType.Entity,
178
- modelType: Service,
179
- title: "Service",
180
- description: "Relation to Service Resource in which this object belongs",
199
+ type: TableColumnType.ObjectID,
200
+ required: true,
201
+ title: "Service ID",
202
+ description:
203
+ "ID of the resource this exception belongs to (Service / Host / DockerHost / KubernetesCluster, or the projectId for unattributed telemetry — disambiguated by serviceType).",
181
204
  example: "d4e5f6a7-b8c9-0123-def1-234567890123",
182
205
  })
183
- @ManyToOne(
184
- () => {
185
- return Service;
186
- },
187
- {
188
- eager: false,
189
- nullable: true,
190
- onDelete: "CASCADE",
191
- orphanedRowAction: "nullify",
192
- },
193
- )
194
- @JoinColumn({ name: "serviceId" })
195
- public service?: Service = undefined;
206
+ @Column({
207
+ type: ColumnType.ObjectID,
208
+ nullable: false,
209
+ transformer: ObjectID.getDatabaseTransformer(),
210
+ })
211
+ public serviceId?: ObjectID = undefined;
196
212
 
197
213
  @ColumnAccessControl({
198
214
  create: [
@@ -210,26 +226,22 @@ export default class TelemetryException extends DatabaseBaseModel {
210
226
  Permission.TelemetryViewer,
211
227
  Permission.ReadTelemetryException,
212
228
  ],
213
- update: [
214
- Permission.ProjectOwner,
215
- Permission.ProjectAdmin,
216
- Permission.EditTelemetryException,
217
- ],
229
+ update: [],
218
230
  })
219
- @Index()
220
231
  @TableColumn({
221
- type: TableColumnType.ObjectID,
222
- required: true,
223
- title: "Service ID",
224
- description: "ID of your Service resource where this object belongs",
225
- example: "d4e5f6a7-b8c9-0123-def1-234567890123",
232
+ type: TableColumnType.ShortText,
233
+ canReadOnRelationQuery: true,
234
+ title: "Service Type",
235
+ description:
236
+ "Resource type that produced this exception (e.g. OpenTelemetry service, Host, DockerHost, KubernetesCluster, or Unknown for unattributed telemetry).",
237
+ example: "OpenTelemetry",
226
238
  })
227
239
  @Column({
228
- type: ColumnType.ObjectID,
229
- nullable: false,
230
- transformer: ObjectID.getDatabaseTransformer(),
240
+ nullable: true,
241
+ type: ColumnType.ShortText,
242
+ length: ColumnLength.ShortText,
231
243
  })
232
- public serviceId?: ObjectID = undefined;
244
+ public serviceType?: ServiceType = undefined;
233
245
 
234
246
  @ColumnAccessControl({
235
247
  create: [
@@ -15,6 +15,7 @@ import TenantColumn from "../../Types/Database/TenantColumn";
15
15
  import Decimal from "../../Types/Decimal";
16
16
  import IconProp from "../../Types/Icon/IconProp";
17
17
  import ProductType from "../../Types/MeteredPlan/ProductType";
18
+ import ServiceType from "../../Types/Telemetry/ServiceType";
18
19
  import ObjectID from "../../Types/ObjectID";
19
20
  import Permission from "../../Types/Permission";
20
21
  import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@@ -270,8 +271,16 @@ export default class TelemetryUsageBilling extends BaseModel {
270
271
  {
271
272
  eager: false,
272
273
  nullable: true,
273
- onDelete: "CASCADE",
274
- orphanedRowAction: "nullify",
274
+ /*
275
+ * No DB-level foreign key. Telemetry that arrives without a
276
+ * service.name is metered against a synthetic "unattributed"
277
+ * bucket whose serviceId is the projectId (ServiceType.Unknown),
278
+ * which has no matching Service row — a FK would reject those
279
+ * billing rows. The relation is kept for read-side joins; it
280
+ * resolves to null for the unattributed bucket and the UI renders
281
+ * a synthetic "Unknown Service" in its place.
282
+ */
283
+ createForeignKeyConstraints: false,
275
284
  },
276
285
  )
277
286
  @JoinColumn({ name: "serviceId" })
@@ -301,6 +310,30 @@ export default class TelemetryUsageBilling extends BaseModel {
301
310
  })
302
311
  public serviceId?: ObjectID = undefined;
303
312
 
313
+ @ColumnAccessControl({
314
+ create: [],
315
+ read: [
316
+ Permission.ProjectOwner,
317
+ Permission.ProjectAdmin,
318
+ Permission.ManageProjectBilling,
319
+ ],
320
+ update: [],
321
+ })
322
+ @TableColumn({
323
+ type: TableColumnType.ShortText,
324
+ canReadOnRelationQuery: true,
325
+ title: "Service Type",
326
+ description:
327
+ "Resource type that produced this telemetry (e.g. OpenTelemetry service, Host, DockerHost, KubernetesCluster, or Unknown for unattributed telemetry).",
328
+ example: "OpenTelemetry",
329
+ })
330
+ @Column({
331
+ nullable: true,
332
+ type: ColumnType.ShortText,
333
+ length: ColumnLength.ShortText,
334
+ })
335
+ public serviceType?: ServiceType = undefined;
336
+
304
337
  @ColumnAccessControl({
305
338
  create: [],
306
339
  read: [],
@@ -1,6 +1,7 @@
1
1
  import AIAgentService from "../Services/AIAgentService";
2
2
  import LlmProviderService from "../Services/LlmProviderService";
3
3
  import TelemetryExceptionService from "../Services/TelemetryExceptionService";
4
+ import ServiceService from "../Services/ServiceService";
4
5
  import ServiceCodeRepositoryService from "../Services/ServiceCodeRepositoryService";
5
6
  import CodeRepositoryService from "../Services/CodeRepositoryService";
6
7
  import AIAgentTaskPullRequestService from "../Services/AIAgentTaskPullRequestService";
@@ -15,6 +16,7 @@ import Response from "../Utils/Response";
15
16
  import AIAgent from "../../Models/DatabaseModels/AIAgent";
16
17
  import LlmProvider from "../../Models/DatabaseModels/LlmProvider";
17
18
  import TelemetryException from "../../Models/DatabaseModels/TelemetryException";
19
+ import Service from "../../Models/DatabaseModels/Service";
18
20
  import ServiceCodeRepository from "../../Models/DatabaseModels/ServiceCodeRepository";
19
21
  import CodeRepository from "../../Models/DatabaseModels/CodeRepository";
20
22
  import AIAgentTaskPullRequest from "../../Models/DatabaseModels/AIAgentTaskPullRequest";
@@ -174,11 +176,7 @@ export default class AIAgentDataAPI {
174
176
  exceptionType: true,
175
177
  fingerprint: true,
176
178
  serviceId: true,
177
- service: {
178
- _id: true,
179
- name: true,
180
- description: true,
181
- },
179
+ serviceType: true,
182
180
  },
183
181
  props: {
184
182
  isRoot: true,
@@ -198,6 +196,26 @@ export default class AIAgentDataAPI {
198
196
  getLogAttributesFromRequest(req as any),
199
197
  );
200
198
 
199
+ /*
200
+ * serviceId is polymorphic — resolve the Service only when it is
201
+ * a real Service. findOneById returns null for Host / DockerHost /
202
+ * KubernetesCluster / unattributed serviceIds (they aren't
203
+ * Services), preserving the previous "name only for real
204
+ * services" behaviour without the dropped ORM relation.
205
+ */
206
+ const exceptionService: Service | null = exception.serviceId
207
+ ? await ServiceService.findOneById({
208
+ id: exception.serviceId,
209
+ select: {
210
+ name: true,
211
+ description: true,
212
+ },
213
+ props: {
214
+ isRoot: true,
215
+ },
216
+ })
217
+ : null;
218
+
201
219
  return Response.sendJsonObjectResponse(req, res, {
202
220
  exception: {
203
221
  id: exception._id?.toString(),
@@ -209,8 +227,8 @@ export default class AIAgentDataAPI {
209
227
  service: exception.serviceId
210
228
  ? {
211
229
  id: exception.serviceId.toString(),
212
- name: exception.service?.name,
213
- description: exception.service?.description,
230
+ name: exceptionService?.name,
231
+ description: exceptionService?.description,
214
232
  }
215
233
  : null,
216
234
  });
@@ -1,5 +1,4 @@
1
1
  import TelemetryException from "../../Models/DatabaseModels/TelemetryException";
2
- import TelemetryServiceModel from "../../Models/DatabaseModels/Service";
3
2
  import AIAgentTask from "../../Models/DatabaseModels/AIAgentTask";
4
3
  import AIAgentTaskTelemetryException from "../../Models/DatabaseModels/AIAgentTaskTelemetryException";
5
4
  import BadDataException from "../../Types/Exception/BadDataException";
@@ -211,8 +210,13 @@ export default class TelemetryExceptionAPI extends BaseAPI<
211
210
 
212
211
  const serviceSummariesJson: JSONArray = summary.serviceSummaries.map(
213
212
  (entry: DashboardServiceSummary): JSONObject => {
213
+ /*
214
+ * serviceId is polymorphic; the client resolves the display name
215
+ * per serviceType (no Service relation to serialize anymore).
216
+ */
214
217
  return {
215
- service: BaseModel.toJSON(entry.service, TelemetryServiceModel),
218
+ serviceId: entry.serviceId,
219
+ serviceType: entry.serviceType,
216
220
  unresolvedCount: entry.unresolvedCount,
217
221
  totalOccurrences: entry.totalOccurrences,
218
222
  };
@@ -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
+ }
@@ -361,6 +361,10 @@ import { AddTransportTypeToProjectSmtpConfig1779975064262 } from "./177997506426
361
361
  import { AddSmtpTransportTypeToGlobalConfig1779976190561 } from "./1779976190561-AddSmtpTransportTypeToGlobalConfig";
362
362
  import { MigrationName1779980428744 } from "./1779980428744-MigrationName";
363
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";
364
368
 
365
369
  export default [
366
370
  InitialMigration,
@@ -726,4 +730,8 @@ export default [
726
730
  AddSmtpTransportTypeToGlobalConfig1779976190561,
727
731
  MigrationName1779980428744,
728
732
  AddDeliveryTrackingToSmsLog1780317745887,
733
+ MigrationName1780381124553,
734
+ MigrationName1780382837019,
735
+ MigrationName1780387560604,
736
+ MigrationName1780388219225,
729
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>,