@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.
- package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/LogDropFilter.ts +9 -20
- package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
- package/Models/DatabaseModels/StatusPageResource.ts +73 -0
- package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
- package/Server/API/BaseAPI.ts +13 -2
- package/Server/API/DashboardAPI.ts +39 -0
- package/Server/EnvironmentConfig.ts +31 -1
- package/Server/Infrastructure/GlobalCache.ts +115 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertStateTimelineService.ts +20 -0
- package/Server/Services/CephClusterService.ts +26 -86
- package/Server/Services/CloudResourceService.ts +23 -83
- package/Server/Services/DatabaseService.ts +111 -0
- package/Server/Services/DockerHostService.ts +19 -79
- package/Server/Services/DockerSwarmClusterService.ts +27 -88
- package/Server/Services/HostService.ts +46 -99
- package/Server/Services/IncidentStateTimelineService.ts +20 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/IoTFleetService.ts +19 -79
- package/Server/Services/KubernetesClusterService.ts +17 -78
- package/Server/Services/LabelService.ts +57 -10
- package/Server/Services/MetricTypeService.ts +84 -0
- package/Server/Services/MonitorService.ts +89 -14
- package/Server/Services/MonitorStatusTimelineService.ts +29 -0
- package/Server/Services/MonitorTemplateService.ts +65 -0
- package/Server/Services/PodmanHostService.ts +19 -79
- package/Server/Services/ProxmoxClusterService.ts +23 -83
- package/Server/Services/RumApplicationService.ts +19 -80
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
- package/Server/Services/ServerlessFunctionService.ts +24 -85
- package/Server/Services/ServiceService.ts +299 -73
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
- package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
- package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
- package/Server/Services/TelemetryEntityService.ts +1 -0
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
- package/Server/Utils/Monitor/MonitorResource.ts +63 -36
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
- package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
- package/Server/Utils/SingleFlight.ts +83 -0
- package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
- package/Server/Utils/Telemetry/Telemetry.ts +351 -122
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
- package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
- package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
- package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
- package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
- package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
- package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
- package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
- package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
- package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
- package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
- package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
- package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
- package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
- package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
- package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
- package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
- package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
- package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
- package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
- package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
- package/Tests/Types/Decimal.test.ts +37 -0
- package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
- package/Tests/Types/Port.test.ts +38 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
- package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
- package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
- package/Types/Dashboard/DashboardTemplates.ts +397 -0
- package/Types/Database/BigIntColumnTransformer.ts +74 -0
- package/Types/Database/DatabaseProperty.ts +26 -0
- package/Types/Database/NumericColumnValue.ts +128 -0
- package/Types/Decimal.ts +16 -4
- package/Types/Permission.ts +48 -0
- package/Types/Port.ts +16 -4
- package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
- package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
- package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +11 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +39 -0
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +27 -1
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +26 -76
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +23 -73
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +86 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +19 -69
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +58 -102
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +19 -69
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +42 -7
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MetricTypeService.js +74 -0
- package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +81 -9
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +19 -69
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +19 -70
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +236 -61
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
- package/build/dist/Server/Utils/SingleFlight.js +76 -0
- package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
- package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
- package/build/dist/Types/Database/DatabaseProperty.js +25 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/NumericColumnValue.js +97 -0
- package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
- package/build/dist/Types/Decimal.js +14 -3
- package/build/dist/Types/Decimal.js.map +1 -1
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Port.js +15 -5
- package/build/dist/Types/Port.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- 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
|
|
165
|
+
const metadata: any = {};
|
|
194
166
|
|
|
195
167
|
if (extra?.agentVersion) {
|
|
196
|
-
|
|
168
|
+
metadata.agentVersion = extra.agentVersion;
|
|
197
169
|
}
|
|
198
170
|
|
|
199
171
|
/*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
69
|
-
*
|
|
70
|
-
* explain why. Note the ids down while the label still
|
|
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
|
|
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
|
|
408
|
+
if (updateBy.data.monitorSteps) {
|
|
408
409
|
/*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
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.
|