@oneuptime/common 10.5.9 → 10.5.18

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 (164) 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/TelemetryAPI.ts +6 -0
  11. package/Server/API/TelemetryExceptionAPI.ts +6 -2
  12. package/Server/EnvironmentConfig.ts +27 -0
  13. package/Server/Infrastructure/ClickhouseDatabase.ts +21 -1
  14. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +19 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.ts +28 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.ts +24 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.ts +47 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.ts +34 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  20. package/Server/Infrastructure/PostgresDatabase.ts +27 -1
  21. package/Server/Infrastructure/QueueWorker.ts +54 -4
  22. package/Server/Infrastructure/Redis.ts +11 -0
  23. package/Server/Services/AnalyticsDatabaseService.ts +87 -0
  24. package/Server/Services/DatabaseService.ts +73 -0
  25. package/Server/Services/TelemetryAttributeService.ts +38 -2
  26. package/Server/Services/TelemetryExceptionService.ts +24 -49
  27. package/Server/Services/TelemetryUsageBillingService.ts +289 -166
  28. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +102 -72
  29. package/Server/Types/Database/Permissions/OwnedScopePermission.ts +81 -60
  30. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +67 -0
  31. package/Server/Utils/Express.ts +32 -0
  32. package/Server/Utils/GracefulShutdown.ts +194 -0
  33. package/Server/Utils/Logger.ts +12 -1
  34. package/Server/Utils/Monitor/MonitorLogUtil.ts +22 -17
  35. package/Server/Utils/Profiling.ts +14 -6
  36. package/Server/Utils/StartServer.ts +13 -5
  37. package/Server/Utils/Telemetry/ContextSpanProcessor.ts +48 -0
  38. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +289 -0
  39. package/Server/Utils/Telemetry/SpanUtil.ts +16 -35
  40. package/Server/Utils/Telemetry/StackTraceParser.ts +423 -0
  41. package/Server/Utils/Telemetry/TelemetryContext.ts +190 -0
  42. package/Server/Utils/Telemetry.ts +33 -7
  43. package/Tests/Server/Services/TelemetryAttributeService.test.ts +83 -0
  44. package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
  45. package/Types/Database/AccessControl/OwnedThrough.ts +31 -3
  46. package/Types/Telemetry/ServiceType.ts +10 -0
  47. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -1
  48. package/UI/Components/Dictionary/Dictionary.tsx +19 -0
  49. package/UI/Components/Filters/FiltersForm.tsx +1 -0
  50. package/UI/Components/Filters/JSONFilter.tsx +2 -0
  51. package/UI/Components/Filters/Types/Filter.ts +1 -0
  52. package/UI/Components/LogsViewer/LogsViewer.tsx +16 -0
  53. package/UI/Utils/Project.ts +6 -0
  54. package/UI/Utils/Telemetry/Telemetry.ts +65 -0
  55. package/UI/Utils/TelemetryService.ts +150 -0
  56. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -1
  57. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  58. package/build/dist/Models/AnalyticsModels/Log.js +1 -1
  59. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  60. package/build/dist/Models/AnalyticsModels/Metric.js +1 -1
  61. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  62. package/build/dist/Models/AnalyticsModels/Profile.js +1 -1
  63. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  64. package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -1
  65. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  66. package/build/dist/Models/AnalyticsModels/Span.js +1 -1
  67. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/TelemetryException.js +47 -33
  69. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  70. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js +36 -2
  71. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js.map +1 -1
  72. package/build/dist/Server/API/AIAgentDataAPI.js +24 -8
  73. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  74. package/build/dist/Server/API/TelemetryAPI.js +4 -0
  75. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  76. package/build/dist/Server/API/TelemetryExceptionAPI.js +6 -2
  77. package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
  78. package/build/dist/Server/EnvironmentConfig.js +19 -0
  79. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  80. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js +16 -2
  81. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js.map +1 -1
  82. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +10 -9
  83. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  84. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js +23 -0
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js.map +1 -0
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js +19 -0
  87. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js.map +1 -0
  88. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js +22 -0
  89. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js.map +1 -0
  90. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js +25 -0
  91. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js.map +1 -0
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  93. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  94. package/build/dist/Server/Infrastructure/PostgresDatabase.js +20 -1
  95. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  96. package/build/dist/Server/Infrastructure/QueueWorker.js +40 -3
  97. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  98. package/build/dist/Server/Infrastructure/Redis.js +5 -0
  99. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  100. package/build/dist/Server/Services/AnalyticsDatabaseService.js +59 -0
  101. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  102. package/build/dist/Server/Services/DatabaseService.js +62 -0
  103. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  104. package/build/dist/Server/Services/TelemetryAttributeService.js +23 -1
  105. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  106. package/build/dist/Server/Services/TelemetryExceptionService.js +16 -41
  107. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  108. package/build/dist/Server/Services/TelemetryUsageBillingService.js +211 -147
  109. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  110. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +84 -63
  111. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  112. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js +67 -49
  113. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js.map +1 -1
  114. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +51 -0
  115. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  116. package/build/dist/Server/Utils/Express.js +23 -0
  117. package/build/dist/Server/Utils/Express.js.map +1 -1
  118. package/build/dist/Server/Utils/GracefulShutdown.js +145 -0
  119. package/build/dist/Server/Utils/GracefulShutdown.js.map +1 -0
  120. package/build/dist/Server/Utils/Logger.js +8 -1
  121. package/build/dist/Server/Utils/Logger.js.map +1 -1
  122. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -10
  123. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  124. package/build/dist/Server/Utils/Profiling.js +8 -3
  125. package/build/dist/Server/Utils/Profiling.js.map +1 -1
  126. package/build/dist/Server/Utils/StartServer.js +12 -4
  127. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  128. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js +37 -0
  129. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +214 -0
  131. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/SpanUtil.js +15 -24
  133. package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
  134. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +365 -0
  135. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -0
  136. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js +124 -0
  137. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js.map +1 -0
  138. package/build/dist/Server/Utils/Telemetry.js +22 -5
  139. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  140. package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js +50 -0
  141. package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js.map +1 -0
  142. package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js +0 -0
  143. package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js.map +1 -0
  144. package/build/dist/Types/Database/AccessControl/OwnedThrough.js +7 -2
  145. package/build/dist/Types/Database/AccessControl/OwnedThrough.js.map +1 -1
  146. package/build/dist/Types/Telemetry/ServiceType.js +10 -0
  147. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  148. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +7 -1
  149. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  150. package/build/dist/UI/Components/Dictionary/Dictionary.js +10 -0
  151. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  152. package/build/dist/UI/Components/Filters/FiltersForm.js +1 -1
  153. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  154. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  155. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  156. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -0
  157. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  158. package/build/dist/UI/Utils/Project.js +5 -0
  159. package/build/dist/UI/Utils/Project.js.map +1 -1
  160. package/build/dist/UI/Utils/Telemetry/Telemetry.js +44 -0
  161. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  162. package/build/dist/UI/Utils/TelemetryService.js +113 -0
  163. package/build/dist/UI/Utils/TelemetryService.js.map +1 -0
  164. package/package.json +1 -1
