@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
@@ -807,6 +807,23 @@ export default class ModelPermission {
807
807
  }
808
808
  }
809
809
 
810
+ /*
811
+ * Telemetry with no owning resource (the unattributed "Unknown"
812
+ * bucket) is tagged with the projectId in place of a resource id. It
813
+ * belongs to the project, not any owner, so an Owned-scoped user
814
+ * (project-level catch-all access) sees it. Gated on hasOwnedGrant:
815
+ * a purely Labels-scoped user asked for label-matching telemetry, and
816
+ * the unattributed bucket carries no labels, so it stays excluded for
817
+ * them.
818
+ */
819
+ if (
820
+ hasOwnedGrant &&
821
+ model.ownedThrough.includeProjectScope &&
822
+ props.tenantId
823
+ ) {
824
+ allowedResourceIds.add(props.tenantId.toString());
825
+ }
826
+
810
827
  const fkColumn: string = model.ownedThrough.fkColumn;
811
828
  const idList: Array<string> =
812
829
  allowedResourceIds.size > 0
@@ -841,64 +858,73 @@ export default class ModelPermission {
841
858
 
842
859
  const ownerTableRegistry: Map<
843
860
  string,
844
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
845
- { ownerUserService: any; ownerTeamService: any; fkColumn: string }
861
+ {
862
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
863
+ ownerUserService: any;
864
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
865
+ ownerTeamService: any;
866
+ fkColumn: string;
867
+ canOwnTelemetry?: boolean;
868
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
869
+ modelService?: any;
870
+ }
846
871
  > =
847
872
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
848
873
  require("../Database/Permissions/OwnerTableRegistry").default;
849
874
 
850
- const serviceEntry:
851
- | {
852
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
853
- ownerUserService: any;
854
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
855
- ownerTeamService: any;
856
- fkColumn: string;
857
- }
858
- | undefined = ownerTableRegistry.get("Service");
859
- if (!serviceEntry) {
860
- cache.ownedIds = result;
861
- return result;
862
- }
875
+ /*
876
+ * Telemetry serviceId is polymorphic — it can reference any resource
877
+ * type flagged `canOwnTelemetry` in the registry (Service, Monitor,
878
+ * Host, DockerHost, KubernetesCluster). Resolve ownership across all of
879
+ * them so a user who owns any such resource sees its telemetry, not
880
+ * just owned Services. The polymorphic set lives only in the registry
881
+ * (single source of truth); the resolved union is the same for every
882
+ * telemetry analytics model, so the single per-request `ownedIds`
883
+ * cache slot still holds it.
884
+ */
885
+ for (const entry of ownerTableRegistry.values()) {
886
+ if (!entry.canOwnTelemetry) {
887
+ continue;
888
+ }
889
+ const fkColumn: string = entry.fkColumn;
863
890
 
864
- if (props.userId) {
865
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
866
- const userOwnedRows: Array<any> =
867
- await serviceEntry.ownerUserService.findBy({
891
+ if (props.userId) {
892
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
893
+ const userOwnedRows: Array<any> = await entry.ownerUserService.findBy({
868
894
  query: {
869
895
  userId: props.userId,
870
896
  ...(props.tenantId ? { projectId: props.tenantId } : {}),
871
897
  },
872
- select: { serviceId: true },
898
+ select: { [fkColumn]: true },
873
899
  props: { isRoot: true },
874
900
  skip: 0,
875
901
  limit: LIMIT_MAX,
876
902
  });
877
- for (const row of userOwnedRows) {
878
- const id: ObjectID | undefined = row.serviceId;
879
- if (id) {
880
- result.add(id.toString());
903
+ for (const row of userOwnedRows) {
904
+ const id: ObjectID | undefined = row[fkColumn];
905
+ if (id) {
906
+ result.add(id.toString());
907
+ }
881
908
  }
882
909
  }
883
- }
884
910
 
885
- if (props.userTeamIds && props.userTeamIds.length > 0) {
886
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
887
- const teamOwnedRows: Array<any> =
888
- await serviceEntry.ownerTeamService.findBy({
911
+ if (props.userTeamIds && props.userTeamIds.length > 0) {
912
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
913
+ const teamOwnedRows: Array<any> = await entry.ownerTeamService.findBy({
889
914
  query: {
890
915
  teamId: QueryHelper.any(props.userTeamIds),
891
916
  ...(props.tenantId ? { projectId: props.tenantId } : {}),
892
917
  },
893
- select: { serviceId: true },
918
+ select: { [fkColumn]: true },
894
919
  props: { isRoot: true },
895
920
  skip: 0,
896
921
  limit: LIMIT_MAX,
897
922
  });
898
- for (const row of teamOwnedRows) {
899
- const id: ObjectID | undefined = row.serviceId;
900
- if (id) {
901
- result.add(id.toString());
923
+ for (const row of teamOwnedRows) {
924
+ const id: ObjectID | undefined = row[fkColumn];
925
+ if (id) {
926
+ result.add(id.toString());
927
+ }
902
928
  }
903
929
  }
904
930
  }
@@ -942,50 +968,54 @@ export default class ModelPermission {
942
968
  return cached;
943
969
  }
944
970
 
945
- const ServiceService: any =
946
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
947
- require("../../Services/ServiceService").default;
948
- const MonitorService: any =
949
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
950
- require("../../Services/MonitorService").default;
951
-
952
971
  const tenantFilter: Record<string, ObjectID> = props.tenantId
953
972
  ? { projectId: props.tenantId }
954
973
  : {};
955
974
 
956
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
957
- const serviceRows: Array<any> = await ServiceService.findBy({
958
- query: {
959
- labels: labelIds,
960
- ...tenantFilter,
961
- },
962
- select: { _id: true },
963
- props: { isRoot: true },
964
- skip: 0,
965
- limit: LIMIT_MAX,
966
- });
967
- for (const row of serviceRows) {
968
- const id: ObjectID | string | undefined = row._id;
969
- if (id) {
970
- result.add(id.toString());
975
+ const ownerTableRegistry: Map<
976
+ string,
977
+ {
978
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
979
+ ownerUserService: any;
980
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
981
+ ownerTeamService: any;
982
+ fkColumn: string;
983
+ canOwnTelemetry?: boolean;
984
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
985
+ modelService?: any;
971
986
  }
972
- }
987
+ > =
988
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
989
+ require("../Database/Permissions/OwnerTableRegistry").default;
973
990
 
974
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
975
- const monitorRows: Array<any> = await MonitorService.findBy({
976
- query: {
977
- labels: labelIds,
978
- ...tenantFilter,
979
- },
980
- select: { _id: true },
981
- props: { isRoot: true },
982
- skip: 0,
983
- limit: LIMIT_MAX,
984
- });
985
- for (const row of monitorRows) {
986
- const id: ObjectID | string | undefined = row._id;
987
- if (id) {
988
- result.add(id.toString());
991
+ /*
992
+ * Telemetry serviceId is polymorphic across every resource type
993
+ * flagged `canOwnTelemetry` in the registry (Service, Monitor, Host,
994
+ * DockerHost, KubernetesCluster), each of which carries labels. Find
995
+ * rows of each whose labels intersect the user's. Keeping this set in
996
+ * the registry (single source of truth) means a new telemetry-owning
997
+ * resource is picked up here automatically — no edits needed.
998
+ */
999
+ for (const entry of ownerTableRegistry.values()) {
1000
+ if (!entry.canOwnTelemetry || !entry.modelService) {
1001
+ continue;
1002
+ }
1003
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1004
+ const rows: Array<any> = await entry.modelService.findBy({
1005
+ query: {
1006
+ labels: labelIds,
1007
+ ...tenantFilter,
1008
+ },
1009
+ select: { _id: true },
1010
+ props: { isRoot: true },
1011
+ skip: 0,
1012
+ limit: LIMIT_MAX,
1013
+ });
1014
+ for (const row of rows) {
1015
+ const id: ObjectID | string | undefined = row._id;
1016
+ if (id) {
1017
+ result.add(id.toString());
1018
+ }
989
1019
  }
990
1020
  }
991
1021
 
@@ -208,15 +208,23 @@ export default class OwnedScopePermission {
208
208
  ): Promise<Array<ObjectID>> {
209
209
  const model: BaseModel = new modelType();
210
210
 
211
- // Determine which model's owner tables to consult.
212
- let resolverName: string;
213
- if (model.ownedThrough) {
214
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
- resolverName = (model.ownedThrough.parentModel as any).name;
216
- } else {
217
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
- resolverName = (modelType as any).name;
219
- }
211
+ /*
212
+ * Which model(s) owner tables to consult. A nested model can inherit
213
+ * ownership from several parent resource types when its FK is
214
+ * polymorphic (e.g. a telemetry serviceId that may point at a Service,
215
+ * Host, DockerHost or KubernetesCluster) — resolve and union the owned
216
+ * ids across all of them. Top-level operational resources consult
217
+ * their own owner tables.
218
+ */
219
+ const resolverNames: Array<string> = model.ownedThrough
220
+ ? model.ownedThrough.parentModels.map(
221
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
222
+ (parentModel: any) => {
223
+ return parentModel.name;
224
+ },
225
+ )
226
+ : // eslint-disable-next-line @typescript-eslint/no-explicit-any
227
+ [(modelType as any).name];
220
228
 
221
229
  /*
222
230
  * Lazy require to avoid the circular dep cycle: this file is reachable
@@ -227,66 +235,79 @@ export default class OwnedScopePermission {
227
235
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
228
236
  require("./OwnerTableRegistry").default;
229
237
 
230
- const registryEntry: OwnerTablePair | undefined =
231
- ownerTableRegistry.get(resolverName);
232
- if (!registryEntry) {
233
- /*
234
- * No registered owner tables for this model — Owned scope can't
235
- * resolve, so nothing is accessible.
236
- */
237
- return [];
238
- }
239
-
240
238
  const seen: Set<string> = new Set<string>();
241
- const fkColumn: string = registryEntry.fkColumn;
242
239
 
243
- /*
244
- * User-ownership lookup. Skipped for non-user callers (API keys, Probes
245
- * with no userId); those evaluate `Owned` as `All` elsewhere.
246
- */
247
- if (props.userId) {
248
- const userOwnedRows: Array<BaseModel> =
249
- await registryEntry.ownerUserService.findBy({
250
- query: {
251
- userId: props.userId,
252
- ...(props.tenantId ? { projectId: props.tenantId } : {}),
253
- },
254
- select: { [fkColumn]: true },
255
- props: { isRoot: true },
256
- skip: 0,
257
- limit: LIMIT_MAX,
258
- });
259
- for (const row of userOwnedRows) {
260
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
261
- const value: ObjectID | undefined = (row as any)[fkColumn];
262
- if (value) {
263
- seen.add(value.toString());
240
+ for (const resolverName of resolverNames) {
241
+ const registryEntry: OwnerTablePair | undefined =
242
+ ownerTableRegistry.get(resolverName);
243
+ if (!registryEntry) {
244
+ /*
245
+ * No registered owner tables for this parent — skip it. Other
246
+ * parents (or includeProjectScope below) may still resolve.
247
+ */
248
+ continue;
249
+ }
250
+
251
+ const fkColumn: string = registryEntry.fkColumn;
252
+
253
+ /*
254
+ * User-ownership lookup. Skipped for non-user callers (API keys,
255
+ * Probes with no userId); those evaluate `Owned` as `All` elsewhere.
256
+ */
257
+ if (props.userId) {
258
+ const userOwnedRows: Array<BaseModel> =
259
+ await registryEntry.ownerUserService.findBy({
260
+ query: {
261
+ userId: props.userId,
262
+ ...(props.tenantId ? { projectId: props.tenantId } : {}),
263
+ },
264
+ select: { [fkColumn]: true },
265
+ props: { isRoot: true },
266
+ skip: 0,
267
+ limit: LIMIT_MAX,
268
+ });
269
+ for (const row of userOwnedRows) {
270
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
271
+ const value: ObjectID | undefined = (row as any)[fkColumn];
272
+ if (value) {
273
+ seen.add(value.toString());
274
+ }
264
275
  }
265
276
  }
266
- }
267
277
 
268
- // Team-ownership lookup.
269
- if (props.userTeamIds && props.userTeamIds.length > 0) {
270
- const teamOwnedRows: Array<BaseModel> =
271
- await registryEntry.ownerTeamService.findBy({
272
- query: {
273
- teamId: QueryHelper.any(props.userTeamIds),
274
- ...(props.tenantId ? { projectId: props.tenantId } : {}),
275
- },
276
- select: { [fkColumn]: true },
277
- props: { isRoot: true },
278
- skip: 0,
279
- limit: LIMIT_MAX,
280
- });
281
- for (const row of teamOwnedRows) {
282
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
283
- const value: ObjectID | undefined = (row as any)[fkColumn];
284
- if (value) {
285
- seen.add(value.toString());
278
+ // Team-ownership lookup.
279
+ if (props.userTeamIds && props.userTeamIds.length > 0) {
280
+ const teamOwnedRows: Array<BaseModel> =
281
+ await registryEntry.ownerTeamService.findBy({
282
+ query: {
283
+ teamId: QueryHelper.any(props.userTeamIds),
284
+ ...(props.tenantId ? { projectId: props.tenantId } : {}),
285
+ },
286
+ select: { [fkColumn]: true },
287
+ props: { isRoot: true },
288
+ skip: 0,
289
+ limit: LIMIT_MAX,
290
+ });
291
+ for (const row of teamOwnedRows) {
292
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
+ const value: ObjectID | undefined = (row as any)[fkColumn];
294
+ if (value) {
295
+ seen.add(value.toString());
296
+ }
286
297
  }
287
298
  }
288
299
  }
289
300
 
301
+ /*
302
+ * Polymorphic FK rows with no owning resource (the unattributed
303
+ * "Unknown" telemetry bucket) carry the projectId in the FK column.
304
+ * They belong to the project, not any single owner, so include the
305
+ * tenant id when the model opts in via includeProjectScope.
306
+ */
307
+ if (model.ownedThrough?.includeProjectScope && props.tenantId) {
308
+ seen.add(props.tenantId.toString());
309
+ }
310
+
290
311
  const result: Array<ObjectID> = [];
291
312
  for (const id of seen) {
292
313
  result.push(new ObjectID(id));
@@ -14,10 +14,27 @@ import ScheduledMaintenanceOwnerTeamService from "../../../Services/ScheduledMai
14
14
  import ScheduledMaintenanceOwnerUserService from "../../../Services/ScheduledMaintenanceOwnerUserService";
15
15
  import ServiceOwnerTeamService from "../../../Services/ServiceOwnerTeamService";
16
16
  import ServiceOwnerUserService from "../../../Services/ServiceOwnerUserService";
17
+ import HostOwnerTeamService from "../../../Services/HostOwnerTeamService";
18
+ import HostOwnerUserService from "../../../Services/HostOwnerUserService";
19
+ import DockerHostOwnerTeamService from "../../../Services/DockerHostOwnerTeamService";
20
+ import DockerHostOwnerUserService from "../../../Services/DockerHostOwnerUserService";
21
+ import KubernetesClusterOwnerTeamService from "../../../Services/KubernetesClusterOwnerTeamService";
22
+ import KubernetesClusterOwnerUserService from "../../../Services/KubernetesClusterOwnerUserService";
17
23
  import StatusPageOwnerTeamService from "../../../Services/StatusPageOwnerTeamService";
18
24
  import StatusPageOwnerUserService from "../../../Services/StatusPageOwnerUserService";
19
25
  import WorkflowOwnerTeamService from "../../../Services/WorkflowOwnerTeamService";
20
26
  import WorkflowOwnerUserService from "../../../Services/WorkflowOwnerUserService";
27
+ /*
28
+ * The resources whose ids can appear in a telemetry row's polymorphic
29
+ * serviceId also expose their own service (for label-scope resolution).
30
+ * Imported here so the registry is the single source of truth for the
31
+ * telemetry serviceId polymorphism — see canOwnTelemetry / modelService.
32
+ */
33
+ import ServiceService from "../../../Services/ServiceService";
34
+ import MonitorService from "../../../Services/MonitorService";
35
+ import HostService from "../../../Services/HostService";
36
+ import DockerHostService from "../../../Services/DockerHostService";
37
+ import KubernetesClusterService from "../../../Services/KubernetesClusterService";
21
38
 
22
39
  /*
23
40
  * Maps an operational model name (e.g. "Monitor") to the two services that
@@ -35,6 +52,22 @@ export interface OwnerTablePair {
35
52
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
53
  ownerTeamService: any;
37
54
  fkColumn: string;
55
+ /*
56
+ * True when a telemetry row's serviceId (polymorphic, discriminated by
57
+ * serviceType) can reference this resource type. The analytics
58
+ * owned/labels scope resolution iterates the entries flagged here, so
59
+ * the telemetry serviceId polymorphism lives only in this registry —
60
+ * adding a new telemetry-owning resource needs just an entry here, with
61
+ * no edits in ModelPermission.
62
+ */
63
+ canOwnTelemetry?: boolean;
64
+ /*
65
+ * The resource's own service, used by labels-scope resolution to find
66
+ * resources whose labels match the user's. Set for canOwnTelemetry
67
+ * types (they all carry labels).
68
+ */
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ modelService?: any;
38
71
  }
39
72
 
40
73
  const ownerTableRegistry: Map<string, OwnerTablePair> = new Map<
@@ -47,6 +80,8 @@ const ownerTableRegistry: Map<string, OwnerTablePair> = new Map<
47
80
  ownerUserService: MonitorOwnerUserService,
48
81
  ownerTeamService: MonitorOwnerTeamService,
49
82
  fkColumn: "monitorId",
83
+ canOwnTelemetry: true,
84
+ modelService: MonitorService,
50
85
  },
51
86
  ],
52
87
  [
@@ -111,6 +146,38 @@ const ownerTableRegistry: Map<string, OwnerTablePair> = new Map<
111
146
  ownerUserService: ServiceOwnerUserService,
112
147
  ownerTeamService: ServiceOwnerTeamService,
113
148
  fkColumn: "serviceId",
149
+ canOwnTelemetry: true,
150
+ modelService: ServiceService,
151
+ },
152
+ ],
153
+ [
154
+ "Host",
155
+ {
156
+ ownerUserService: HostOwnerUserService,
157
+ ownerTeamService: HostOwnerTeamService,
158
+ fkColumn: "hostId",
159
+ canOwnTelemetry: true,
160
+ modelService: HostService,
161
+ },
162
+ ],
163
+ [
164
+ "DockerHost",
165
+ {
166
+ ownerUserService: DockerHostOwnerUserService,
167
+ ownerTeamService: DockerHostOwnerTeamService,
168
+ fkColumn: "dockerHostId",
169
+ canOwnTelemetry: true,
170
+ modelService: DockerHostService,
171
+ },
172
+ ],
173
+ [
174
+ "KubernetesCluster",
175
+ {
176
+ ownerUserService: KubernetesClusterOwnerUserService,
177
+ ownerTeamService: KubernetesClusterOwnerTeamService,
178
+ fkColumn: "kubernetesClusterId",
179
+ canOwnTelemetry: true,
180
+ modelService: KubernetesClusterService,
114
181
  },
115
182
  ],
116
183
  [
@@ -1,5 +1,6 @@
1
1
  import { LogLevel } from "../EnvironmentConfig";
2
2
  import OneUptimeTelemetry, { TelemetryLogger } from "./Telemetry";
3
+ import TelemetryContext from "./Telemetry/TelemetryContext";
3
4
  import { SeverityNumber } from "@opentelemetry/api-logs";
4
5
  import Exception from "../../Types/Exception/Exception";
5
6
  import { JSONObject } from "../../Types/JSON";
@@ -171,9 +172,19 @@ export default class logger {
171
172
  return;
172
173
  }
173
174
 
175
+ /*
176
+ * Merge ambient TelemetryContext attributes (projectId, userId,
177
+ * monitorId, requestId, ...) into every log record. Attributes passed
178
+ * explicitly to the log call take precedence over the ambient context.
179
+ */
180
+ const mergedAttributes: LogAttributes = {
181
+ ...TelemetryContext.getAttributes(),
182
+ ...(data.attributes || {}),
183
+ };
184
+
174
185
  const sanitizedAttributes:
175
186
  | Record<string, string | number | boolean>
176
- | undefined = this.sanitizeAttributes(data.attributes);
187
+ | undefined = this.sanitizeAttributes(mergedAttributes);
177
188
 
178
189
  logger.emit({
179
190
  body: this.serializeLogBody(data.body),
@@ -29,6 +29,7 @@ import logger, {
29
29
  import "./Process";
30
30
  import Response from "./Response";
31
31
  import SpanUtil from "./Telemetry/SpanUtil";
32
+ import TelemetryContext from "./Telemetry/TelemetryContext";
32
33
  import { api } from "@opentelemetry/sdk-node";
33
34
  import StatusCode from "../../Types/API/StatusCode";
34
35
  import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
@@ -213,12 +214,19 @@ app.use((req: ExpressRequest, _res: ExpressResponse, next: NextFunction) => {
213
214
  const requestId: string = crypto.randomUUID();
214
215
  (req as OneUptimeRequest).requestId = requestId;
215
216
 
216
- // Tag the current span with requestId so all downstream spans inherit context
217
- SpanUtil.addAttributesToCurrentSpan({
218
- requestId: requestId,
219
- });
217
+ /*
218
+ * Open a telemetry-context scope for the entire request. requestId is seeded
219
+ * here; projectId/userId are added later by the auth middleware. Because
220
+ * ContextSpanProcessor and Logger read this ambient context, every span and
221
+ * log produced downstream inherits it automatically.
222
+ */
223
+ TelemetryContext.runWithContext({ requestId: requestId }, () => {
224
+ SpanUtil.addAttributesToCurrentSpan({
225
+ requestId: requestId,
226
+ });
220
227
 
221
- next();
228
+ next();
229
+ });
222
230
  });
223
231
 
224
232
  export interface InitFuctionOptions {
@@ -0,0 +1,48 @@
1
+ import TelemetryContext from "./TelemetryContext";
2
+ import type { Context } from "@opentelemetry/api";
3
+ import type {
4
+ ReadableSpan,
5
+ Span,
6
+ SpanProcessor,
7
+ } from "@opentelemetry/sdk-trace-base";
8
+
9
+ /**
10
+ * Copies the ambient {@link TelemetryContext} attributes (projectId, userId,
11
+ * monitorId, incidentId, requestId, ...) onto every span at creation time.
12
+ *
13
+ * Combined with `TelemetryContext` scopes seeded at each entry point (HTTP
14
+ * request, worker job, probe check, cron run), this makes the full
15
+ * tenant/business context queryable on all spans — including the ~1958
16
+ * attribute-less `@CaptureSpan` spans — without touching any of those call
17
+ * sites.
18
+ */
19
+ export default class ContextSpanProcessor implements SpanProcessor {
20
+ public onStart(span: Span, _parentContext: Context): void {
21
+ try {
22
+ const attributes: Record<string, string | number | boolean> =
23
+ TelemetryContext.getAttributes();
24
+
25
+ for (const key in attributes) {
26
+ const value: string | number | boolean | undefined = attributes[key];
27
+
28
+ if (value !== undefined && value !== null) {
29
+ span.setAttribute(key, value);
30
+ }
31
+ }
32
+ } catch {
33
+ // Context enrichment must never break span creation.
34
+ }
35
+ }
36
+
37
+ public onEnd(_span: ReadableSpan): void {
38
+ // no-op: enrichment happens entirely in onStart.
39
+ }
40
+
41
+ public shutdown(): Promise<void> {
42
+ return Promise.resolve();
43
+ }
44
+
45
+ public forceFlush(): Promise<void> {
46
+ return Promise.resolve();
47
+ }
48
+ }
@@ -1,5 +1,6 @@
1
- import OpenTelemetryAPI, { Span } from "@opentelemetry/api";
2
1
  import { DisableTelemetry } from "../../EnvironmentConfig";
2
+ import TelemetryContext from "./TelemetryContext";
3
+ import OpenTelemetryAPI, { Span } from "@opentelemetry/api";
3
4
 
4
5
  export interface SpanAttributes {
5
6
  userId?: string | undefined;
@@ -21,14 +22,26 @@ export interface SpanAttributes {
21
22
 
22
23
  export default class SpanUtil {
23
24
  /**
24
- * Add attributes to the currently active span.
25
- * Safe to call even when there is no active span or telemetry is disabled.
25
+ * Add attributes to the current unit of work.
26
+ *
27
+ * This does two things:
28
+ * 1. Merges the attributes into the ambient {@link TelemetryContext} so that
29
+ * every span and log produced later in this request/job/check inherits
30
+ * them (OTel span attributes do NOT propagate parent -> child on their
31
+ * own, so this is what actually makes context flow downstream).
32
+ * 2. Tags the currently active span immediately, if there is one.
33
+ *
34
+ * Safe to call even when there is no active span or scope, or when telemetry
35
+ * is disabled.
26
36
  */
27
37
  public static addAttributesToCurrentSpan(attributes: SpanAttributes): void {
28
38
  if (DisableTelemetry) {
29
39
  return;
30
40
  }
31
41
 
42
+ // Propagate to all downstream spans + logs via the ambient context.
43
+ TelemetryContext.setAttributes(attributes);
44
+
32
45
  const span: Span | undefined = OpenTelemetryAPI.trace.getActiveSpan();
33
46
 
34
47
  if (!span) {
@@ -55,36 +68,4 @@ export default class SpanUtil {
55
68
  }
56
69
  }
57
70
  }
58
-
59
- /**
60
- * Build span attributes from a request-like object.
61
- * Similar to getLogAttributesFromRequest in Logger but for spans.
62
- */
63
- public static getSpanAttributesFromRequest(
64
- req?: {
65
- requestId?: string;
66
- tenantId?: { toString(): string };
67
- userAuthorization?: { userId?: { toString(): string } };
68
- } | null,
69
- ): SpanAttributes {
70
- if (!req) {
71
- return {};
72
- }
73
-
74
- const attributes: SpanAttributes = {};
75
-
76
- if (req.requestId) {
77
- attributes["requestId"] = req.requestId;
78
- }
79
-
80
- if (req.tenantId) {
81
- attributes["projectId"] = req.tenantId.toString();
82
- }
83
-
84
- if (req.userAuthorization?.userId) {
85
- attributes["userId"] = req.userAuthorization.userId.toString();
86
- }
87
-
88
- return attributes;
89
- }
90
71
  }