@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/KubernetesCluster";
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";
@@ -145,7 +146,6 @@ export class Service extends DatabaseService<Model> {
145
146
  agentVersion?: string | undefined;
146
147
  },
147
148
  ): Promise<void> {
148
- const cacheKey: string = clusterId.toString();
149
149
  const extrasFingerprint: string = crypto
150
150
  .createHash("sha1")
151
151
  .update(
@@ -155,34 +155,6 @@ export class Service extends DatabaseService<Model> {
155
155
  )
156
156
  .digest("hex");
157
157
 
158
- let cached: string | null = null;
159
- try {
160
- cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
161
- } catch {
162
- /*
163
- * Cache unavailable — fail open and refresh lastSeenAt anyway. A
164
- * cache error must never skip the DB write below, otherwise the
165
- * resource is wrongly marked "disconnected" while telemetry is
166
- * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
167
- */
168
- cached = null;
169
- }
170
-
171
- if (cached === extrasFingerprint) {
172
- return; // same data was written recently
173
- }
174
-
175
- try {
176
- await GlobalCache.setString(
177
- LAST_SEEN_CACHE_NAMESPACE,
178
- cacheKey,
179
- extrasFingerprint,
180
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
181
- );
182
- } catch {
183
- // Best-effort throttle write; proceed with the DB update regardless.
184
- }
185
-
186
158
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
187
159
  const liveness: any = {
188
160
  lastSeenAt: OneUptimeDate.getCurrentDate(),
@@ -190,61 +162,28 @@ export class Service extends DatabaseService<Model> {
190
162
  };
191
163
 
192
164
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
- const data: any = { ...liveness };
165
+ const metadata: any = {};
194
166
 
195
167
  if (extra?.agentVersion) {
196
- data.agentVersion = extra.agentVersion;
168
+ metadata.agentVersion = extra.agentVersion;
197
169
  }
198
170
 
199
171
  /*
200
- * Heartbeat write: a single-statement UPDATE with no hooks and no
201
- * `version` bump, avoiding the hot-row Postgres lock convoy that the
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.
172
+ * One gated, non-blocking heartbeat write. The gates, the fail-open /
173
+ * fail-closed split and the liveness-only fallback all live in
174
+ * ResourceHeartbeat see there for why this row's throttle used to
175
+ * provide no throttling at all.
207
176
  */
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
- }
177
+ await ResourceHeartbeat.write({
178
+ service: this,
179
+ id: clusterId,
180
+ cacheNamespace: LAST_SEEN_CACHE_NAMESPACE,
181
+ throttleInSeconds: LAST_SEEN_THROTTLE_SECONDS,
182
+ liveness: liveness,
183
+ metadata: metadata,
184
+ fingerprint: extrasFingerprint,
185
+ describe: `kubernetes cluster ${clusterId.toString()}`,
186
+ });
248
187
  }
249
188
 
250
189
  /**
@@ -5,7 +5,10 @@ import QueryHelper from "../Types/Database/QueryHelper";
5
5
  import DatabaseService from "./DatabaseService";
6
6
  import ServiceLevelObjectiveMonitorRuleEngineService from "./ServiceLevelObjectiveMonitorRuleEngineService";
7
7
  import ServiceLevelObjectiveService from "./ServiceLevelObjectiveService";
8
+ import StatusPageMonitorRuleEngineService from "./StatusPageMonitorRuleEngineService";
9
+ import StatusPageMonitorRuleService from "./StatusPageMonitorRuleService";
8
10
  import ServiceLevelObjective from "../../Models/DatabaseModels/ServiceLevelObjective";
11
+ import StatusPageMonitorRule from "../../Models/DatabaseModels/StatusPageMonitorRule";
9
12
  import BadDataException from "../../Types/Exception/BadDataException";
10
13
  import LIMIT_MAX from "../../Types/Database/LimitMax";
11
14
  import ObjectID from "../../Types/ObjectID";
@@ -65,16 +68,17 @@ export class Service extends DatabaseService<Model> {
65
68
 
66
69
  /*
67
70
  * 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.
71
+ * service hook ever fires for the SLO label rules or status page monitor
72
+ * rules that referenced it. Those would keep the monitors the rule attached,
73
+ * with nothing left to explain why. Note the ids down while the label still
74
+ * exists, and re-run the (now smaller) rules once it is gone.
72
75
  */
73
76
  @CaptureSpan()
74
77
  protected override async onBeforeDelete(
75
78
  deleteBy: DeleteBy<Model>,
76
79
  ): Promise<OnDelete<Model>> {
77
80
  let serviceLevelObjectiveIds: Array<ObjectID> = [];
81
+ let statusPageMonitorRuleIds: Array<ObjectID> = [];
78
82
 
79
83
  try {
80
84
  const labels: Array<Model> = await this.findBy({
@@ -120,10 +124,33 @@ export class Service extends DatabaseService<Model> {
120
124
  .filter((id: ObjectID | null): id is ObjectID => {
121
125
  return Boolean(id);
122
126
  });
127
+
128
+ const statusPageMonitorRules: Array<StatusPageMonitorRule> =
129
+ await StatusPageMonitorRuleService.findBy({
130
+ query: {
131
+ monitorLabels: labelIds,
132
+ },
133
+ select: {
134
+ _id: true,
135
+ },
136
+ limit: LIMIT_MAX,
137
+ skip: 0,
138
+ props: {
139
+ isRoot: true,
140
+ },
141
+ });
142
+
143
+ statusPageMonitorRuleIds = statusPageMonitorRules
144
+ .map((rule: StatusPageMonitorRule) => {
145
+ return rule.id;
146
+ })
147
+ .filter((id: ObjectID | null): id is ObjectID => {
148
+ return Boolean(id);
149
+ });
123
150
  }
124
151
  } catch (err) {
125
152
  logger.error(
126
- `Error collecting SLO label rules affected by a label delete: ${err}`,
153
+ `Error collecting label rules affected by a label delete: ${err}`,
127
154
  );
128
155
  }
129
156
 
@@ -131,6 +158,7 @@ export class Service extends DatabaseService<Model> {
131
158
  deleteBy,
132
159
  carryForward: {
133
160
  serviceLevelObjectiveIds: serviceLevelObjectiveIds,
161
+ statusPageMonitorRuleIds: statusPageMonitorRuleIds,
134
162
  },
135
163
  };
136
164
  }
@@ -140,12 +168,16 @@ export class Service extends DatabaseService<Model> {
140
168
  onDelete: OnDelete<Model>,
141
169
  _itemIdsBeforeDelete: Array<ObjectID>,
142
170
  ): Promise<OnDelete<Model>> {
171
+ const carryForward: {
172
+ serviceLevelObjectiveIds?: Array<ObjectID>;
173
+ statusPageMonitorRuleIds?: Array<ObjectID>;
174
+ } | null = onDelete.carryForward as {
175
+ serviceLevelObjectiveIds?: Array<ObjectID>;
176
+ statusPageMonitorRuleIds?: Array<ObjectID>;
177
+ } | null;
178
+
143
179
  const serviceLevelObjectiveIds: Array<ObjectID> =
144
- (
145
- onDelete.carryForward as {
146
- serviceLevelObjectiveIds?: Array<ObjectID>;
147
- } | null
148
- )?.serviceLevelObjectiveIds || [];
180
+ carryForward?.serviceLevelObjectiveIds || [];
149
181
 
150
182
  for (const serviceLevelObjectiveId of serviceLevelObjectiveIds) {
151
183
  try {
@@ -159,6 +191,21 @@ export class Service extends DatabaseService<Model> {
159
191
  }
160
192
  }
161
193
 
194
+ const statusPageMonitorRuleIds: Array<ObjectID> =
195
+ carryForward?.statusPageMonitorRuleIds || [];
196
+
197
+ for (const statusPageMonitorRuleId of statusPageMonitorRuleIds) {
198
+ try {
199
+ await StatusPageMonitorRuleEngineService.syncResourcesForRule({
200
+ statusPageMonitorRuleId: statusPageMonitorRuleId,
201
+ });
202
+ } catch (err) {
203
+ logger.error(
204
+ `Error re-applying status page monitor rule ${statusPageMonitorRuleId.toString()} after a label delete: ${err}`,
205
+ );
206
+ }
207
+ }
208
+
162
209
  return onDelete;
163
210
  }
164
211
 
@@ -1,10 +1,94 @@
1
1
  import DatabaseService from "./DatabaseService";
2
2
  import Model from "../../Models/DatabaseModels/MetricType";
3
+ import ObjectID from "../../Types/ObjectID";
4
+ import BadDataException from "../../Types/Exception/BadDataException";
5
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
6
+ import { RelationMetadata } from "typeorm/metadata/RelationMetadata";
3
7
 
4
8
  export class Service extends DatabaseService<Model> {
5
9
  public constructor() {
6
10
  super(Model);
7
11
  }
12
+
13
+ /*
14
+ * Additively associate services with a metric type, in ONE statement.
15
+ *
16
+ * This replaces routing the association through `updateOneById` with the
17
+ * whole `services` array in the payload. That looked innocuous and was not:
18
+ * `services` is a `TableColumnType.EntityArray`, and its mere PRESENCE as a
19
+ * key flips `hasRelationUpdates` in DatabaseService, which routes the write
20
+ * to `getRepository().save()` — a real BEGIN/COMMIT that reloads the entity,
21
+ * loads the relation ids, bumps `version`, and DELETEs then re-INSERTs
22
+ * junction rows, holding the MetricType row's write lock across every one of
23
+ * those round trips. On the ingest path, where this runs per metric name per
24
+ * batch with no backpressure, it was the only multi-round-trip lock hold in
25
+ * the pipeline.
26
+ *
27
+ * The whole-array write was also silently LOSING data. `save()` diffs the
28
+ * array it is given against what is currently in the database and deletes
29
+ * anything missing — but each ingest worker only knows the services in ITS
30
+ * batch. Two workers with different batches therefore deleted each other's
31
+ * associations and re-inserted them on the next batch: permanent junction
32
+ * churn, and real service-to-metric links disappearing from the UI in
33
+ * between. Additive insert makes that impossible to express.
34
+ *
35
+ * ON CONFLICT DO NOTHING is well-defined here: the junction's primary key is
36
+ * exactly (metricTypeId, serviceId), so a concurrent writer adding the same
37
+ * association is a no-op rather than a unique violation.
38
+ */
39
+ @CaptureSpan()
40
+ public async attachServices(data: {
41
+ metricTypeId: ObjectID;
42
+ serviceIds: Array<ObjectID>;
43
+ }): Promise<void> {
44
+ if (!data.metricTypeId) {
45
+ throw new BadDataException("metricTypeId is required");
46
+ }
47
+
48
+ if (!data.serviceIds || data.serviceIds.length === 0) {
49
+ return;
50
+ }
51
+
52
+ /*
53
+ * Identifiers come from entity metadata, never from the caller, and every
54
+ * value is bound as a parameter — the same rule the other raw-SQL paths in
55
+ * DatabaseService follow.
56
+ */
57
+ const relation: RelationMetadata | undefined =
58
+ this.getRepository().metadata.findRelationWithPropertyPath("services");
59
+
60
+ const junction: string | undefined =
61
+ relation?.junctionEntityMetadata?.tableName;
62
+ const metricTypeColumn: string | undefined =
63
+ relation?.junctionEntityMetadata?.columns[0]?.databaseName;
64
+ const serviceColumn: string | undefined =
65
+ relation?.junctionEntityMetadata?.columns[1]?.databaseName;
66
+
67
+ if (!junction || !metricTypeColumn || !serviceColumn) {
68
+ throw new BadDataException(
69
+ "MetricTypeService.attachServices: the services relation has no junction metadata",
70
+ );
71
+ }
72
+
73
+ /*
74
+ * Deduplicate and SORT. Concurrent statements that insert overlapping sets
75
+ * acquire their row locks in the same order this way, which is what keeps
76
+ * two workers associating the same services from deadlocking each other.
77
+ */
78
+ const serviceIds: Array<string> = Array.from(
79
+ new Set(
80
+ data.serviceIds.map((id: ObjectID) => {
81
+ return id.toString();
82
+ }),
83
+ ),
84
+ ).sort();
85
+
86
+ await this.getRepository().manager.query(
87
+ `INSERT INTO "${junction}" ("${metricTypeColumn}", "${serviceColumn}") ` +
88
+ `SELECT $1, unnest($2::uuid[]) ON CONFLICT DO NOTHING`,
89
+ [data.metricTypeId.toString(), serviceIds],
90
+ );
91
+ }
8
92
  }
9
93
 
10
94
  export default new Service();
@@ -15,6 +15,7 @@ import MonitorOwnerUserService from "./MonitorOwnerUserService";
15
15
  import MonitorProbeService from "./MonitorProbeService";
16
16
  import MonitorStatusService from "./MonitorStatusService";
17
17
  import ServiceLevelObjectiveMonitorRuleEngineService from "./ServiceLevelObjectiveMonitorRuleEngineService";
18
+ import StatusPageMonitorRuleEngineService from "./StatusPageMonitorRuleEngineService";
18
19
  import NetworkSiteService from "./NetworkSiteService";
19
20
  import MonitorStatusTimelineService, {
20
21
  MONITOR_STATUS_SAME_AS_PREVIOUS_ERROR_MESSAGE,
@@ -404,27 +405,47 @@ export class Service extends DatabaseService<Model> {
404
405
  resolveReferenceId(updateBy.data.currentMonitorStatusId) ||
405
406
  resolveReferenceId(updateBy.data.currentMonitorStatus);
406
407
 
407
- if (updateBy.data.monitorSteps || currentMonitorStatusId) {
408
+ if (updateBy.data.monitorSteps) {
408
409
  /*
409
- * Root/API updates do not always carry a tenantId, so fall back to the
410
- * project of each monitor the query actually matches.
410
+ * Validated per matched monitor rather than per distinct project, because
411
+ * the check needs that monitor's CURRENT monitorSteps: a reference id it
412
+ * already holds is exempt from the existence check, so an update never
413
+ * refuses a monitor that was stored broken before the guard existed. See
414
+ * MonitorStepsProjectValidator.
411
415
  */
416
+ const monitors: Array<Model> = await this.findBy({
417
+ query: updateBy.query,
418
+ select: {
419
+ projectId: true,
420
+ monitorSteps: true,
421
+ },
422
+ limit: LIMIT_MAX,
423
+ skip: 0,
424
+ props: {
425
+ isRoot: true,
426
+ ignoreHooks: true,
427
+ },
428
+ });
429
+
430
+ for (const monitor of monitors) {
431
+ await MonitorStepsProjectValidator.validateMonitorStepsBelongToProject({
432
+ monitorSteps: updateBy.data.monitorSteps as MonitorSteps | JSONObject,
433
+ /*
434
+ * Root/API updates do not always carry a tenantId, so fall back to
435
+ * the project of the monitor being updated.
436
+ */
437
+ projectId: updateBy.props.tenantId || monitor.projectId,
438
+ alreadyStoredMonitorSteps: monitor.monitorSteps,
439
+ });
440
+ }
441
+ }
442
+
443
+ if (currentMonitorStatusId) {
412
444
  const projectIds: Array<ObjectID> = updateBy.props.tenantId
413
445
  ? [updateBy.props.tenantId]
414
446
  : await this.getProjectIdsForUpdateQuery(updateBy);
415
447
 
416
448
  for (const projectId of projectIds) {
417
- if (updateBy.data.monitorSteps) {
418
- await MonitorStepsProjectValidator.validateMonitorStepsBelongToProject(
419
- {
420
- monitorSteps: updateBy.data.monitorSteps as
421
- | MonitorSteps
422
- | JSONObject,
423
- projectId: projectId,
424
- },
425
- );
426
- }
427
-
428
449
  await ProjectScopedReferenceValidator.validateReferencesBelongToProject(
429
450
  {
430
451
  projectId: projectId,
@@ -644,6 +665,37 @@ export class Service extends DatabaseService<Model> {
644
665
  }
645
666
  }
646
667
 
668
+ /*
669
+ * Status page monitor rules match on labels, name and description, so an
670
+ * edit to any of the three can pull this monitor onto a status page or
671
+ * push it off one. Keyed on `!== undefined` for the same reason as the SLO
672
+ * block above: clearing every label arrives as `[]`, and that is exactly
673
+ * the edit that should detach the monitor from every label-driven rule.
674
+ */
675
+ if (
676
+ (onUpdate.updateBy.data.labels !== undefined ||
677
+ onUpdate.updateBy.data.name !== undefined ||
678
+ onUpdate.updateBy.data.description !== undefined) &&
679
+ updatedItemIds.length > 0
680
+ ) {
681
+ for (const monitorId of updatedItemIds) {
682
+ try {
683
+ await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
684
+ monitorId: monitorId,
685
+ projectId: onUpdate.updateBy.props.tenantId as ObjectID,
686
+ });
687
+ } catch (error) {
688
+ logger.error(
689
+ "Syncing status page monitor rules failed in MonitorService.onUpdateSuccess",
690
+ {
691
+ monitorId: monitorId?.toString(),
692
+ } as LogAttributes,
693
+ );
694
+ logger.error(error as Error);
695
+ }
696
+ }
697
+ }
698
+
647
699
  return onUpdate;
648
700
  }
649
701
 
@@ -1040,6 +1092,29 @@ ${createdItem.description?.trim() || "No description provided."}
1040
1092
  }
1041
1093
  return Promise.resolve();
1042
1094
  })
1095
+ .then(async () => {
1096
+ /*
1097
+ * Also runs after the label rules above, so a monitor that only earns
1098
+ * its labels from a MonitorLabelRule still lands on the status pages
1099
+ * those labels imply.
1100
+ */
1101
+ try {
1102
+ await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
1103
+ monitorId: createdItem.id!,
1104
+ projectId: createdItem.projectId!,
1105
+ });
1106
+ } catch (error) {
1107
+ logger.error(
1108
+ "Syncing status page monitor rules failed in MonitorService.onCreateSuccess",
1109
+ {
1110
+ projectId: createdItem.projectId?.toString(),
1111
+ monitorId: createdItem.id?.toString(),
1112
+ } as LogAttributes,
1113
+ );
1114
+ logger.error(error as Error);
1115
+ }
1116
+ return Promise.resolve();
1117
+ })
1043
1118
  .then(async () => {
1044
1119
  try {
1045
1120
  return await this.refreshMonitorProbeStatus(createdItem.id!);
@@ -4,6 +4,7 @@ import DeleteBy from "../Types/Database/DeleteBy";
4
4
  import { OnCreate, OnDelete } from "../Types/Database/Hooks";
5
5
  import QueryHelper from "../Types/Database/QueryHelper";
6
6
  import logger, { LogAttributes } from "../Utils/Logger";
7
+ import ProjectScopedReferenceValidator from "../Utils/Database/ProjectScopedReferenceValidator";
7
8
  import DatabaseService from "./DatabaseService";
8
9
  import MonitorService from "./MonitorService";
9
10
  import UserService from "./UserService";
@@ -191,6 +192,34 @@ export class Service extends DatabaseService<MonitorStatusTimeline> {
191
192
  throw new BadDataException("monitorStatusId is null");
192
193
  }
193
194
 
195
+ /*
196
+ * Every writer of this table funnels through here, so this is the one place
197
+ * that can turn issue #3039's failure mode into an answer. A monitorStatusId
198
+ * that does not exist (or belongs to another project) used to travel all the
199
+ * way to Postgres and come back as
200
+ * insert or update on table "MonitorStatusTimeline" violates foreign key
201
+ * constraint "FK_574feb4161c5216c2c7ee0faaf8"
202
+ * — an opaque 500 over the API, and a job that fails and retries forever in
203
+ * the probe worker. Reject it here instead, naming the id.
204
+ *
205
+ * The probe/telemetry ingest callers screen the id themselves before they
206
+ * get here (Utils/Monitor/MonitorStatusTimeline and Utils/Monitor/MonitorResource)
207
+ * so a monitor whose stored criteria already point at a deleted status skip
208
+ * the status change rather than failing their whole run. This check is the
209
+ * backstop for everything else, chiefly direct API writes.
210
+ */
211
+ await ProjectScopedReferenceValidator.validateReferencesBelongToProject({
212
+ projectId: createBy.props.tenantId || createBy.data.projectId,
213
+ subject: "monitor status timeline",
214
+ references: [
215
+ {
216
+ modelName: "Monitor Status",
217
+ id: monitorStatusId,
218
+ service: MonitorStatusService,
219
+ },
220
+ ],
221
+ });
222
+
194
223
  const stateBeforeThis: MonitorStatusTimeline | null = await this.findOneBy({
195
224
  query: {
196
225
  monitorId: monitorId,
@@ -1,8 +1,14 @@
1
1
  import DatabaseService from "./DatabaseService";
2
2
  import MonitorService from "./MonitorService";
3
+ import CreateBy from "../Types/Database/CreateBy";
4
+ import UpdateBy from "../Types/Database/UpdateBy";
5
+ import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
6
+ import MonitorStepsProjectValidator from "../Utils/Monitor/MonitorStepsProjectValidator";
3
7
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
4
8
  import BadDataException from "../../Types/Exception/BadDataException";
5
9
  import LIMIT_MAX from "../../Types/Database/LimitMax";
10
+ import { JSONObject } from "../../Types/JSON";
11
+ import MonitorSteps from "../../Types/Monitor/MonitorSteps";
6
12
  import ObjectID from "../../Types/ObjectID";
7
13
  import PositiveNumber from "../../Types/PositiveNumber";
8
14
  import Model from "../../Models/DatabaseModels/MonitorTemplate";
@@ -37,6 +43,65 @@ export class Service extends DatabaseService<Model> {
37
43
  super(Model);
38
44
  }
39
45
 
46
+ /*
47
+ * A template's monitorSteps embeds the same reference ids a monitor's does,
48
+ * and every one of them reaches a real monitor eventually — through "create
49
+ * monitor from template" and through syncLinkedMonitors, which pushes the
50
+ * blob onto every linked monitor. Validating it here is what makes the error
51
+ * land on the template the bad id was typed into, rather than on a monitor
52
+ * sync days later. Without this the template is the one place a dangling id
53
+ * can still enter the system.
54
+ */
55
+ @CaptureSpan()
56
+ protected override async onBeforeCreate(
57
+ createBy: CreateBy<Model>,
58
+ ): Promise<OnCreate<Model>> {
59
+ await MonitorStepsProjectValidator.validateMonitorStepsBelongToProject({
60
+ monitorSteps: createBy.data.monitorSteps,
61
+ projectId: createBy.props.tenantId || createBy.data.projectId,
62
+ });
63
+
64
+ return { createBy, carryForward: null };
65
+ }
66
+
67
+ @CaptureSpan()
68
+ protected override async onBeforeUpdate(
69
+ updateBy: UpdateBy<Model>,
70
+ ): Promise<OnUpdate<Model>> {
71
+ if (!updateBy.data.monitorSteps) {
72
+ return { updateBy, carryForward: null };
73
+ }
74
+
75
+ /*
76
+ * Per matched template, so each is checked against its own project and its
77
+ * own currently-stored ids — see MonitorService.onBeforeUpdate for why the
78
+ * stored ids matter.
79
+ */
80
+ const templates: Array<Model> = await this.findBy({
81
+ query: updateBy.query,
82
+ select: {
83
+ projectId: true,
84
+ monitorSteps: true,
85
+ },
86
+ limit: LIMIT_MAX,
87
+ skip: 0,
88
+ props: {
89
+ isRoot: true,
90
+ ignoreHooks: true,
91
+ },
92
+ });
93
+
94
+ for (const template of templates) {
95
+ await MonitorStepsProjectValidator.validateMonitorStepsBelongToProject({
96
+ monitorSteps: updateBy.data.monitorSteps as MonitorSteps | JSONObject,
97
+ projectId: updateBy.props.tenantId || template.projectId,
98
+ alreadyStoredMonitorSteps: template.monitorSteps,
99
+ });
100
+ }
101
+
102
+ return { updateBy, carryForward: null };
103
+ }
104
+
40
105
  /**
41
106
  * Count monitors created from this template.
42
107
  * Caller must already have read access on the template via the API layer.