@@ -4,6 +4,7 @@ import Sleep from "../../Types/Sleep";
4
4
  import { DataSource, DataSourceOptions } from "typeorm";
5
5
  import { createDatabase, dropDatabase } from "typeorm-extension";
6
6
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
7
+ import GracefulShutdown, { ShutdownPriority } from "../Utils/GracefulShutdown";
7
8
 
8
9
  export type DatabaseSourceOptions = DataSourceOptions;
9
10
  export type DatabaseSource = DataSource;
@@ -30,6 +31,15 @@ export default class Database {
30
31
 
31
32
  @CaptureSpan()
32
33
  public static async connect(): Promise<DataSource> {
34
+ /*
35
+ * Idempotent: a second connect() must not overwrite (and thereby orphan)
36
+ * the existing pool. Return the live DataSource instead of building a new
37
+ * one.
38
+ */
39
+ if (this.dataSource) {
40
+ return this.dataSource;
41
+ }
42
+
33
43
  let retry: number = 0;
34
44
 
35
45
  const dataSourceOptions: DataSourceOptions = this.getDatasourceOptions();
@@ -64,7 +74,23 @@ export default class Database {
64
74
  }
65
75
  };
66
76
 
67
- return await connectToDatabase();
77
+ const dataSource: DataSource = await connectToDatabase();
78
+
79
+ /*
80
+ * Drain the pool on shutdown. Registered here (after a successful
81
+ * connect) so we never register cleanup for a pool that was never
82
+ * created, and — thanks to GracefulShutdown deduping by name — exactly
83
+ * once even if connect() is somehow reached twice.
84
+ */
85
+ GracefulShutdown.registerHandler(
86
+ "PostgresDatabase",
87
+ ShutdownPriority.DataStores,
88
+ () => {
89
+ return this.disconnect();
90
+ },
91
+ );
92
+
93
+ return dataSource;
68
94
  } catch (err) {
69
95
  logger.error("Postgres Database Connection Failed");
70
96
  logger.error(err);
@@ -8,7 +8,14 @@ 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";
18
+ import GracefulShutdown, { ShutdownPriority } from "../Utils/GracefulShutdown";
12
19
 
13
20
  export default class QueueWorker {
14
21
  @CaptureSpan()
@@ -45,7 +52,40 @@ export default class QueueWorker {
45
52
  let outcome: "success" | "failure" | "timeout" = "success";
46
53
 
47
54
  try {
48
- await onJobInQueue(job);
55
+ /*
56
+ * Seed a telemetry-context scope for this job so every span and log it
57
+ * produces inherits the queue/job name plus any tenant identifiers
58
+ * carried in the job payload (projectId, monitorId, incidentId, ...).
59
+ */
60
+ await TelemetryContext.runWithContext(
61
+ {
62
+ queueName: queueName,
63
+ jobName: job.name || "unknown",
64
+ ...TelemetryContext.pickKnownAttributes(job.data),
65
+ },
66
+ () => {
67
+ /*
68
+ * Wrap the job in an explicit root span so every background job has
69
+ * a consistent, named trace root that carries the seeded context —
70
+ * the @CaptureSpan service calls it makes become children of this.
71
+ */
72
+ return Telemetry.startActiveSpan<Promise<void>>({
73
+ name: `worker.job ${queueName}/${job.name || "unknown"}`,
74
+ fn: async (span: Span): Promise<void> => {
75
+ try {
76
+ await onJobInQueue(job);
77
+ span.setStatus({ code: SpanStatusCode.OK });
78
+ } catch (err) {
79
+ span.recordException(err as SpanException);
80
+ span.setStatus({ code: SpanStatusCode.ERROR });
81
+ throw err;
82
+ } finally {
83
+ span.end();
84
+ }
85
+ },
86
+ });
87
+ },
88
+ );
49
89
  } catch (err) {
50
90
  outcome =
51
91
  err instanceof TimeoutException ||
@@ -77,9 +117,19 @@ export default class QueueWorker {
77
117
  : {}),
78
118
  });
79
119
 
80
- process.on("SIGINT", async () => {
81
- await worker.close();
82
- });
120
+ /*
121
+ * Stop pulling new jobs and let in-flight ones finish on shutdown. Runs in
122
+ * the Workers tier — before datastores are drained — so jobs mid-flight can
123
+ * still reach Postgres / Redis. Replaces a SIGINT-only handler that never
124
+ * fired in containers (Kubernetes / docker stop send SIGTERM).
125
+ */
126
+ GracefulShutdown.registerHandler(
127
+ `QueueWorker:${queueName}`,
128
+ ShutdownPriority.Workers,
129
+ () => {
130
+ return worker.close();
131
+ },
132
+ );
83
133
 
84
134
  return worker;
85
135
  }
@@ -15,6 +15,7 @@ import logger from "../Utils/Logger";
15
15
  import Sleep from "../../Types/Sleep";
16
16
  import { Redis as RedisClient, RedisOptions } from "ioredis";
17
17
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
18
+ import GracefulShutdown, { ShutdownPriority } from "../Utils/GracefulShutdown";
18
19
 
19
20
  export type ClientType = RedisClient;
20
21
  export type RedisOptionsType = RedisOptions;
@@ -122,6 +123,16 @@ export default abstract class Redis {
122
123
  logger.debug(
123
124
  `Redis connected on ${RedisHostname}:${RedisPort.toNumber()}`,
124
125
  );
126
+
127
+ // Close the Redis connection on shutdown.
128
+ GracefulShutdown.registerHandler(
129
+ "Redis",
130
+ ShutdownPriority.DataStores,
131
+ () => {
132
+ return this.disconnect();
133
+ },
134
+ );
135
+
125
136
  return this.client;
126
137
  } catch (err) {
127
138
  logger.error("Redis Connection Failed");
@@ -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);
@@ -364,6 +364,7 @@ export class TelemetryAttributeService {
364
364
  telemetryType: TelemetryType;
365
365
  metricName?: string | undefined;
366
366
  attributeKey: string;
367
+ searchText?: string | undefined;
367
368
  }): Promise<string[]> {
368
369
  const source: TelemetrySource | null = this.getTelemetrySource(
369
370
  data.telemetryType,
@@ -378,15 +379,17 @@ export class TelemetryAttributeService {
378
379
  source,
379
380
  metricName: data.metricName,
380
381
  attributeKey: data.attributeKey,
382
+ searchText: data.searchText,
381
383
  });
382
384
  }
