@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
@@ -243,11 +243,14 @@ export class Service extends DatabaseService<Model> {
243
243
  }
244
244
 
245
245
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
246
- const data: any = {
246
+ const liveness: any = {
247
247
  lastSeenAt: OneUptimeDate.getCurrentDate(),
248
248
  otelCollectorStatus: "connected",
249
249
  };
250
250
 
251
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
+ const data: any = { ...liveness };
253
+
251
254
  if (extra?.osType) {
252
255
  data.osType = extra.osType;
253
256
  }
@@ -313,11 +316,51 @@ export class Service extends DatabaseService<Model> {
313
316
  * Heartbeat write: a single-statement UPDATE with no hooks and no
314
317
  * `version` bump, avoiding the hot-row Postgres lock convoy that the
315
318
  * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
319
+ *
320
+ * Every optional value above comes from an OpenTelemetry resource
321
+ * attribute, so its length is whatever the collector felt like sending.
322
+ * updateColumnsByIdWithoutHooks clamps them to their declared column
323
+ * widths for us — see truncateStringColumnsToMaxLength.
316
324
  */
317
- await this.updateColumnsByIdWithoutHooks({
318
- id: hostId,
319
- data: data,
320
- });
325
+ try {
326
+ await this.updateColumnsByIdWithoutHooks({
327
+ id: hostId,
328
+ data: data,
329
+ });
330
+ } catch (err) {
331
+ /*
332
+ * Liveness must survive bad metadata. The enriched write carries
333
+ * `lastSeenAt` and `otelCollectorStatus` alongside a dozen optional,
334
+ * collector-supplied columns; if any one of them makes Postgres
335
+ * reject the statement, the host silently stops advancing
336
+ * lastSeenAt and MarkDisconnectedHosts flips a perfectly healthy
337
+ * host to "disconnected" 15 minutes later — with telemetry still
338
+ * arriving the whole time. That was issue #3006 (a host.ip list
339
+ * longer than the then-varchar(500) column). Retry with liveness
340
+ * only so the enrichment is what gets dropped, never the heartbeat.
341
+ */
342
+ logger.warn(
343
+ `HostService.updateLastSeen: metadata write failed for host ${hostId.toString()}, falling back to a liveness-only update: ${
344
+ err instanceof Error ? err.message : String(err)
345
+ }`,
346
+ );
347
+
348
+ /*
349
+ * The throttle fingerprint was already stored, so without this the
350
+ * next batch would short-circuit on a cache hit and skip the retry
351
+ * for the rest of the window.
352
+ */
353
+ try {
354
+ await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
355
+ } catch {
356
+ // Best-effort; the fallback write below is what actually matters.
357
+ }
358
+
359
+ await this.updateColumnsByIdWithoutHooks({
360
+ id: hostId,
361
+ data: liveness,
362
+ });
363
+ }
321
364
  }
322
365
 
323
366
  private fingerprintExtras(extra?: {
@@ -5,6 +5,7 @@ import ColumnLength from "../../Types/Database/ColumnLength";
5
5
  import ObjectID from "../../Types/ObjectID";
6
6
  import OneUptimeDate from "../../Types/Date";
7
7
  import { clampIoTTimestamp } from "../Utils/Telemetry/IoTSnapshotScan";
8
+ import { truncateShortText } from "../Utils/Database/TruncateColumnValue";
8
9
  import logger from "../Utils/Logger";
9
10
 
10
11
  /*
@@ -59,23 +60,6 @@ export interface IoTInventorySummary {
59
60
  const UPSERT_BATCH_SIZE: number = 500;
60
61
  const STALE_DELETE_WARN_THRESHOLD: number = 100;
61
62
 
62
- /*
63
- * The identity/text columns are ShortText (100 chars). A single value
64
- * over the column limit fails the whole multi-row INSERT chunk it rides
65
- * in — truncate per value instead so one malformed device can never
66
- * drop the other rows of its chunk.
67
- */
68
- function truncateShortText(value: string): string;
69
- function truncateShortText(value: string | null): string | null;
70
- function truncateShortText(value: string | null): string | null {
71
- if (value === null) {
72
- return value;
73
- }
74
- return value.length > ColumnLength.ShortText
75
- ? value.substring(0, ColumnLength.ShortText)
76
- : value;
77
- }
78
-
79
63
  /*
80
64
  * Column order used by bulkUpsert() and its generated parameter tuples.
81
65
  * Keep this and the INSERT column list in perfect sync.
@@ -214,11 +214,14 @@ export class Service extends DatabaseService<Model> {
214
214
  }
215
215
 
216
216
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
- const data: any = {
217
+ const liveness: any = {
218
218
  lastSeenAt: OneUptimeDate.getCurrentDate(),
219
219
  otelCollectorStatus: "connected",
220
220
  };
221
221
 
222
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
223
+ const data: any = { ...liveness };
224
+
222
225
  if (extra?.agentVersion) {
223
226
  data.agentVersion = extra.agentVersion;
224
227
  }
@@ -234,11 +237,50 @@ export class Service extends DatabaseService<Model> {
234
237
  * Heartbeat write: a single-statement UPDATE with no hooks and no
235
238
  * `version` bump, avoiding the hot-row Postgres lock convoy that the
236
239
  * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
240
+ *
241
+ * Collector-supplied strings are clamped to their declared column
242
+ * widths inside updateColumnsByIdWithoutHooks — see
243
+ * DatabaseService.truncateStringColumnsToMaxLength.
237
244
  */
238
- await this.updateColumnsByIdWithoutHooks({
239
- id: fleetId,
240
- data: data,
241
- });
245
+ try {
246
+ await this.updateColumnsByIdWithoutHooks({
247
+ id: fleetId,
248
+ data: data,
249
+ });
250
+ } catch (err) {
251
+ /*
252
+ * Liveness must survive bad metadata. The enriched write carries
253
+ * `lastSeenAt` and `otelCollectorStatus` alongside optional,
254
+ * collector-supplied columns; if any one of them makes Postgres
255
+ * reject the statement, lastSeenAt silently stops advancing and
256
+ * markDisconnectedFleets flips a perfectly healthy IoT fleet to
257
+ * "disconnected" 15 minutes later — with telemetry still arriving the
258
+ * whole time. That was issue #3006 on the Host path (a host.ip list
259
+ * longer than the then-varchar(500) column). Retry with liveness only
260
+ * so the enrichment is what gets dropped, never the heartbeat.
261
+ */
262
+ logger.warn(
263
+ `IoTFleetService.updateLastSeen: metadata write failed for IoT fleet ${fleetId.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: fleetId,
281
+ data: liveness,
282
+ });
283
+ }
242
284
  }
243
285
 
244
286
  /**
@@ -184,11 +184,14 @@ export class Service extends DatabaseService<Model> {
184
184
  }
185
185
 
186
186
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
187
- const data: any = {
187
+ const liveness: any = {
188
188
  lastSeenAt: OneUptimeDate.getCurrentDate(),
189
189
  otelCollectorStatus: "connected",
190
190
  };
191
191
 
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
+ const data: any = { ...liveness };
194
+
192
195
  if (extra?.agentVersion) {
193
196
  data.agentVersion = extra.agentVersion;
194
197
  }
@@ -197,11 +200,51 @@ export class Service extends DatabaseService<Model> {
197
200
  * Heartbeat write: a single-statement UPDATE with no hooks and no
198
201
  * `version` bump, avoiding the hot-row Postgres lock convoy that the
199
202
  * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
203
+ *
204
+ * Collector-supplied strings are clamped to their declared column
205
+ * widths inside updateColumnsByIdWithoutHooks — see
206
+ * DatabaseService.truncateStringColumnsToMaxLength.
200
207
  */
201
- await this.updateColumnsByIdWithoutHooks({
202
- id: clusterId,
203
- data: data,
204
- });
208
+ try {
209
+ await this.updateColumnsByIdWithoutHooks({
210
+ id: clusterId,
211
+ data: data,
212
+ });
213
+ } catch (err) {
214
+ /*
215
+ * Liveness must survive bad metadata. The enriched write carries
216
+ * `lastSeenAt` and `otelCollectorStatus` alongside optional,
217
+ * collector-supplied columns; if any one of them makes Postgres
218
+ * reject the statement, lastSeenAt silently stops advancing and
219
+ * markDisconnectedClusters flips a perfectly healthy Kubernetes
220
+ * cluster to "disconnected" 15 minutes later — with telemetry still
221
+ * arriving the whole time. That was issue #3006 on the Host path (a
222
+ * host.ip list longer than the then-varchar(500) column). Retry with
223
+ * liveness only so the enrichment is what gets dropped, never the
224
+ * heartbeat.
225
+ */
226
+ logger.warn(
227
+ `KubernetesClusterService.updateLastSeen: metadata write failed for Kubernetes cluster ${clusterId.toString()}, falling back to a liveness-only update: ${
228
+ err instanceof Error ? err.message : String(err)
229
+ }`,
230
+ );
231
+
232
+ /*
233
+ * The throttle fingerprint was already stored, so without this the
234
+ * next batch would short-circuit on a cache hit and skip the retry
235
+ * for the rest of the window.
236
+ */
237
+ try {
238
+ await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
239
+ } catch {
240
+ // Best-effort; the fallback write below is what actually matters.
241
+ }
242
+
243
+ await this.updateColumnsByIdWithoutHooks({
244
+ id: clusterId,
245
+ data: liveness,
246
+ });
247
+ }
205
248
  }
206
249
 
207
250
  /**
@@ -1,7 +1,12 @@
1
1
  import DatabaseService from "./DatabaseService";
2
2
  import Model from "../../Models/DatabaseModels/KubernetesContainer";
3
3
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
4
+ import ColumnLength from "../../Types/Database/ColumnLength";
4
5
  import ObjectID from "../../Types/ObjectID";
6
+ import {
7
+ truncateLongText,
8
+ truncateShortText,
9
+ } from "../Utils/Database/TruncateColumnValue";
5
10
  import logger from "../Utils/Logger";
6
11
 
7
12
  /*
@@ -61,6 +66,29 @@ const UPSERT_COLUMNS: Array<string> = [
61
66
  "version",
62
67
  ];
63
68
 
69
+ /*
70
+ * KubernetesContainer's text columns are bounded varchars — podName and
71
+ * image at 500 chars, podNamespaceKey / name / state / reason at 100.
72
+ * The bulk paths below go through manager.query(), which skips the
73
+ * length validation DatabaseService applies to ordinary writes, so a
74
+ * single oversized value aborts the whole 500-row INSERT chunk it rides
75
+ * in. Clamp per value instead, so one pathological pod can never drop
76
+ * the rest of the snapshot.
77
+ */
78
+ function sanitizeContainer(
79
+ c: ParsedKubernetesContainer,
80
+ ): ParsedKubernetesContainer {
81
+ return {
82
+ ...c,
83
+ podNamespaceKey: truncateShortText(c.podNamespaceKey),
84
+ podName: truncateLongText(c.podName),
85
+ name: truncateShortText(c.name),
86
+ image: truncateLongText(c.image),
87
+ state: truncateShortText(c.state),
88
+ reason: truncateShortText(c.reason),
89
+ };
90
+ }
91
+
64
92
  export class Service extends DatabaseService<Model> {
65
93
  public constructor() {
66
94
  super(Model);
@@ -86,12 +114,20 @@ export class Service extends DatabaseService<Model> {
86
114
  return;
87
115
  }
88
116
 
89
- for (
90
- let i: number = 0;
91
- i < data.containers.length;
92
- i += UPSERT_BATCH_SIZE
93
- ) {
94
- const chunk: Array<ParsedKubernetesContainer> = data.containers.slice(
117
+ const containers: Array<ParsedKubernetesContainer> = data.containers.map(
118
+ (c: ParsedKubernetesContainer) => {
119
+ const sanitized: ParsedKubernetesContainer = sanitizeContainer(c);
120
+ if (sanitized.podName !== c.podName) {
121
+ logger.warn(
122
+ `KubernetesContainer podName exceeds ${ColumnLength.LongText} chars; truncated to "${sanitized.podName}" (cluster ${data.kubernetesClusterId.toString()}).`,
123
+ );
124
+ }
125
+ return sanitized;
126
+ },
127
+ );
128
+
129
+ for (let i: number = 0; i < containers.length; i += UPSERT_BATCH_SIZE) {
130
+ const chunk: Array<ParsedKubernetesContainer> = containers.slice(
95
131
  i,
96
132
  i + UPSERT_BATCH_SIZE,
97
133
  );
@@ -224,10 +260,15 @@ export class Service extends DatabaseService<Model> {
224
260
  valueFragments.push(
225
261
  `($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}::numeric, $${paramIndex++}::bigint, $${paramIndex++}::timestamptz)`,
226
262
  );
263
+ /*
264
+ * The identity clamps MUST match bulkUpsert's, or this mirror
265
+ * UPDATE would silently miss the row the upsert just wrote
266
+ * under the truncated podName.
267
+ */
227
268
  params.push(
228
- m.podNamespaceKey,
229
- m.podName,
230
- m.name,
269
+ truncateShortText(m.podNamespaceKey),
270
+ truncateLongText(m.podName),
271
+ truncateShortText(m.name),
231
272
  m.cpuPercent !== null && m.cpuPercent !== undefined
232
273
  ? m.cpuPercent
233
274
  : null,
@@ -1,9 +1,14 @@
1
1
  import DatabaseService from "./DatabaseService";
2
2
  import Model from "../../Models/DatabaseModels/KubernetesResource";
3
3
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
4
+ import ColumnLength from "../../Types/Database/ColumnLength";
4
5
  import ObjectID from "../../Types/ObjectID";
5
6
  import OneUptimeDate from "../../Types/Date";
6
7
  import { ParsedKubernetesResource } from "../../Types/Kubernetes/KubernetesInventoryExtractor";
8
+ import {
9
+ truncateLongText,
10
+ truncateShortText,
11
+ } from "../Utils/Database/TruncateColumnValue";
7
12
  import logger from "../Utils/Logger";
8
13
 
9
14
  /*
@@ -317,6 +322,28 @@ const UPSERT_COLUMNS: Array<keyof ParsedKubernetesResource | string> = [
317
322
  "version",
318
323
  ];
319
324
 
325
+ /*
326
+ * KubernetesResource's text columns are bounded varchars — name and
327
+ * controllerDeploymentName at 500 chars, kind / namespaceKey / uid /
328
+ * phase at 100. The bulk paths below go through manager.query(), which
329
+ * skips the length validation DatabaseService applies to ordinary
330
+ * writes, so a single oversized value aborts the whole 500-row INSERT
331
+ * chunk it rides in. Clamp per value instead, so one pathological
332
+ * object can never drop the rest of the snapshot.
333
+ */
334
+ function sanitizeResource(
335
+ r: ParsedKubernetesResource,
336
+ ): ParsedKubernetesResource {
337
+ return {
338
+ ...r,
339
+ kind: truncateShortText(r.kind),
340
+ namespaceKey: truncateShortText(r.namespaceKey),
341
+ name: truncateLongText(r.name),
342
+ uid: truncateShortText(r.uid),
343
+ phase: truncateShortText(r.phase),
344
+ };
345
+ }
346
+
320
347
  export class Service extends DatabaseService<Model> {
321
348
  public constructor() {
322
349
  super(Model);
@@ -338,9 +365,21 @@ export class Service extends DatabaseService<Model> {
338
365
  return;
339
366
  }
340
367
 
368
+ const resources: Array<ParsedKubernetesResource> = data.resources.map(
369
+ (r: ParsedKubernetesResource) => {
370
+ const sanitized: ParsedKubernetesResource = sanitizeResource(r);
371
+ if (sanitized.name !== r.name) {
372
+ logger.warn(
373
+ `KubernetesResource name exceeds ${ColumnLength.LongText} chars; truncated to "${sanitized.name}" (cluster ${data.kubernetesClusterId.toString()}).`,
374
+ );
375
+ }
376
+ return sanitized;
377
+ },
378
+ );
379
+
341
380
  // Chunk to keep individual statement parameter counts reasonable.
342
- for (let i: number = 0; i < data.resources.length; i += UPSERT_BATCH_SIZE) {
343
- const chunk: Array<ParsedKubernetesResource> = data.resources.slice(
381
+ for (let i: number = 0; i < resources.length; i += UPSERT_BATCH_SIZE) {
382
+ const chunk: Array<ParsedKubernetesResource> = resources.slice(
344
383
  i,
345
384
  i + UPSERT_BATCH_SIZE,
346
385
  );
@@ -450,10 +489,17 @@ export class Service extends DatabaseService<Model> {
450
489
  valueFragments.push(
451
490
  `($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}::numeric, $${paramIndex++}::bigint, $${paramIndex++}::numeric, $${paramIndex++}::timestamptz, $${paramIndex++}, $${paramIndex++})`,
452
491
  );
492
+ /*
493
+ * The identity clamps MUST match bulkUpsert's, or this mirror
494
+ * UPDATE would silently miss the row the upsert just wrote
495
+ * under the truncated name. controllerDeploymentName /
496
+ * controllerCronJobName are assigned columns, so an oversized
497
+ * value there aborts the chunk exactly like the INSERT path.
498
+ */
453
499
  params.push(
454
- m.kind,
455
- m.namespaceKey,
456
- m.name,
500
+ truncateShortText(m.kind),
501
+ truncateShortText(m.namespaceKey),
502
+ truncateLongText(m.name),
457
503
  m.cpuPercent !== null && m.cpuPercent !== undefined
458
504
  ? m.cpuPercent
459
505
  : null,
@@ -464,8 +510,8 @@ export class Service extends DatabaseService<Model> {
464
510
  ? m.memoryPercent
465
511
  : null,
466
512
  m.observedAt,
467
- m.controllerDeploymentName ?? null,
468
- m.controllerCronJobName ?? null,
513
+ truncateLongText(m.controllerDeploymentName ?? null),
514
+ truncateShortText(m.controllerCronJobName ?? null),
469
515
  );
470
516
  }
471
517
 
@@ -1,14 +1,20 @@
1
1
  import CreateBy from "../Types/Database/CreateBy";
2
- import { OnCreate } from "../Types/Database/Hooks";
2
+ import DeleteBy from "../Types/Database/DeleteBy";
3
+ import { OnCreate, OnDelete } from "../Types/Database/Hooks";
3
4
  import QueryHelper from "../Types/Database/QueryHelper";
4
5
  import DatabaseService from "./DatabaseService";
6
+ import ServiceLevelObjectiveMonitorRuleEngineService from "./ServiceLevelObjectiveMonitorRuleEngineService";
7
+ import ServiceLevelObjectiveService from "./ServiceLevelObjectiveService";
8
+ import ServiceLevelObjective from "../../Models/DatabaseModels/ServiceLevelObjective";
5
9
  import BadDataException from "../../Types/Exception/BadDataException";
10
+ import LIMIT_MAX from "../../Types/Database/LimitMax";
6
11
  import ObjectID from "../../Types/ObjectID";
7
12
  import Color from "../../Types/Color";
8
13
  import { BrightColors } from "../../Types/BrandColors";
9
14
  import GlobalCache from "../Infrastructure/GlobalCache";
10
15
  import Model from "../../Models/DatabaseModels/Label";
11
16
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
17
+ import logger from "../Utils/Logger";
12
18
 
13
19
  const AUTO_LABEL_ID_CACHE_NAMESPACE: string = "auto-label-id";
14
20
  const AUTO_LABEL_ID_CACHE_TTL_SECONDS: number = 24 * 60 * 60;
@@ -57,6 +63,105 @@ export class Service extends DatabaseService<Model> {
57
63
  return Promise.resolve({ createBy, carryForward: null });
58
64
  }
59
65
 
66
+ /*
67
+ * Deleting a label cascades its join rows away at the database level, so no
68
+ * service hook ever fires for the SLO label rules that referenced it. Those
69
+ * SLOs would keep the monitors the rule attached, with nothing left to
70
+ * explain why. Note the ids down while the label still exists, and re-run
71
+ * the (now smaller) rules once it is gone.
72
+ */
73
+ @CaptureSpan()
74
+ protected override async onBeforeDelete(
75
+ deleteBy: DeleteBy<Model>,
76
+ ): Promise<OnDelete<Model>> {
77
+ let serviceLevelObjectiveIds: Array<ObjectID> = [];
78
+
79
+ try {
80
+ const labels: Array<Model> = await this.findBy({
81
+ query: deleteBy.query,
82
+ select: {
83
+ _id: true,
84
+ },
85
+ limit: LIMIT_MAX,
86
+ skip: 0,
87
+ props: {
88
+ isRoot: true,
89
+ },
90
+ });
91
+
92
+ const labelIds: Array<ObjectID> = labels
93
+ .map((label: Model) => {
94
+ return label.id;
95
+ })
96
+ .filter((id: ObjectID | null): id is ObjectID => {
97
+ return Boolean(id);
98
+ });
99
+
100
+ if (labelIds.length > 0) {
101
+ const slos: Array<ServiceLevelObjective> =
102
+ await ServiceLevelObjectiveService.findBy({
103
+ query: {
104
+ monitorLabels: labelIds,
105
+ },
106
+ select: {
107
+ _id: true,
108
+ },
109
+ limit: LIMIT_MAX,
110
+ skip: 0,
111
+ props: {
112
+ isRoot: true,
113
+ },
114
+ });
115
+
116
+ serviceLevelObjectiveIds = slos
117
+ .map((slo: ServiceLevelObjective) => {
118
+ return slo.id;
119
+ })
120
+ .filter((id: ObjectID | null): id is ObjectID => {
121
+ return Boolean(id);
122
+ });
123
+ }
124
+ } catch (err) {
125
+ logger.error(
126
+ `Error collecting SLO label rules affected by a label delete: ${err}`,
127
+ );
128
+ }
129
+
130
+ return {
131
+ deleteBy,
132
+ carryForward: {
133
+ serviceLevelObjectiveIds: serviceLevelObjectiveIds,
134
+ },
135
+ };
136
+ }
137
+
138
+ @CaptureSpan()
139
+ protected override async onDeleteSuccess(
140
+ onDelete: OnDelete<Model>,
141
+ _itemIdsBeforeDelete: Array<ObjectID>,
142
+ ): Promise<OnDelete<Model>> {
143
+ const serviceLevelObjectiveIds: Array<ObjectID> =
144
+ (
145
+ onDelete.carryForward as {
146
+ serviceLevelObjectiveIds?: Array<ObjectID>;
147
+ } | null
148
+ )?.serviceLevelObjectiveIds || [];
149
+
150
+ for (const serviceLevelObjectiveId of serviceLevelObjectiveIds) {
151
+ try {
152
+ await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
153
+ serviceLevelObjectiveId: serviceLevelObjectiveId,
154
+ });
155
+ } catch (err) {
156
+ logger.error(
157
+ `Error re-applying the monitor label rule for SLO ${serviceLevelObjectiveId.toString()} after a label delete: ${err}`,
158
+ );
159
+ }
160
+ }
161
+
162
+ return onDelete;
163
+ }
164
+
60
165
  /**
61
166
  * Find existing labels by name (case-insensitive) or auto-create
62
167
  * them, returning the ObjectIDs in the same order as the input.
@@ -265,6 +265,68 @@ export class Service extends DatabaseService<Model> {
265
265
  });
266
266
  }
267
267
 
268
+ /*
269
+ * THE rule for "may this project use this provider?": a global provider is
270
+ * shared with everyone, and a project-owned provider belongs only to its
271
+ * own project. Every caller that honours a caller-supplied llmProviderId
272
+ * routes through this one predicate so the checks cannot drift apart —
273
+ * a second, subtly different copy of this comparison is how one entry point
274
+ * ends up accepting a provider another one rejects.
275
+ */
276
+ private isProviderUsableBy(provider: Model, projectId: ObjectID): boolean {
277
+ if (provider.isGlobalLlm === true) {
278
+ return true;
279
+ }
280
+
281
+ return Boolean(
282
+ provider.projectId &&
283
+ provider.projectId.toString() === projectId.toString(),
284
+ );
285
+ }
286
+
287
+ /*
288
+ * Can this project run against this provider id? Answers the question
289
+ * without loading secrets — callers that only need a yes/no (the runbook AI
290
+ * step validating a pinned provider before it runs) must not pull an apiKey
291
+ * into memory to get it. A provider that does not exist, or belongs to
292
+ * another project, is not usable.
293
+ */
294
+ @CaptureSpan()
295
+ public async isProviderUsableByProject(data: {
296
+ projectId: ObjectID;
297
+ llmProviderId: ObjectID;
298
+ }): Promise<boolean> {
299
+ /*
300
+ * _id is a uuid column: querying it with a non-uuid string is a Postgres
301
+ * cast error, not an empty result. A caller-supplied id reaches us
302
+ * straight from an unvalidated JSON config, so shape-check before the
303
+ * query and report "not usable" rather than throwing a driver error.
304
+ */
305
+ if (!ObjectID.isValidUUID(data.llmProviderId.toString())) {
306
+ return false;
307
+ }
308
+
309
+ const provider: Model | null = await this.findOneBy({
310
+ query: {
311
+ _id: data.llmProviderId.toString(),
312
+ },
313
+ select: {
314
+ _id: true,
315
+ projectId: true,
316
+ isGlobalLlm: true,
317
+ },
318
+ props: {
319
+ isRoot: true,
320
+ },
321
+ });
322
+
323
+ if (!provider) {
324
+ return false;
325
+ }
326
+
327
+ return this.isProviderUsableBy(provider, data.projectId);
328
+ }
329
+
268
330
  /*
269
331
  * Resolve the provider to use for a chat turn. When the user has explicitly
270
332
  * chosen a provider (llmProviderId), use it — but only if it is actually
@@ -278,7 +340,11 @@ export class Service extends DatabaseService<Model> {
278
340
  projectId: ObjectID;
279
341
  llmProviderId?: ObjectID | undefined;
280
342
  }): Promise<Model | null> {
281
- if (data.llmProviderId) {
343
+ if (
344
+ data.llmProviderId &&
345
+ // A non-uuid id would be a Postgres cast error, not a miss. Fall back.
346
+ ObjectID.isValidUUID(data.llmProviderId.toString())
347
+ ) {
282
348
  const provider: Model | null = await this.findOneBy({
283
349
  query: {
284
350
  _id: data.llmProviderId.toString(),
@@ -300,14 +366,8 @@ export class Service extends DatabaseService<Model> {
300
366
  },
301
367
  });
302
368
 
303
- if (provider) {
304
- const isGlobal: boolean = provider.isGlobalLlm === true;
305
- const belongsToProject: boolean =
306
- provider.projectId?.toString() === data.projectId.toString();
307
-
308
- if (isGlobal || belongsToProject) {
309
- return provider;
310
- }
369
+ if (provider && this.isProviderUsableBy(provider, data.projectId)) {
370
+ return provider;
311
371
  }
312
372
  // Fall through to default resolution when the id is invalid/inaccessible.
313
373
  }