@oneuptime/common 12.0.0 → 12.0.2

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 (237) hide show
  1. package/Models/DatabaseModels/Host.ts +12 -4
  2. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  3. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  4. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +12 -2
  5. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  6. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  7. package/Models/DatabaseModels/StatusPage.ts +95 -0
  8. package/Server/API/TeamMemberAPI.ts +85 -0
  9. package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +12 -2
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  15. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  16. package/Server/Services/BillingService.ts +600 -51
  17. package/Server/Services/CephClusterService.ts +47 -5
  18. package/Server/Services/CephResourceService.ts +43 -4
  19. package/Server/Services/CloudResourceService.ts +47 -5
  20. package/Server/Services/DatabaseService.ts +87 -6
  21. package/Server/Services/DockerHostService.ts +47 -5
  22. package/Server/Services/DockerResourceService.ts +62 -8
  23. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  24. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  25. package/Server/Services/HostService.ts +48 -5
  26. package/Server/Services/IoTDeviceService.ts +1 -17
  27. package/Server/Services/IoTFleetService.ts +47 -5
  28. package/Server/Services/KubernetesClusterService.ts +48 -5
  29. package/Server/Services/KubernetesContainerService.ts +50 -9
  30. package/Server/Services/KubernetesResourceService.ts +53 -7
  31. package/Server/Services/LabelService.ts +106 -1
  32. package/Server/Services/LlmProviderService.ts +69 -9
  33. package/Server/Services/MonitorService.ts +57 -0
  34. package/Server/Services/PodmanHostService.ts +47 -5
  35. package/Server/Services/PodmanResourceService.ts +62 -8
  36. package/Server/Services/ProjectService.ts +165 -35
  37. package/Server/Services/ProxmoxClusterService.ts +47 -5
  38. package/Server/Services/ProxmoxResourceService.ts +43 -4
  39. package/Server/Services/RumApplicationService.ts +48 -5
  40. package/Server/Services/RunnerService.ts +1 -7
  41. package/Server/Services/ServerlessFunctionService.ts +48 -5
  42. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  43. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  44. package/Server/Services/StatusPageService.ts +95 -36
  45. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  46. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  47. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  48. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  49. package/Server/Types/Database/QueryHelper.ts +17 -32
  50. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  51. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
  52. package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
  53. package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -0
  54. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  55. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  56. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  57. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  58. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  59. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  60. package/Tests/Server/Services/BillingService.test.ts +78 -3
  61. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  62. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  63. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  64. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  65. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  66. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  67. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  68. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  69. package/Tests/Server/Services/LlmProviderUsableByProject.test.ts +293 -0
  70. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  71. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  72. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  73. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  74. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  75. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  76. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  77. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  78. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  79. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  80. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  81. package/Tests/Server/Utils/AI/ToolArgsExtractors.test.ts +141 -0
  82. package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
  83. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  84. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
  85. package/Tests/Types/Events/Recurring.test.ts +157 -0
  86. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
  87. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
  88. package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -0
  89. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  90. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  91. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  92. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  93. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  94. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  95. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  96. package/Types/Date.ts +112 -0
  97. package/Types/Events/Recurring.ts +99 -1
  98. package/Types/Runbook/RunbookStep.ts +15 -0
  99. package/Types/Runner/RunnerLiveStatus.ts +114 -0
  100. package/Types/StatusPage/StatusPageReport.ts +11 -0
  101. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  102. package/UI/Components/Detail/Detail.tsx +155 -0
  103. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  104. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  105. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  106. package/UI/Utils/TeamMembersByUser.ts +302 -0
  107. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  108. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  109. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  110. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  111. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  112. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  114. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +16 -4
  116. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  118. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  120. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  122. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  123. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  124. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +11 -1
  126. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js.map +1 -1
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  134. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  135. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  137. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  138. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  139. package/build/dist/Server/Services/BillingService.js +466 -36
  140. package/build/dist/Server/Services/BillingService.js.map +1 -1
  141. package/build/dist/Server/Services/CephClusterService.js +42 -5
  142. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  143. package/build/dist/Server/Services/CephResourceService.js +28 -3
  144. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  145. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  146. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  147. package/build/dist/Server/Services/DatabaseService.js +69 -2
  148. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  149. package/build/dist/Server/Services/DockerHostService.js +42 -5
  150. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  151. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  152. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  153. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  154. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  155. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  156. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  157. package/build/dist/Server/Services/HostService.js +43 -5
  158. package/build/dist/Server/Services/HostService.js.map +1 -1
  159. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  160. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  161. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  162. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  163. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  164. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  165. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  166. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  167. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  168. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  169. package/build/dist/Server/Services/LabelService.js +92 -0
  170. package/build/dist/Server/Services/LabelService.js.map +1 -1
  171. package/build/dist/Server/Services/LlmProviderService.js +61 -8
  172. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  173. package/build/dist/Server/Services/MonitorService.js +47 -0
  174. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  175. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  176. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  177. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  178. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  179. package/build/dist/Server/Services/ProjectService.js +135 -21
  180. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  181. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  182. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  183. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  184. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  185. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  186. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  187. package/build/dist/Server/Services/RunnerService.js +1 -6
  188. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  189. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  190. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  191. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  192. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  193. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  194. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +69 -46
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  198. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  199. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  200. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  201. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  202. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  203. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  204. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  205. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  206. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  207. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  208. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  209. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
  210. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  211. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  212. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  213. package/build/dist/Types/Date.js +68 -0
  214. package/build/dist/Types/Date.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +63 -0
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
  218. package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
  219. package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
  220. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  221. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  222. package/build/dist/UI/Components/Detail/Detail.js +97 -0
  223. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  224. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  226. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  227. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  228. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  229. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  230. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  231. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  232. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  233. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  234. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  235. package/jest.config.json +2 -0
  236. package/package.json +1 -1
  237. package/tsconfig.json +12 -1
