@oneuptime/common 12.0.2 → 12.0.3

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 (228) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  2. package/Models/DatabaseModels/Index.ts +2 -0
  3. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  4. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  5. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  6. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  7. package/Server/API/BaseAPI.ts +13 -2
  8. package/Server/API/DashboardAPI.ts +39 -0
  9. package/Server/EnvironmentConfig.ts +31 -1
  10. package/Server/Infrastructure/GlobalCache.ts +115 -0
  11. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  14. package/Server/Services/AlertStateTimelineService.ts +20 -0
  15. package/Server/Services/CephClusterService.ts +26 -86
  16. package/Server/Services/CloudResourceService.ts +23 -83
  17. package/Server/Services/DatabaseService.ts +111 -0
  18. package/Server/Services/DockerHostService.ts +19 -79
  19. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  20. package/Server/Services/HostService.ts +46 -99
  21. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/IoTFleetService.ts +19 -79
  24. package/Server/Services/KubernetesClusterService.ts +17 -78
  25. package/Server/Services/LabelService.ts +57 -10
  26. package/Server/Services/MetricTypeService.ts +84 -0
  27. package/Server/Services/MonitorService.ts +89 -14
  28. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  29. package/Server/Services/MonitorTemplateService.ts +65 -0
  30. package/Server/Services/PodmanHostService.ts +19 -79
  31. package/Server/Services/ProxmoxClusterService.ts +23 -83
  32. package/Server/Services/RumApplicationService.ts +19 -80
  33. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  34. package/Server/Services/ServerlessFunctionService.ts +24 -85
  35. package/Server/Services/ServiceService.ts +299 -73
  36. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  37. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  38. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  39. package/Server/Services/TelemetryEntityService.ts +1 -0
  40. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  41. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  42. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  43. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  44. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  45. package/Server/Utils/SingleFlight.ts +83 -0
  46. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  47. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  48. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  49. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  50. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  51. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  52. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  53. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  54. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  55. package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
  56. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  57. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  58. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  59. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  60. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  61. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  62. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  63. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  64. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  65. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  66. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  67. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  68. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  69. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  70. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  71. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  72. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  73. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  74. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  75. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  76. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  77. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  78. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  79. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  80. package/Tests/Types/Decimal.test.ts +37 -0
  81. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  82. package/Tests/Types/Port.test.ts +38 -0
  83. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  84. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  85. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  86. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  87. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  88. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  89. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  90. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  91. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  92. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  93. package/Types/Database/DatabaseProperty.ts +26 -0
  94. package/Types/Database/NumericColumnValue.ts +128 -0
  95. package/Types/Decimal.ts +16 -4
  96. package/Types/Permission.ts +48 -0
  97. package/Types/Port.ts +16 -4
  98. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  99. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  100. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  101. package/Utils/Dashboard/Components/Index.ts +7 -0
  102. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  103. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  105. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  107. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  109. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  110. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  111. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  113. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  114. package/build/dist/Server/API/BaseAPI.js +11 -1
  115. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  116. package/build/dist/Server/API/DashboardAPI.js +39 -0
  117. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  118. package/build/dist/Server/EnvironmentConfig.js +27 -1
  119. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  120. package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
  121. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  122. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  123. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  128. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  129. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  130. package/build/dist/Server/Services/CephClusterService.js +26 -76
  131. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  132. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  133. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  134. package/build/dist/Server/Services/DatabaseService.js +86 -0
  135. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  136. package/build/dist/Server/Services/DockerHostService.js +19 -69
  137. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  138. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  139. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  140. package/build/dist/Server/Services/HostService.js +58 -102
  141. package/build/dist/Server/Services/HostService.js.map +1 -1
  142. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  143. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  144. package/build/dist/Server/Services/Index.js +2 -0
  145. package/build/dist/Server/Services/Index.js.map +1 -1
  146. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  147. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  148. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  149. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  150. package/build/dist/Server/Services/LabelService.js +42 -7
  151. package/build/dist/Server/Services/LabelService.js.map +1 -1
  152. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  153. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  154. package/build/dist/Server/Services/MonitorService.js +81 -9
  155. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  156. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  157. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  158. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  159. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  160. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  161. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  162. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  163. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  164. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  165. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  166. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  167. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  168. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  169. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  170. package/build/dist/Server/Services/ServiceService.js +236 -61
  171. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  172. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  173. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  174. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  175. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  176. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  177. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  178. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  179. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  180. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  181. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  182. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  183. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  184. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  185. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  186. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  187. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  188. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  189. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  190. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  191. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  192. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  193. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  194. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  195. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  196. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  197. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  198. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  199. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  200. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  201. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  202. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  203. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  204. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  205. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  206. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  207. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  208. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  209. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  210. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  211. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  212. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  213. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  214. package/build/dist/Types/Decimal.js +14 -3
  215. package/build/dist/Types/Decimal.js.map +1 -1
  216. package/build/dist/Types/Permission.js +42 -0
  217. package/build/dist/Types/Permission.js.map +1 -1
  218. package/build/dist/Types/Port.js +15 -5
  219. package/build/dist/Types/Port.js.map +1 -1
  220. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  221. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  222. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  223. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  224. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  225. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  226. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  227. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  228. package/package.json +1 -1