383
385
 
384
- private static async fetchAttributeValuesFromDatabase(data: {
386
+ private static buildAttributeValuesStatement(data: {
385
387
  projectId: ObjectID;
386
388
  source: TelemetrySource;
387
389
  metricName?: string | undefined;
388
390
  attributeKey: string;
389
- }): Promise<Array<string>> {
391
+ searchText?: string | undefined;
392
+ }): Statement {
390
393
  const lookbackStartDate: Date =
391
394
  TelemetryAttributeService.getLookbackStartDate();
392
395
 
@@ -419,6 +422,26 @@ export class TelemetryAttributeService {
419
422
  );
420
423
  }
421
424
 
425
+ /*
426
+ * Case-insensitive substring filter so the value autocomplete keeps
427
+ * narrowing server-side as the user types. Without it only the first
428
+ * ATTRIBUTE_VALUES_LIMIT values (alphabetically) are ever reachable,
429
+ * which hides matches on high-cardinality keys (host.name, url, ...).
430
+ * Mirrors the ILIKE idiom used for bodySearchText / nameSearchText.
431
+ */
432
+ if (data.searchText && data.searchText.trim().length > 0) {
433
+ statement.append(
434
+ SQL`
435
+ AND ${data.source.attributesColumn}[${{
436
+ type: TableColumnType.Text,
437
+ value: data.attributeKey,
438
+ }}] ILIKE ${{
439
+ type: TableColumnType.Text,
440
+ value: `%${data.searchText.trim()}%`,
441
+ }}`,
442
+ );
443
+ }
444
+
422
445
  statement.append(
423
446
  SQL`
424
447
  ORDER BY attributeValue ASC
@@ -428,6 +451,19 @@ export class TelemetryAttributeService {
428
451
  }}`,
429
452
  );
430
453
 
454
+ return statement;
455
+ }
456
+
457
+ private static async fetchAttributeValuesFromDatabase(data: {
458
+ projectId: ObjectID;
459
+ source: TelemetrySource;
460
+ metricName?: string | undefined;
461
+ attributeKey: string;
462
+ searchText?: string | undefined;
463
+ }): Promise<Array<string>> {
464
+ const statement: Statement =
465
+ TelemetryAttributeService.buildAttributeValuesStatement(data);
466
+
431
467
  const dbResult: Results = await data.source.service.executeQuery(statement);
432
468
  const response: DbJSONResponse = await dbResult.json<{
433
469
  data?: Array<JSONObject>;
@@ -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
  });