@@ -317,11 +317,14 @@ export class Service extends DatabaseService<Model> {
317
317
  }
318
318
 
319
319
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
320
- const data: any = {
320
+ const liveness: any = {
321
321
  lastSeenAt: OneUptimeDate.getCurrentDate(),
322
322
  otelCollectorStatus: "connected",
323
323
  };
324
324
 
325
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
326
+ const data: any = { ...liveness };
327
+
325
328
  if (extra?.agentVersion) {
326
329
  data.agentVersion = extra.agentVersion;
327
330
  }
@@ -336,11 +339,51 @@ export class Service extends DatabaseService<Model> {
336
339
  * Heartbeat write: a single-statement UPDATE with no hooks and no
337
340
  * `version` bump, avoiding the hot-row Postgres lock convoy that the
338
341
  * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
342
+ *
343
+ * Collector-supplied strings are clamped to their declared column
344
+ * widths inside updateColumnsByIdWithoutHooks — see
345
+ * DatabaseService.truncateStringColumnsToMaxLength.
339
346
  */
340
- await this.updateColumnsByIdWithoutHooks({
341
- id: rumApplicationId,
342
- data: data,
343
- });
347
+ try {
348
+ await this.updateColumnsByIdWithoutHooks({
349
+ id: rumApplicationId,
350
+ data: data,
351
+ });
352
+ } catch (err) {
353
+ /*
354
+ * Liveness must survive bad metadata. The enriched write carries
355
+ * `lastSeenAt` and `otelCollectorStatus` alongside optional,
356
+ * collector-supplied columns; if any one of them makes Postgres
357
+ * reject the statement, lastSeenAt silently stops advancing and
358
+ * markDisconnectedApplications flips a perfectly healthy RUM
359
+ * application to "disconnected" 15 minutes later — with telemetry
360
+ * still arriving the whole time. That was issue #3006 on the Host
361
+ * path (a host.ip list longer than the then-varchar(500) column).
362
+ * Retry with liveness only so the enrichment is what gets dropped,
363
+ * never the heartbeat.
364
+ */
365
+ logger.warn(
366
+ `RumApplicationService.updateLastSeen: metadata write failed for RUM application ${rumApplicationId.toString()}, falling back to a liveness-only update: ${
367
+ err instanceof Error ? err.message : String(err)
368
+ }`,
369
+ );
370
+
371
+ /*
372
+ * The throttle fingerprint was already stored, so without this the
373
+ * next batch would short-circuit on a cache hit and skip the retry
374
+ * for the rest of the window.
375
+ */
376
+ try {
377
+ await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
378
+ } catch {
379
+ // Best-effort; the fallback write below is what actually matters.
380
+ }
381
+
382
+ await this.updateColumnsByIdWithoutHooks({
383
+ id: rumApplicationId,
384
+ data: liveness,
385
+ });
386
+ }
344
387
  }
345
388
 
346
389
  /*
@@ -11,13 +11,7 @@ import OneUptimeDate from "../../Types/Date";
11
11
  import { JSONObject } from "../../Types/JSON";
12
12
  import QueryHelper from "../Types/Database/QueryHelper";
13
13
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
14
-
15
- /*
16
- * How recently a Runner must have heartbeated to count as online. Matches the
17
- * window AIAgentService.isAgentAlive uses, and comfortably clears the Runner's
18
- * 60s heartbeat interval.
19
- */
20
- const RUNNER_ALIVE_WINDOW_IN_MINUTES: number = 5;
14
+ import { RUNNER_ALIVE_WINDOW_IN_MINUTES } from "../../Types/Runner/RunnerLiveStatus";
21
15
 
22
16
  export class Service extends DatabaseService<Model> {
23
17
  public constructor() {
@@ -199,11 +199,14 @@ export class Service extends DatabaseService<Model> {
199
199
  }
200
200
 
201
201
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
- const data: any = {
202
+ const liveness: any = {
203
203
  lastSeenAt: OneUptimeDate.getCurrentDate(),
204
204
  otelCollectorStatus: "connected",
205
205
  };
206
206
 
207
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
+ const data: any = { ...liveness };
209
+
207
210
  if (extra?.agentVersion) {
208
211
  data.agentVersion = extra.agentVersion;
209
212
  }
@@ -233,11 +236,51 @@ export class Service extends DatabaseService<Model> {
233
236
  * Heartbeat write: a single-statement UPDATE with no hooks and no
234
237
  * `version` bump, avoiding the hot-row Postgres lock convoy that the
235
238
  * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
239
+ *
240
+ * Collector-supplied strings are clamped to their declared column
241
+ * widths inside updateColumnsByIdWithoutHooks — see
242
+ * DatabaseService.truncateStringColumnsToMaxLength.
236
243
  */
237
- await this.updateColumnsByIdWithoutHooks({
238
- id: serverlessFunctionId,
239
- data: data,
240
- });
244
+ try {
245
+ await this.updateColumnsByIdWithoutHooks({
246
+ id: serverlessFunctionId,
247
+ data: data,
248
+ });
249
+ } catch (err) {
250
+ /*
251
+ * Liveness must survive bad metadata. The enriched write carries
252
+ * `lastSeenAt` and `otelCollectorStatus` alongside optional,
253
+ * collector-supplied columns; if any one of them makes Postgres
254
+ * reject the statement, lastSeenAt silently stops advancing and
255
+ * markDisconnectedFunctions flips a perfectly healthy serverless
256
+ * function to "disconnected" 15 minutes later — with telemetry still
257
+ * arriving the whole time. That was issue #3006 on the Host path (a
258
+ * host.ip list longer than the then-varchar(500) column). Retry with
259
+ * liveness only so the enrichment is what gets dropped, never the
260
+ * heartbeat.
261
+ */
262
+ logger.warn(
263
+ `ServerlessFunctionService.updateLastSeen: metadata write failed for serverless function ${serverlessFunctionId.toString()}, falling back to a liveness-only update: ${
264
+ err instanceof Error ? err.message : String(err)
265
+ }`,
266
+ );
267
+
268
+ /*
269
+ * The throttle fingerprint was already stored, so without this the
270
+ * next batch would short-circuit on a cache hit and skip the retry
271
+ * for the rest of the window.
272
+ */
273
+ try {
274
+ await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
275
+ } catch {
276
+ // Best-effort; the fallback write below is what actually matters.
277
+ }
278
+
279
+ await this.updateColumnsByIdWithoutHooks({
280
+ id: serverlessFunctionId,
281
+ data: liveness,
282
+ });
283
+ }
241
284
  }
242
285
 
243
286
  /**
@@ -0,0 +1,426 @@
1
+ import Monitor from "../../Models/DatabaseModels/Monitor";
2
+ import ServiceLevelObjective from "../../Models/DatabaseModels/ServiceLevelObjective";
3
+ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
4
+ import PartialEntity from "../../Types/Database/PartialEntity";
5
+ import ObjectID from "../../Types/ObjectID";
6
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
7
+ import logger, { LogAttributes } from "../Utils/Logger";
8
+ import MonitorService from "./MonitorService";
9
+ import ServiceLevelObjectiveService from "./ServiceLevelObjectiveService";
10
+
11
+ /**
12
+ * What a sync did, so callers (and tests) can assert on the outcome without
13
+ * re-reading the SLO.
14
+ */
15
+ export interface SloMonitorSyncResult {
16
+ monitorIdsAdded: Array<string>;
17
+ monitorIdsRemoved: Array<string>;
18
+ }
19
+
20
+ function emptyResult(): SloMonitorSyncResult {
21
+ return { monitorIdsAdded: [], monitorIdsRemoved: [] };
22
+ }
23
+
24
+ function toIdSet(
25
+ items: Array<{ id?: ObjectID | null }> | undefined,
26
+ ): Set<string> {
27
+ const ids: Set<string> = new Set<string>();
28
+
29
+ for (const item of items || []) {
30
+ const id: string = item?.id?.toString() || "";
31
+
32
+ if (id) {
33
+ ids.add(id);
34
+ }
35
+ }
36
+
37
+ return ids;
38
+ }
39
+
40
+ function toMonitors(ids: Iterable<string>): Array<Monitor> {
41
+ const monitors: Array<Monitor> = [];
42
+
43
+ for (const id of ids) {
44
+ const monitor: Monitor = new Monitor();
45
+ monitor.id = new ObjectID(id);
46
+ monitors.push(monitor);
47
+ }
48
+
49
+ return monitors;
50
+ }
51
+
52
+ function isSameSet(a: Set<string>, b: Set<string>): boolean {
53
+ if (a.size !== b.size) {
54
+ return false;
55
+ }
56
+
57
+ for (const value of a) {
58
+ if (!b.has(value)) {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ return true;
64
+ }
65
+
66
+ /**
67
+ * Keeps an SLO's monitor list in step with its label rule.
68
+ *
69
+ * An SLO can name a set of monitor labels ("Auto-Add Monitors With Labels").
70
+ * Every monitor in the project carrying at least one of them belongs to the
71
+ * SLO, and stops belonging to it the moment it carries none. Two entry points
72
+ * drive that, because the membership can be invalidated from either side:
73
+ *
74
+ * - the SLO side (its rule changed) -> syncMonitorsForSlo
75
+ * - the monitor side (its labels changed) -> syncSlosForMonitor
76
+ *
77
+ * Both write through ServiceLevelObjective.autoAddedMonitors, which records
78
+ * which attachments the rule owns. Only those are ever detached, so a monitor
79
+ * a human attached by hand survives every rule evaluation — including the one
80
+ * that runs when the rule is deleted.
81
+ */
82
+ export class ServiceLevelObjectiveMonitorRuleEngineServiceClass {
83
+ /**
84
+ * Re-evaluates one SLO's label rule against every monitor in its project.
85
+ * Use when the SLO's rule changed (created, edited, or cleared) — it is the
86
+ * authoritative, self-healing path and repairs any drift it finds.
87
+ */
88
+ @CaptureSpan()
89
+ public async syncMonitorsForSlo(data: {
90
+ serviceLevelObjectiveId: ObjectID;
91
+ }): Promise<SloMonitorSyncResult> {
92
+ const slo: ServiceLevelObjective | null =
93
+ await ServiceLevelObjectiveService.findOneById({
94
+ id: data.serviceLevelObjectiveId,
95
+ select: {
96
+ _id: true,
97
+ projectId: true,
98
+ monitorLabels: {
99
+ _id: true,
100
+ },
101
+ monitors: {
102
+ _id: true,
103
+ },
104
+ autoAddedMonitors: {
105
+ _id: true,
106
+ },
107
+ },
108
+ props: {
109
+ isRoot: true,
110
+ },
111
+ });
112
+
113
+ if (!slo || !slo.id || !slo.projectId) {
114
+ return emptyResult();
115
+ }
116
+
117
+ const ruleLabelIds: Set<string> = toIdSet(slo.monitorLabels);
118
+
119
+ /*
120
+ * No rule (or the rule was just cleared) means nothing matches, which in
121
+ * turn detaches everything the rule had attached. Deleting the rule
122
+ * therefore undoes the rule rather than leaving its monitors stranded on
123
+ * an SLO nobody remembers configuring.
124
+ */
125
+ const matchedMonitorIds: Set<string> =
126
+ ruleLabelIds.size === 0
127
+ ? new Set<string>()
128
+ : await this.findMonitorIdsWithAnyLabel({
129
+ projectId: slo.projectId,
130
+ labelIds: ruleLabelIds,
131
+ });
132
+
133
+ return await this.writeMembership({
134
+ slo: slo,
135
+ attachedMonitorIds: toIdSet(slo.monitors),
136
+ autoAddedMonitorIds: toIdSet(slo.autoAddedMonitors),
137
+ matchedMonitorIds: matchedMonitorIds,
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Re-evaluates every label rule in the project against a single monitor.
143
+ * Use when the monitor's labels changed: it touches only this monitor's
144
+ * membership, so it costs one query per project regardless of how many
145
+ * monitors the rules match.
146
+ */
147
+ @CaptureSpan()
148
+ public async syncSlosForMonitor(data: {
149
+ monitorId: ObjectID;
150
+ projectId?: ObjectID | undefined;
151
+ }): Promise<Array<SloMonitorSyncResult>> {
152
+ const monitor: Monitor | null = await MonitorService.findOneById({
153
+ id: data.monitorId,
154
+ select: {
155
+ _id: true,
156
+ projectId: true,
157
+ labels: {
158
+ _id: true,
159
+ },
160
+ },
161
+ props: {
162
+ isRoot: true,
163
+ },
164
+ });
165
+
166
+ if (!monitor || !monitor.id) {
167
+ return [];
168
+ }
169
+
170
+ const projectId: ObjectID | undefined =
171
+ monitor.projectId || data.projectId || undefined;
172
+
173
+ if (!projectId) {
174
+ return [];
175
+ }
176
+
177
+ const monitorLabelIds: Set<string> = toIdSet(monitor.labels);
178
+ const monitorId: string = monitor.id.toString();
179
+
180
+ const slos: Array<ServiceLevelObjective> =
181
+ await ServiceLevelObjectiveService.findBy({
182
+ query: {
183
+ projectId: projectId,
184
+ },
185
+ select: {
186
+ _id: true,
187
+ projectId: true,
188
+ monitorLabels: {
189
+ _id: true,
190
+ },
191
+ monitors: {
192
+ _id: true,
193
+ },
194
+ autoAddedMonitors: {
195
+ _id: true,
196
+ },
197
+ },
198
+ limit: LIMIT_PER_PROJECT,
199
+ skip: 0,
200
+ props: {
201
+ isRoot: true,
202
+ },
203
+ });
204
+
205
+ const results: Array<SloMonitorSyncResult> = [];
206
+
207
+ for (const slo of slos) {
208
+ const ruleLabelIds: Set<string> = toIdSet(slo.monitorLabels);
209
+
210
+ // SLOs without a label rule are curated by hand. Never touch them.
211
+ if (ruleLabelIds.size === 0) {
212
+ continue;
213
+ }
214
+
215
+ const attachedMonitorIds: Set<string> = toIdSet(slo.monitors);
216
+ const autoAddedMonitorIds: Set<string> = toIdSet(slo.autoAddedMonitors);
217
+
218
+ const doesMatch: boolean = this.doesLabelSetMatchRule({
219
+ labelIds: monitorLabelIds,
220
+ ruleLabelIds: ruleLabelIds,
221
+ });
222
+
223
+ /*
224
+ * Only this monitor's membership is in question, so the "matched" set
225
+ * handed to writeMembership is the SLO's current auto-added set with
226
+ * this monitor toggled in or out. Every other auto-added monitor is
227
+ * reported as still matching and is left exactly where it is.
228
+ */
229
+ const matchedMonitorIds: Set<string> = new Set<string>(
230
+ autoAddedMonitorIds,
231
+ );
232
+
233
+ if (doesMatch) {
234
+ matchedMonitorIds.add(monitorId);
235
+ } else {
236
+ matchedMonitorIds.delete(monitorId);
237
+ }
238
+
239
+ try {
240
+ const result: SloMonitorSyncResult = await this.writeMembership({
241
+ slo: slo,
242
+ attachedMonitorIds: attachedMonitorIds,
243
+ autoAddedMonitorIds: autoAddedMonitorIds,
244
+ matchedMonitorIds: matchedMonitorIds,
245
+ });
246
+
247
+ if (
248
+ result.monitorIdsAdded.length > 0 ||
249
+ result.monitorIdsRemoved.length > 0
250
+ ) {
251
+ results.push(result);
252
+ }
253
+ } catch (error) {
254
+ /*
255
+ * One SLO failing must not stop the others: the monitor's labels have
256
+ * already changed, and half a sync is better than none.
257
+ */
258
+ logger.error(
259
+ `Error syncing SLO ${slo.id?.toString()} for monitor ${monitorId}: ${error}`,
260
+ {
261
+ projectId: projectId.toString(),
262
+ monitorId: monitorId,
263
+ } as LogAttributes,
264
+ );
265
+ }
266
+ }
267
+
268
+ return results;
269
+ }
270
+
271
+ /**
272
+ * Every monitor in the project carrying at least one of `labelIds`.
273
+ */
274
+ @CaptureSpan()
275
+ private async findMonitorIdsWithAnyLabel(data: {
276
+ projectId: ObjectID;
277
+ labelIds: Set<string>;
278
+ }): Promise<Set<string>> {
279
+ const labelIds: Array<ObjectID> = Array.from(data.labelIds).map(
280
+ (id: string) => {
281
+ return new ObjectID(id);
282
+ },
283
+ );
284
+
285
+ if (labelIds.length === 0) {
286
+ return new Set<string>();
287
+ }
288
+
289
+ const monitors: Array<Monitor> = await MonitorService.findBy({
290
+ query: {
291
+ projectId: data.projectId,
292
+ labels: labelIds,
293
+ },
294
+ select: {
295
+ _id: true,
296
+ },
297
+ limit: LIMIT_PER_PROJECT,
298
+ skip: 0,
299
+ props: {
300
+ isRoot: true,
301
+ },
302
+ });
303
+
304
+ return toIdSet(monitors);
305
+ }
306
+
307
+ public doesLabelSetMatchRule(data: {
308
+ labelIds: Set<string>;
309
+ ruleLabelIds: Set<string>;
310
+ }): boolean {
311
+ if (data.ruleLabelIds.size === 0 || data.labelIds.size === 0) {
312
+ return false;
313
+ }
314
+
315
+ for (const labelId of data.labelIds) {
316
+ if (data.ruleLabelIds.has(labelId)) {
317
+ return true;
318
+ }
319
+ }
320
+
321
+ return false;
322
+ }
323
+
324
+ /**
325
+ * Applies a computed match set to one SLO and persists the difference.
326
+ *
327
+ * `matchedMonitorIds` is the full set of monitors the rule claims. Anything
328
+ * in it that is not attached gets attached and recorded as rule-owned;
329
+ * anything recorded as rule-owned that the rule no longer claims gets
330
+ * detached. Attachments that are not rule-owned are invisible to both
331
+ * halves, which is what keeps manual monitors safe.
332
+ */
333
+ private async writeMembership(data: {
334
+ slo: ServiceLevelObjective;
335
+ attachedMonitorIds: Set<string>;
336
+ autoAddedMonitorIds: Set<string>;
337
+ matchedMonitorIds: Set<string>;
338
+ }): Promise<SloMonitorSyncResult> {
339
+ const { slo, attachedMonitorIds, autoAddedMonitorIds, matchedMonitorIds } =
340
+ data;
341
+
342
+ if (!slo.id) {
343
+ return emptyResult();
344
+ }
345
+
346
+ const monitorIdsAdded: Array<string> = Array.from(matchedMonitorIds).filter(
347
+ (id: string) => {
348
+ return !attachedMonitorIds.has(id);
349
+ },
350
+ );
351
+
352
+ const monitorIdsRemoved: Array<string> = Array.from(
353
+ autoAddedMonitorIds,
354
+ ).filter((id: string) => {
355
+ return !matchedMonitorIds.has(id);
356
+ });
357
+
358
+ /*
359
+ * A monitor the rule still matches but that a human attached by hand stays
360
+ * manual: it is never promoted into autoAddedMonitors, so dropping the
361
+ * label later detaches nothing.
362
+ */
363
+ const nextAutoAddedMonitorIds: Set<string> = new Set<string>(
364
+ Array.from(autoAddedMonitorIds).filter((id: string) => {
365
+ return matchedMonitorIds.has(id);
366
+ }),
367
+ );
368
+
369
+ for (const id of monitorIdsAdded) {
370
+ nextAutoAddedMonitorIds.add(id);
371
+ }
372
+
373
+ const nextAttachedMonitorIds: Set<string> = new Set<string>(
374
+ Array.from(attachedMonitorIds).filter((id: string) => {
375
+ return !monitorIdsRemoved.includes(id);
376
+ }),
377
+ );
378
+
379
+ for (const id of monitorIdsAdded) {
380
+ nextAttachedMonitorIds.add(id);
381
+ }
382
+
383
+ /*
384
+ * Nothing to write. Checked against both sets rather than against
385
+ * added/removed alone, because a repair can leave the monitor list
386
+ * untouched while still needing to fix the auto-added bookkeeping.
387
+ */
388
+ if (
389
+ isSameSet(nextAttachedMonitorIds, attachedMonitorIds) &&
390
+ isSameSet(nextAutoAddedMonitorIds, autoAddedMonitorIds)
391
+ ) {
392
+ return emptyResult();
393
+ }
394
+
395
+ /*
396
+ * Cast through unknown: PartialEntity is a deep mapped type, and letting
397
+ * the compiler infer it for two arrays of Monitor (whose own relations
398
+ * fan out across most of the schema) trips TS2589 "type instantiation is
399
+ * excessively deep". The shape is exactly what the column types say.
400
+ */
401
+ const updateData: PartialEntity<ServiceLevelObjective> = {
402
+ monitors: toMonitors(nextAttachedMonitorIds),
403
+ autoAddedMonitors: toMonitors(nextAutoAddedMonitorIds),
404
+ } as unknown as PartialEntity<ServiceLevelObjective>;
405
+
406
+ await ServiceLevelObjectiveService.updateOneById({
407
+ id: slo.id,
408
+ data: updateData,
409
+ props: {
410
+ isRoot: true,
411
+ },
412
+ });
413
+
414
+ logger.debug(
415
+ `SLO monitor label rule synced SLO ${slo.id.toString()}: +${monitorIdsAdded.length} / -${monitorIdsRemoved.length} monitors`,
416
+ { projectId: slo.projectId?.toString() } as LogAttributes,
417
+ );
418
+
419
+ return {
420
+ monitorIdsAdded: monitorIdsAdded,
421
+ monitorIdsRemoved: monitorIdsRemoved,
422
+ };
423
+ }
424
+ }
425
+
426
+ export default new ServiceLevelObjectiveMonitorRuleEngineServiceClass();
@@ -25,6 +25,7 @@ import DatabaseService from "./DatabaseService";
25
25
  import MonitorStatusService from "./MonitorStatusService";
26
26
  import ProjectService from "./ProjectService";
27
27
  import ServiceLevelObjectiveBurnRateRuleService from "./ServiceLevelObjectiveBurnRateRuleService";
28
+ import ServiceLevelObjectiveMonitorRuleEngineService from "./ServiceLevelObjectiveMonitorRuleEngineService";
28
29
  import ServiceLevelObjectiveOwnerTeamService from "./ServiceLevelObjectiveOwnerTeamService";
29
30
  import ServiceLevelObjectiveOwnerUserService from "./ServiceLevelObjectiveOwnerUserService";
30
31
  import TeamMemberService from "./TeamMemberService";
@@ -156,6 +157,22 @@ export class Service extends DatabaseService<Model> {
156
157
  );
157
158
  }
158
159
 
160
+ /*
161
+ * An SLO created with a label rule must already carry the monitors that
162
+ * rule matches — otherwise it reads "no monitors attached, cannot be
163
+ * evaluated" until someone happens to edit a monitor.
164
+ */
165
+ try {
166
+ await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
167
+ serviceLevelObjectiveId: createdItem.id!,
168
+ });
169
+ } catch (err) {
170
+ logger.error(
171
+ `Error applying the monitor label rule for SLO ${createdItem.id?.toString()}: ${err}`,
172
+ { projectId: createdItem.projectId?.toString() } as LogAttributes,
173
+ );
174
+ }
175
+
159
176
  return createdItem;
160
177
  }
161
178
 
@@ -237,6 +254,28 @@ export class Service extends DatabaseService<Model> {
237
254
  }
238
255
  }
239
256
 
257
+ /*
258
+ * The label rule changed, so the monitor set it implies changed with it.
259
+ * Re-run it before the re-evaluation stamp below: the sync writes
260
+ * `monitors`, which goes through this same hook and stamps
261
+ * nextEvaluationAt itself once the new set is persisted.
262
+ */
263
+ if (onUpdate.updateBy.data.monitorLabels !== undefined) {
264
+ for (const updatedItemId of updatedItemIds) {
265
+ try {
266
+ await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo(
267
+ {
268
+ serviceLevelObjectiveId: updatedItemId,
269
+ },
270
+ );
271
+ } catch (err) {
272
+ logger.error(
273
+ `Error applying the monitor label rule for SLO ${updatedItemId.toString()}: ${err}`,
274
+ );
275
+ }
276
+ }
277
+ }
278
+
240
279
  /*
241
280
  * If the objective's math inputs changed, force a re-evaluation on the
242
281
  * worker's next tick.