@@ -5,6 +5,7 @@ import Model from "../../Models/DatabaseModels/DockerSwarmCluster";
5
5
  import Label from "../../Models/DatabaseModels/Label";
6
6
  import { OnCreate } from "../Types/Database/Hooks";
7
7
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
8
+ import ResourceHeartbeat from "../Utils/Telemetry/ResourceHeartbeat";
8
9
  import ObjectID from "../../Types/ObjectID";
9
10
  import QueryHelper from "../Types/Database/QueryHelper";
10
11
  import OneUptimeDate from "../../Types/Date";
@@ -182,7 +183,6 @@ export class Service extends DatabaseService<Model> {
182
183
  networkCount?: number | undefined;
183
184
  },
184
185
  ): Promise<void> {
185
- const cacheKey: string = clusterId.toString();
186
186
  const extrasFingerprint: string = crypto
187
187
  .createHash("sha1")
188
188
  .update(
@@ -202,34 +202,6 @@ export class Service extends DatabaseService<Model> {
202
202
  )
203
203
  .digest("hex");
204
204
 
205
- let cached: string | null = null;
206
- try {
207
- cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
208
- } catch {
209
- /*
210
- * Cache unavailable — fail open and refresh lastSeenAt anyway. A
211
- * cache error must never skip the DB write below, otherwise the
212
- * resource is wrongly marked "disconnected" while telemetry is
213
- * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
214
- */
215
- cached = null;
216
- }
217
-
218
- if (cached === extrasFingerprint) {
219
- return; // same data was written recently
220
- }
221
-
222
- try {
223
- await GlobalCache.setString(
224
- LAST_SEEN_CACHE_NAMESPACE,
225
- cacheKey,
226
- extrasFingerprint,
227
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
228
- );
229
- } catch {
230
- // Best-effort throttle write; proceed with the DB update regardless.
231
- }
232
-
233
205
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
234
206
  const liveness: any = {
235
207
  lastSeenAt: OneUptimeDate.getCurrentDate(),
@@ -237,92 +209,59 @@ export class Service extends DatabaseService<Model> {
237
209
  };
238
210
 
239
211
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
- const data: any = { ...liveness };
212
+ const metadata: any = {};
241
213
 
242
214
  if (extra?.dockerVersion) {
243
- data.dockerVersion = extra.dockerVersion;
215
+ metadata.dockerVersion = extra.dockerVersion;
244
216
  }
245
217
  if (extra?.swarmId) {
246
- data.swarmId = extra.swarmId;
218
+ metadata.swarmId = extra.swarmId;
247
219
  }
248
220
  if (extra?.agentVersion) {
249
- data.agentVersion = extra.agentVersion;
221
+ metadata.agentVersion = extra.agentVersion;
250
222
  }
251
223
  // Counts: 0 is a legitimate value — gate on undefined, not falsiness.
252
224
  if (extra?.nodeCount !== undefined) {
253
- data.nodeCount = extra.nodeCount;
225
+ metadata.nodeCount = extra.nodeCount;
254
226
  }
255
227
  if (extra?.readyNodeCount !== undefined) {
256
- data.readyNodeCount = extra.readyNodeCount;
228
+ metadata.readyNodeCount = extra.readyNodeCount;
257
229
  }
258
230
  if (extra?.managerNodeCount !== undefined) {
259
- data.managerNodeCount = extra.managerNodeCount;
231
+ metadata.managerNodeCount = extra.managerNodeCount;
260
232
  }
261
233
  if (extra?.serviceCount !== undefined) {
262
- data.serviceCount = extra.serviceCount;
234
+ metadata.serviceCount = extra.serviceCount;
263
235
  }
264
236
  if (extra?.taskCount !== undefined) {
265
- data.taskCount = extra.taskCount;
237
+ metadata.taskCount = extra.taskCount;
266
238
  }
267
239
  if (extra?.runningTaskCount !== undefined) {
268
- data.runningTaskCount = extra.runningTaskCount;
240
+ metadata.runningTaskCount = extra.runningTaskCount;
269
241
  }
270
242
  if (extra?.stackCount !== undefined) {
271
- data.stackCount = extra.stackCount;
243
+ metadata.stackCount = extra.stackCount;
272
244
  }
273
245
  if (extra?.networkCount !== undefined) {
274
- data.networkCount = extra.networkCount;
246
+ metadata.networkCount = extra.networkCount;
275
247
  }
276
248
 
277
249
  /*
278
- * Heartbeat write: a single-statement UPDATE with no hooks and no
279
- * `version` bump, avoiding the hot-row Postgres lock convoy that the
280
- * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
281
- *
282
- * Collector-supplied strings are clamped to their declared column
283
- * widths inside updateColumnsByIdWithoutHooks — see
284
- * DatabaseService.truncateStringColumnsToMaxLength.
250
+ * One gated, non-blocking heartbeat write. The gates, the fail-open /
251
+ * fail-closed split and the liveness-only fallback all live in
252
+ * ResourceHeartbeat see there for why this row's throttle used to
253
+ * provide no throttling at all.
285
254
  */
286
- try {
287
- await this.updateColumnsByIdWithoutHooks({
288
- id: clusterId,
289
- data: data,
290
- });
291
- } catch (err) {
292
- /*
293
- * Liveness must survive bad metadata. The enriched write carries
294
- * `lastSeenAt` and `otelCollectorStatus` alongside optional,
295
- * collector-supplied columns; if any one of them makes Postgres
296
- * reject the statement, lastSeenAt silently stops advancing and
297
- * markDisconnectedClusters flips a perfectly healthy Docker Swarm
298
- * cluster to "disconnected" 15 minutes later — with telemetry still
299
- * arriving the whole time. That was issue #3006 on the Host path (a
300
- * host.ip list longer than the then-varchar(500) column). Retry with
301
- * liveness only so the enrichment is what gets dropped, never the
302
- * heartbeat.
303
- */
304
- logger.warn(
305
- `DockerSwarmClusterService.updateLastSeen: metadata write failed for Docker Swarm cluster ${clusterId.toString()}, falling back to a liveness-only update: ${
306
- err instanceof Error ? err.message : String(err)
307
- }`,
308
- );
309
-
310
- /*
311
- * The throttle fingerprint was already stored, so without this the
312
- * next batch would short-circuit on a cache hit and skip the retry
313
- * for the rest of the window.
314
- */
315
- try {
316
- await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
317
- } catch {
318
- // Best-effort; the fallback write below is what actually matters.
319
- }
320
-
321
- await this.updateColumnsByIdWithoutHooks({
322
- id: clusterId,
323
- data: liveness,
324
- });
325
- }
255
+ await ResourceHeartbeat.write({
256
+ service: this,
257
+ id: clusterId,
258
+ cacheNamespace: LAST_SEEN_CACHE_NAMESPACE,
259
+ throttleInSeconds: LAST_SEEN_THROTTLE_SECONDS,
260
+ liveness: liveness,
261
+ metadata: metadata,
262
+ fingerprint: extrasFingerprint,
263
+ describe: `docker swarm cluster ${clusterId.toString()}`,
264
+ });
326
265
  }
327
266
 
328
267
  /**
@@ -5,6 +5,7 @@ import Model from "../../Models/DatabaseModels/Host";
5
5
  import Label from "../../Models/DatabaseModels/Label";
6
6
  import { OnCreate } from "../Types/Database/Hooks";
7
7
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
8
+ import ResourceHeartbeat from "../Utils/Telemetry/ResourceHeartbeat";
8
9
  import ObjectID from "../../Types/ObjectID";
9
10
  import QueryHelper from "../Types/Database/QueryHelper";
10
11
  import OneUptimeDate from "../../Types/Date";
@@ -212,35 +213,7 @@ export class Service extends DatabaseService<Model> {
212
213
  * once per throttle window. If any extra value changed (e.g. cpuCores
213
214
  * updated, IP address changed), bust the cache and write immediately.
214
215
  */
215
- const cacheKey: string = hostId.toString();
216
216
  const extrasFingerprint: string = this.fingerprintExtras(extra);
217
- let cached: string | null = null;
218
- try {
219
- cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
220
- } catch {
221
- /*
222
- * Cache unavailable — fail open and refresh lastSeenAt anyway. A
223
- * cache error must never skip the DB write below, otherwise the
224
- * resource is wrongly marked "disconnected" while telemetry is
225
- * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
226
- */
227
- cached = null;
228
- }
229
-
230
- if (cached === extrasFingerprint) {
231
- return; // same data was written recently
232
- }
233
-
234
- try {
235
- await GlobalCache.setString(
236
- LAST_SEEN_CACHE_NAMESPACE,
237
- cacheKey,
238
- extrasFingerprint,
239
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
240
- );
241
- } catch {
242
- // Best-effort throttle write; proceed with the DB update regardless.
243
- }
244
217
 
245
218
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
246
219
  const liveness: any = {
@@ -249,118 +222,85 @@ export class Service extends DatabaseService<Model> {
249
222
  };
250
223
 
251
224
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
- const data: any = { ...liveness };
225
+ const metadata: any = {};
253
226
 
254
227
  if (extra?.osType) {
255
- data.osType = extra.osType;
228
+ metadata.osType = extra.osType;
256
229
  }
257
230
  if (extra?.osVersion) {
258
- data.osVersion = extra.osVersion;
231
+ metadata.osVersion = extra.osVersion;
259
232
  }
260
233
  if (extra?.hostId) {
261
- data.hostId = extra.hostId;
234
+ metadata.hostId = extra.hostId;
262
235
  }
263
236
  if (extra?.hostArch) {
264
- data.hostArch = extra.hostArch;
237
+ metadata.hostArch = extra.hostArch;
265
238
  }
266
239
  if (extra?.hostType) {
267
- data.hostType = extra.hostType;
240
+ metadata.hostType = extra.hostType;
268
241
  }
269
242
  if (extra?.hostIpAddresses) {
270
- data.hostIpAddresses = extra.hostIpAddresses;
243
+ metadata.hostIpAddresses = extra.hostIpAddresses;
271
244
  }
272
245
  if (extra?.cpuCores !== undefined) {
273
- data.cpuCores = extra.cpuCores;
246
+ metadata.cpuCores = extra.cpuCores;
274
247
  }
275
248
  if (extra?.totalMemoryBytes !== undefined) {
276
- data.totalMemoryBytes = extra.totalMemoryBytes;
249
+ metadata.totalMemoryBytes = extra.totalMemoryBytes;
277
250
  }
278
251
  if (extra?.processCount !== undefined) {
279
- data.processCount = extra.processCount;
252
+ metadata.processCount = extra.processCount;
280
253
  }
281
254
  if (extra?.containerRuntime) {
282
- data.containerRuntime = extra.containerRuntime;
255
+ metadata.containerRuntime = extra.containerRuntime;
283
256
  }
284
257
  if (extra?.dockerHostId) {
285
- data.dockerHostId = extra.dockerHostId;
258
+ metadata.dockerHostId = extra.dockerHostId;
286
259
  }
287
260
  if (extra?.kubernetesClusterId) {
288
- data.kubernetesClusterId = extra.kubernetesClusterId;
261
+ metadata.kubernetesClusterId = extra.kubernetesClusterId;
289
262
  }
290
263
  if (extra?.agentVersion) {
291
- data.agentVersion = extra.agentVersion;
264
+ metadata.agentVersion = extra.agentVersion;
292
265
  }
293
266
  if (extra?.deploymentEnvironment) {
294
- data.deploymentEnvironment = extra.deploymentEnvironment;
267
+ metadata.deploymentEnvironment = extra.deploymentEnvironment;
295
268
  }
296
269
  if (extra?.runtimeName) {
297
- data.runtimeName = extra.runtimeName;
270
+ metadata.runtimeName = extra.runtimeName;
298
271
  }
299
272
  if (extra?.runtimeVersion) {
300
- data.runtimeVersion = extra.runtimeVersion;
273
+ metadata.runtimeVersion = extra.runtimeVersion;
301
274
  }
302
275
  if (extra?.cloudProvider) {
303
- data.cloudProvider = extra.cloudProvider;
276
+ metadata.cloudProvider = extra.cloudProvider;
304
277
  }
305
278
  if (extra?.cloudPlatform) {
306
- data.cloudPlatform = extra.cloudPlatform;
279
+ metadata.cloudPlatform = extra.cloudPlatform;
307
280
  }
308
281
  if (extra?.cloudRegion) {
309
- data.cloudRegion = extra.cloudRegion;
282
+ metadata.cloudRegion = extra.cloudRegion;
310
283
  }
311
284
  if (extra?.cloudAccountId) {
312
- data.cloudAccountId = extra.cloudAccountId;
285
+ metadata.cloudAccountId = extra.cloudAccountId;
313
286
  }
314
287
 
315
288
  /*
316
- * Heartbeat write: a single-statement UPDATE with no hooks and no
317
- * `version` bump, avoiding the hot-row Postgres lock convoy that the
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.
289
+ * One gated, non-blocking heartbeat write. The gates, the fail-open /
290
+ * fail-closed split and the liveness-only fallback all live in
291
+ * ResourceHeartbeat see there for why this row's throttle used to
292
+ * provide no throttling at all.
324
293
  */
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
- }
294
+ await ResourceHeartbeat.write({
295
+ service: this,
296
+ id: hostId,
297
+ cacheNamespace: LAST_SEEN_CACHE_NAMESPACE,
298
+ throttleInSeconds: LAST_SEEN_THROTTLE_SECONDS,
299
+ liveness: liveness,
300
+ metadata: metadata,
301
+ fingerprint: extrasFingerprint,
302
+ describe: `host ${hostId.toString()}`,
303
+ });
364
304
  }
365
305
 
366
306
  private fingerprintExtras(extra?: {
@@ -392,9 +332,16 @@ export class Service extends DatabaseService<Model> {
392
332
  hostArch: extra?.hostArch ?? null,
393
333
  hostType: extra?.hostType ?? null,
394
334
  hostIpAddresses: extra?.hostIpAddresses ?? null,
395
- cpuCores: extra?.cpuCores ?? null,
396
- totalMemoryBytes: extra?.totalMemoryBytes ?? null,
397
- processCount: extra?.processCount ?? null,
335
+ /*
336
+ * cpuCores / totalMemoryBytes / processCount are DELIBERATELY not
337
+ * hashed. They are per-scrape gauges, and `system.processes.count` in
338
+ * particular is partitioned by process.status and summed, so it changes
339
+ * on essentially every scrape. Hashing them made the throttle key differ
340
+ * every time and the 60s window never engaged — for exactly the hosts
341
+ * producing the most batches, which is the worst possible place to lose
342
+ * a throttle. The columns are still written; they simply ride along on
343
+ * whatever write the throttle admits rather than forcing one.
344
+ */
398
345
  containerRuntime: extra?.containerRuntime ?? null,
399
346
  dockerHostId: extra?.dockerHostId?.toString() ?? null,
400
347
  kubernetesClusterId: extra?.kubernetesClusterId?.toString() ?? null,
@@ -25,6 +25,7 @@ import IncidentStateTimeline from "../../Models/DatabaseModels/IncidentStateTime
25
25
  import IncidentMember from "../../Models/DatabaseModels/IncidentMember";
26
26
  import IncidentRole from "../../Models/DatabaseModels/IncidentRole";
27
27
  import { IsBillingEnabled } from "../EnvironmentConfig";
28
+ import ProjectScopedReferenceValidator from "../Utils/Database/ProjectScopedReferenceValidator";
28
29
  import logger, { LogAttributes } from "../Utils/Logger";
29
30
  import IncidentFeedService from "./IncidentFeedService";
30
31
  import AIIncidentPostmortemRunner from "../Utils/AI/SRE/IncidentPostmortemRunner";
@@ -128,6 +129,25 @@ export class Service extends DatabaseService<IncidentStateTimeline> {
128
129
  throw new BadDataException("incidentStateId is null");
129
130
  }
130
131
 
132
+ /*
133
+ * Same guard, same reason, as MonitorStatusTimelineService: an id that
134
+ * exists nowhere (or belongs to another project) used to reach Postgres
135
+ * and come back as a raw foreign key violation. The ordering check below
136
+ * does look the state up, but only when there IS a preceding row with an
137
+ * order, so the first timeline row for an incident went through unchecked.
138
+ */
139
+ await ProjectScopedReferenceValidator.validateReferencesBelongToProject({
140
+ projectId: createBy.props.tenantId || createBy.data.projectId,
141
+ subject: "incident state timeline",
142
+ references: [
143
+ {
144
+ modelName: "Incident State",
145
+ id: incidentStateId,
146
+ service: IncidentStateService,
147
+ },
148
+ ],
149
+ });
150
+
131
151
  // Execute queries for before and after states in parallel for better performance
132
152
  const [stateBeforeThis, stateAfterThis] = await Promise.all([
133
153
  this.findOneBy({
@@ -177,6 +177,7 @@ import StatusPageOwnerUserService from "./StatusPageOwnerUserService";
177
177
  import StatusPagePrivateUserService from "./StatusPagePrivateUserService";
178
178
  import StatusPagePrivateUserSessionService from "./StatusPagePrivateUserSessionService";
179
179
  import StatusPageResourceService from "./StatusPageResourceService";
180
+ import StatusPageMonitorRuleService from "./StatusPageMonitorRuleService";
180
181
  // Status Page
181
182
  import StatusPageService from "./StatusPageService";
182
183
  import StatusPageSsoService from "./StatusPageSsoService";
@@ -438,6 +439,7 @@ const services: Array<BaseService> = [
438
439
  StatusPagePrivateUserService,
439
440
  StatusPagePrivateUserSessionService,
440
441
  StatusPageResourceService,
442
+ StatusPageMonitorRuleService,
441
443
  StatusPageService,
442
444
  StatusPageSsoService,
443
445
  StatusPageOidcService,
@@ -5,6 +5,7 @@ import Model from "../../Models/DatabaseModels/IoTFleet";
5
5
  import Label from "../../Models/DatabaseModels/Label";
6
6
  import { OnCreate } from "../Types/Database/Hooks";
7
7
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
8
+ import ResourceHeartbeat from "../Utils/Telemetry/ResourceHeartbeat";
8
9
  import ObjectID from "../../Types/ObjectID";
9
10
  import QueryHelper from "../Types/Database/QueryHelper";
10
11
  import OneUptimeDate from "../../Types/Date";
@@ -173,7 +174,6 @@ export class Service extends DatabaseService<Model> {
173
174
  onlineDeviceCount?: number | undefined;
174
175
  },
175
176
  ): Promise<void> {
176
- const cacheKey: string = fleetId.toString();
177
177
  const extrasFingerprint: string = crypto
178
178
  .createHash("sha1")
179
179
  .update(
@@ -185,34 +185,6 @@ export class Service extends DatabaseService<Model> {
185
185
  )
186
186
  .digest("hex");
187
187
 
188
- let cached: string | null = null;
189
- try {
190
- cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
191
- } catch {
192
- /*
193
- * Cache unavailable — fail open and refresh lastSeenAt anyway. A
194
- * cache error must never skip the DB write below, otherwise the
195
- * resource is wrongly marked "disconnected" while telemetry is
196
- * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
197
- */
198
- cached = null;
199
- }
200
-
201
- if (cached === extrasFingerprint) {
202
- return; // same data was written recently
203
- }
204
-
205
- try {
206
- await GlobalCache.setString(
207
- LAST_SEEN_CACHE_NAMESPACE,
208
- cacheKey,
209
- extrasFingerprint,
210
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
211
- );
212
- } catch {
213
- // Best-effort throttle write; proceed with the DB update regardless.
214
- }
215
-
216
188
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
189
  const liveness: any = {
218
190
  lastSeenAt: OneUptimeDate.getCurrentDate(),
@@ -220,67 +192,35 @@ export class Service extends DatabaseService<Model> {
220
192
  };
221
193
 
222
194
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
223
- const data: any = { ...liveness };
195
+ const metadata: any = {};
224
196
 
225
197
  if (extra?.agentVersion) {
226
- data.agentVersion = extra.agentVersion;
198
+ metadata.agentVersion = extra.agentVersion;
227
199
  }
228
200
  // Counts: 0 is a legitimate value — gate on undefined, not falsiness.
229
201
  if (extra?.deviceCount !== undefined) {
230
- data.deviceCount = extra.deviceCount;
202
+ metadata.deviceCount = extra.deviceCount;
231
203
  }
232
204
  if (extra?.onlineDeviceCount !== undefined) {
233
- data.onlineDeviceCount = extra.onlineDeviceCount;
205
+ metadata.onlineDeviceCount = extra.onlineDeviceCount;
234
206
  }
235
207
 
236
208
  /*
237
- * Heartbeat write: a single-statement UPDATE with no hooks and no
238
- * `version` bump, avoiding the hot-row Postgres lock convoy that the
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.
209
+ * One gated, non-blocking heartbeat write. The gates, the fail-open /
210
+ * fail-closed split and the liveness-only fallback all live in
211
+ * ResourceHeartbeat see there for why this row's throttle used to
212
+ * provide no throttling at all.
244
213
  */
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
- }
214
+ await ResourceHeartbeat.write({
215
+ service: this,
216
+ id: fleetId,
217
+ cacheNamespace: LAST_SEEN_CACHE_NAMESPACE,
218
+ throttleInSeconds: LAST_SEEN_THROTTLE_SECONDS,
219
+ liveness: liveness,
220
+ metadata: metadata,
221
+ fingerprint: extrasFingerprint,
222
+ describe: `iot fleet ${fleetId.toString()}`,
223
+ });
284
224
  }
285
225
 
286
226
  /**