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