@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
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
import Label from "../../../Models/DatabaseModels/Label";
|
|
2
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
3
|
+
import ServiceLevelObjective from "../../../Models/DatabaseModels/ServiceLevelObjective";
|
|
4
|
+
import MonitorService from "../../../Server/Services/MonitorService";
|
|
5
|
+
import ServiceLevelObjectiveMonitorRuleEngineService, {
|
|
6
|
+
SloMonitorSyncResult,
|
|
7
|
+
} from "../../../Server/Services/ServiceLevelObjectiveMonitorRuleEngineService";
|
|
8
|
+
import ServiceLevelObjectiveService from "../../../Server/Services/ServiceLevelObjectiveService";
|
|
9
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
10
|
+
import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Contract under test - the SLO monitor label rule.
|
|
14
|
+
*
|
|
15
|
+
* An SLO can say "measure every monitor labelled Production" instead of
|
|
16
|
+
* naming monitors one by one. That promise has two halves, and both have to
|
|
17
|
+
* hold or the SLO quietly measures the wrong thing:
|
|
18
|
+
*
|
|
19
|
+
* - a monitor that starts matching gets attached (whether the rule moved or
|
|
20
|
+
* the monitor's labels did), and a monitor that stops matching gets
|
|
21
|
+
* detached again;
|
|
22
|
+
*
|
|
23
|
+
* - a monitor a human attached by hand is never touched. It is not adopted
|
|
24
|
+
* into the rule when it happens to match, and - the case that would
|
|
25
|
+
* actually lose someone's configuration - it is never detached when it
|
|
26
|
+
* stops matching, or when the rule is deleted outright.
|
|
27
|
+
*
|
|
28
|
+
* The second half is what ServiceLevelObjective.autoAddedMonitors exists for,
|
|
29
|
+
* so most of what follows is about that boundary.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
33
|
+
"22222222-2222-4222-8222-222222222222",
|
|
34
|
+
);
|
|
35
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
36
|
+
"23232323-2323-4323-8323-232323232323",
|
|
37
|
+
);
|
|
38
|
+
const SLO_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
|
|
39
|
+
const OTHER_SLO_ID: ObjectID = new ObjectID(
|
|
40
|
+
"1a1a1a1a-1a1a-4a1a-8a1a-1a1a1a1a1a1a",
|
|
41
|
+
);
|
|
42
|
+
const MONITOR_A_ID: ObjectID = new ObjectID(
|
|
43
|
+
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
44
|
+
);
|
|
45
|
+
const MONITOR_B_ID: ObjectID = new ObjectID(
|
|
46
|
+
"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
|
47
|
+
);
|
|
48
|
+
const MONITOR_C_ID: ObjectID = new ObjectID(
|
|
49
|
+
"cccccccc-cccc-4ccc-8ccc-cccccccccccc",
|
|
50
|
+
);
|
|
51
|
+
const LABEL_PRODUCTION_ID: ObjectID = new ObjectID(
|
|
52
|
+
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
|
53
|
+
);
|
|
54
|
+
const LABEL_TIER1_ID: ObjectID = new ObjectID(
|
|
55
|
+
"eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
|
56
|
+
);
|
|
57
|
+
const LABEL_STAGING_ID: ObjectID = new ObjectID(
|
|
58
|
+
"ffffffff-ffff-4fff-8fff-ffffffffffff",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
function fakeLabel(id: ObjectID): Label {
|
|
62
|
+
return { id: id, _id: id.toString() } as unknown as Label;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fakeMonitor(id: ObjectID): Monitor {
|
|
66
|
+
return { id: id, _id: id.toString() } as unknown as Monitor;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fakeSlo(fields: {
|
|
70
|
+
id?: ObjectID | undefined;
|
|
71
|
+
projectId?: ObjectID | undefined;
|
|
72
|
+
monitorLabels?: Array<ObjectID> | undefined;
|
|
73
|
+
monitors?: Array<ObjectID> | undefined;
|
|
74
|
+
autoAddedMonitors?: Array<ObjectID> | undefined;
|
|
75
|
+
}): ServiceLevelObjective {
|
|
76
|
+
const id: ObjectID = fields.id || SLO_ID;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
id: id,
|
|
80
|
+
_id: id.toString(),
|
|
81
|
+
projectId:
|
|
82
|
+
fields.projectId === undefined ? PROJECT_ID : fields.projectId || null,
|
|
83
|
+
monitorLabels: (fields.monitorLabels || []).map(fakeLabel),
|
|
84
|
+
monitors: (fields.monitors || []).map(fakeMonitor),
|
|
85
|
+
autoAddedMonitors: (fields.autoAddedMonitors || []).map(fakeMonitor),
|
|
86
|
+
} as unknown as ServiceLevelObjective;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fakeMonitorWithLabels(
|
|
90
|
+
labelIds: Array<ObjectID>,
|
|
91
|
+
overrides?: {
|
|
92
|
+
id?: ObjectID | undefined;
|
|
93
|
+
projectId?: ObjectID | null | undefined;
|
|
94
|
+
},
|
|
95
|
+
): Monitor {
|
|
96
|
+
return {
|
|
97
|
+
id: overrides?.id || MONITOR_A_ID,
|
|
98
|
+
_id: (overrides?.id || MONITOR_A_ID).toString(),
|
|
99
|
+
projectId:
|
|
100
|
+
overrides?.projectId === undefined ? PROJECT_ID : overrides.projectId,
|
|
101
|
+
labels: labelIds.map(fakeLabel),
|
|
102
|
+
} as unknown as Monitor;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The ids the engine wrote to one of the two monitor relations, sorted so
|
|
107
|
+
* assertions do not depend on Set iteration order.
|
|
108
|
+
*/
|
|
109
|
+
function writtenIds(
|
|
110
|
+
updateOneByIdSpy: jest.SpyInstance,
|
|
111
|
+
relation: "monitors" | "autoAddedMonitors",
|
|
112
|
+
callIndex: number = 0,
|
|
113
|
+
): Array<string> {
|
|
114
|
+
const call: { data: Record<string, Array<Monitor>> } = updateOneByIdSpy.mock
|
|
115
|
+
.calls[callIndex]![0] as {
|
|
116
|
+
data: Record<string, Array<Monitor>>;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return (call.data[relation] || [])
|
|
120
|
+
.map((monitor: Monitor) => {
|
|
121
|
+
return monitor.id?.toString() || "";
|
|
122
|
+
})
|
|
123
|
+
.sort();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writtenSloId(
|
|
127
|
+
updateOneByIdSpy: jest.SpyInstance,
|
|
128
|
+
callIndex: number = 0,
|
|
129
|
+
): string {
|
|
130
|
+
const call: { id: ObjectID } = updateOneByIdSpy.mock.calls[callIndex]![0] as {
|
|
131
|
+
id: ObjectID;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return call.id.toString();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe("ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo", () => {
|
|
138
|
+
let updateOneByIdSpy: jest.SpyInstance;
|
|
139
|
+
let sloFindOneByIdSpy: jest.SpyInstance;
|
|
140
|
+
let monitorFindBySpy: jest.SpyInstance;
|
|
141
|
+
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
updateOneByIdSpy = jest
|
|
144
|
+
.spyOn(ServiceLevelObjectiveService, "updateOneById")
|
|
145
|
+
.mockResolvedValue(1);
|
|
146
|
+
sloFindOneByIdSpy = jest.spyOn(ServiceLevelObjectiveService, "findOneById");
|
|
147
|
+
monitorFindBySpy = jest.spyOn(MonitorService, "findBy");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
jest.restoreAllMocks();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("attaches every monitor in the project that carries a rule label", async () => {
|
|
155
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
156
|
+
fakeSlo({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
157
|
+
);
|
|
158
|
+
monitorFindBySpy.mockResolvedValue([
|
|
159
|
+
fakeMonitor(MONITOR_A_ID),
|
|
160
|
+
fakeMonitor(MONITOR_B_ID),
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const result: SloMonitorSyncResult =
|
|
164
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
165
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(writtenSloId(updateOneByIdSpy)).toBe(SLO_ID.toString());
|
|
170
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual(
|
|
171
|
+
[MONITOR_A_ID.toString(), MONITOR_B_ID.toString()].sort(),
|
|
172
|
+
);
|
|
173
|
+
expect(result.monitorIdsAdded.sort()).toEqual(
|
|
174
|
+
[MONITOR_A_ID.toString(), MONITOR_B_ID.toString()].sort(),
|
|
175
|
+
);
|
|
176
|
+
expect(result.monitorIdsRemoved).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("records what it attached in autoAddedMonitors so it can undo it later", async () => {
|
|
180
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
181
|
+
fakeSlo({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
182
|
+
);
|
|
183
|
+
monitorFindBySpy.mockResolvedValue([fakeMonitor(MONITOR_A_ID)]);
|
|
184
|
+
|
|
185
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
186
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(writtenIds(updateOneByIdSpy, "autoAddedMonitors")).toEqual([
|
|
190
|
+
MONITOR_A_ID.toString(),
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("writes as root - the rule is server-side and must not need the caller's permissions", async () => {
|
|
195
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
196
|
+
fakeSlo({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
197
|
+
);
|
|
198
|
+
monitorFindBySpy.mockResolvedValue([fakeMonitor(MONITOR_A_ID)]);
|
|
199
|
+
|
|
200
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
201
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const call: { props: Record<string, unknown> } = updateOneByIdSpy.mock
|
|
205
|
+
.calls[0]![0] as { props: Record<string, unknown> };
|
|
206
|
+
expect(call.props).toEqual({ isRoot: true });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("matches on any of the rule's labels, not all of them", async () => {
|
|
210
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
211
|
+
fakeSlo({ monitorLabels: [LABEL_PRODUCTION_ID, LABEL_TIER1_ID] }),
|
|
212
|
+
);
|
|
213
|
+
monitorFindBySpy.mockResolvedValue([fakeMonitor(MONITOR_A_ID)]);
|
|
214
|
+
|
|
215
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
216
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const query: { projectId: ObjectID; labels: Array<ObjectID> } =
|
|
220
|
+
monitorFindBySpy.mock.calls[0]![0]!.query;
|
|
221
|
+
|
|
222
|
+
expect(query.projectId).toEqual(PROJECT_ID);
|
|
223
|
+
expect(
|
|
224
|
+
query.labels.map((id: ObjectID) => {
|
|
225
|
+
return id.toString();
|
|
226
|
+
}),
|
|
227
|
+
).toEqual([LABEL_PRODUCTION_ID.toString(), LABEL_TIER1_ID.toString()]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("scopes the monitor lookup to the SLO's own project", async () => {
|
|
231
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
232
|
+
fakeSlo({
|
|
233
|
+
projectId: OTHER_PROJECT_ID,
|
|
234
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
238
|
+
|
|
239
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
240
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(monitorFindBySpy.mock.calls[0]![0]!.query.projectId).toEqual(
|
|
244
|
+
OTHER_PROJECT_ID,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("leaves a hand-attached monitor out of autoAddedMonitors even when it matches the rule", async () => {
|
|
249
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
250
|
+
fakeSlo({
|
|
251
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
252
|
+
monitors: [MONITOR_A_ID],
|
|
253
|
+
autoAddedMonitors: [],
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
// Monitor A matches the rule, but a human put it there first.
|
|
257
|
+
monitorFindBySpy.mockResolvedValue([
|
|
258
|
+
fakeMonitor(MONITOR_A_ID),
|
|
259
|
+
fakeMonitor(MONITOR_B_ID),
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
const result: SloMonitorSyncResult =
|
|
263
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
264
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(result.monitorIdsAdded).toEqual([MONITOR_B_ID.toString()]);
|
|
268
|
+
expect(writtenIds(updateOneByIdSpy, "autoAddedMonitors")).toEqual([
|
|
269
|
+
MONITOR_B_ID.toString(),
|
|
270
|
+
]);
|
|
271
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual(
|
|
272
|
+
[MONITOR_A_ID.toString(), MONITOR_B_ID.toString()].sort(),
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("detaches a monitor it had attached once the monitor stops matching", async () => {
|
|
277
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
278
|
+
fakeSlo({
|
|
279
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
280
|
+
monitors: [MONITOR_A_ID, MONITOR_B_ID],
|
|
281
|
+
autoAddedMonitors: [MONITOR_A_ID, MONITOR_B_ID],
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
monitorFindBySpy.mockResolvedValue([fakeMonitor(MONITOR_A_ID)]);
|
|
285
|
+
|
|
286
|
+
const result: SloMonitorSyncResult =
|
|
287
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
288
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(result.monitorIdsRemoved).toEqual([MONITOR_B_ID.toString()]);
|
|
292
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual([
|
|
293
|
+
MONITOR_A_ID.toString(),
|
|
294
|
+
]);
|
|
295
|
+
expect(writtenIds(updateOneByIdSpy, "autoAddedMonitors")).toEqual([
|
|
296
|
+
MONITOR_A_ID.toString(),
|
|
297
|
+
]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("never detaches a hand-attached monitor, however badly it fails the rule", async () => {
|
|
301
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
302
|
+
fakeSlo({
|
|
303
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
304
|
+
monitors: [MONITOR_A_ID, MONITOR_C_ID],
|
|
305
|
+
autoAddedMonitors: [MONITOR_A_ID],
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
// Neither A nor C matches any more; only A is the rule's to take back.
|
|
309
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
310
|
+
|
|
311
|
+
const result: SloMonitorSyncResult =
|
|
312
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
313
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(result.monitorIdsRemoved).toEqual([MONITOR_A_ID.toString()]);
|
|
317
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual([
|
|
318
|
+
MONITOR_C_ID.toString(),
|
|
319
|
+
]);
|
|
320
|
+
expect(writtenIds(updateOneByIdSpy, "autoAddedMonitors")).toEqual([]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("gives back every rule-attached monitor when the rule is cleared, and keeps the manual ones", async () => {
|
|
324
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
325
|
+
fakeSlo({
|
|
326
|
+
monitorLabels: [],
|
|
327
|
+
monitors: [MONITOR_A_ID, MONITOR_B_ID, MONITOR_C_ID],
|
|
328
|
+
autoAddedMonitors: [MONITOR_A_ID, MONITOR_B_ID],
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const result: SloMonitorSyncResult =
|
|
333
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
334
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// No rule means nothing to look up - the monitor query must not even run.
|
|
338
|
+
expect(monitorFindBySpy).not.toHaveBeenCalled();
|
|
339
|
+
expect(result.monitorIdsRemoved.sort()).toEqual(
|
|
340
|
+
[MONITOR_A_ID.toString(), MONITOR_B_ID.toString()].sort(),
|
|
341
|
+
);
|
|
342
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual([
|
|
343
|
+
MONITOR_C_ID.toString(),
|
|
344
|
+
]);
|
|
345
|
+
expect(writtenIds(updateOneByIdSpy, "autoAddedMonitors")).toEqual([]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("writes nothing when the rule already agrees with the SLO", async () => {
|
|
349
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
350
|
+
fakeSlo({
|
|
351
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
352
|
+
monitors: [MONITOR_A_ID, MONITOR_C_ID],
|
|
353
|
+
autoAddedMonitors: [MONITOR_A_ID],
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
monitorFindBySpy.mockResolvedValue([fakeMonitor(MONITOR_A_ID)]);
|
|
357
|
+
|
|
358
|
+
const result: SloMonitorSyncResult =
|
|
359
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
360
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
364
|
+
expect(result).toEqual({ monitorIdsAdded: [], monitorIdsRemoved: [] });
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("writes nothing for an SLO with neither a rule nor anything it attached", async () => {
|
|
368
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
369
|
+
fakeSlo({ monitorLabels: [], monitors: [MONITOR_C_ID] }),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
373
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("re-attaches a monitor that is recorded as rule-attached but has gone missing from the list", async () => {
|
|
380
|
+
/*
|
|
381
|
+
* Bookkeeping drift, e.g. a monitor removed straight out of the Monitors
|
|
382
|
+
* picker while the rule still claims it. The sync is authoritative and
|
|
383
|
+
* repairs it rather than leaving the two relations disagreeing.
|
|
384
|
+
*/
|
|
385
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
386
|
+
fakeSlo({
|
|
387
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
388
|
+
monitors: [],
|
|
389
|
+
autoAddedMonitors: [MONITOR_A_ID],
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
monitorFindBySpy.mockResolvedValue([fakeMonitor(MONITOR_A_ID)]);
|
|
393
|
+
|
|
394
|
+
const result: SloMonitorSyncResult =
|
|
395
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
396
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(result.monitorIdsAdded).toEqual([MONITOR_A_ID.toString()]);
|
|
400
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual([
|
|
401
|
+
MONITOR_A_ID.toString(),
|
|
402
|
+
]);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("does nothing for an SLO it cannot read", async () => {
|
|
406
|
+
sloFindOneByIdSpy.mockResolvedValue(null);
|
|
407
|
+
|
|
408
|
+
const result: SloMonitorSyncResult =
|
|
409
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
410
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(monitorFindBySpy).not.toHaveBeenCalled();
|
|
414
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
415
|
+
expect(result).toEqual({ monitorIdsAdded: [], monitorIdsRemoved: [] });
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("does nothing for an SLO with no project - there is no tenant to scope the lookup to", async () => {
|
|
419
|
+
sloFindOneByIdSpy.mockResolvedValue(
|
|
420
|
+
fakeSlo({ projectId: null as unknown as ObjectID }),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
424
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(monitorFindBySpy).not.toHaveBeenCalled();
|
|
428
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("reads the relations it needs to decide - the rule, the list, and the bookkeeping", async () => {
|
|
432
|
+
sloFindOneByIdSpy.mockResolvedValue(fakeSlo({ monitorLabels: [] }));
|
|
433
|
+
|
|
434
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
435
|
+
serviceLevelObjectiveId: SLO_ID,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const select: Record<string, unknown> =
|
|
439
|
+
sloFindOneByIdSpy.mock.calls[0]![0]!.select;
|
|
440
|
+
|
|
441
|
+
expect(Object.keys(select).sort()).toEqual([
|
|
442
|
+
"_id",
|
|
443
|
+
"autoAddedMonitors",
|
|
444
|
+
"monitorLabels",
|
|
445
|
+
"monitors",
|
|
446
|
+
"projectId",
|
|
447
|
+
]);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor", () => {
|
|
452
|
+
let updateOneByIdSpy: jest.SpyInstance;
|
|
453
|
+
let sloFindBySpy: jest.SpyInstance;
|
|
454
|
+
let monitorFindOneByIdSpy: jest.SpyInstance;
|
|
455
|
+
|
|
456
|
+
beforeEach(() => {
|
|
457
|
+
updateOneByIdSpy = jest
|
|
458
|
+
.spyOn(ServiceLevelObjectiveService, "updateOneById")
|
|
459
|
+
.mockResolvedValue(1);
|
|
460
|
+
sloFindBySpy = jest.spyOn(ServiceLevelObjectiveService, "findBy");
|
|
461
|
+
monitorFindOneByIdSpy = jest.spyOn(MonitorService, "findOneById");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
afterEach(() => {
|
|
465
|
+
jest.restoreAllMocks();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("attaches the monitor to an SLO whose rule its new labels satisfy", async () => {
|
|
469
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
470
|
+
fakeMonitorWithLabels([LABEL_PRODUCTION_ID]),
|
|
471
|
+
);
|
|
472
|
+
sloFindBySpy.mockResolvedValue([
|
|
473
|
+
fakeSlo({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
474
|
+
]);
|
|
475
|
+
|
|
476
|
+
const results: Array<SloMonitorSyncResult> =
|
|
477
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
478
|
+
monitorId: MONITOR_A_ID,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
482
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual([
|
|
483
|
+
MONITOR_A_ID.toString(),
|
|
484
|
+
]);
|
|
485
|
+
expect(writtenIds(updateOneByIdSpy, "autoAddedMonitors")).toEqual([
|
|
486
|
+
MONITOR_A_ID.toString(),
|
|
487
|
+
]);
|
|
488
|
+
expect(results).toHaveLength(1);
|
|
489
|
+
expect(results[0]!.monitorIdsAdded).toEqual([MONITOR_A_ID.toString()]);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("detaches the monitor once its last matching label is taken away", async () => {
|
|
493
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
494
|
+
fakeMonitorWithLabels([LABEL_STAGING_ID]),
|
|
495
|
+
);
|
|
496
|
+
sloFindBySpy.mockResolvedValue([
|
|
497
|
+
fakeSlo({
|
|
498
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
499
|
+
monitors: [MONITOR_A_ID],
|
|
500
|
+
autoAddedMonitors: [MONITOR_A_ID],
|
|
501
|
+
}),
|
|
502
|
+
]);
|
|
503
|
+
|
|
504
|
+
const results: Array<SloMonitorSyncResult> =
|
|
505
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
506
|
+
monitorId: MONITOR_A_ID,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual([]);
|
|
510
|
+
expect(writtenIds(updateOneByIdSpy, "autoAddedMonitors")).toEqual([]);
|
|
511
|
+
expect(results[0]!.monitorIdsRemoved).toEqual([MONITOR_A_ID.toString()]);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("detaches the monitor when every label is cleared at once", async () => {
|
|
515
|
+
monitorFindOneByIdSpy.mockResolvedValue(fakeMonitorWithLabels([]));
|
|
516
|
+
sloFindBySpy.mockResolvedValue([
|
|
517
|
+
fakeSlo({
|
|
518
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
519
|
+
monitors: [MONITOR_A_ID],
|
|
520
|
+
autoAddedMonitors: [MONITOR_A_ID],
|
|
521
|
+
}),
|
|
522
|
+
]);
|
|
523
|
+
|
|
524
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
525
|
+
monitorId: MONITOR_A_ID,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual([]);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("leaves the other monitors an SLO's rule attached exactly where they are", async () => {
|
|
532
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
533
|
+
fakeMonitorWithLabels([LABEL_STAGING_ID]),
|
|
534
|
+
);
|
|
535
|
+
sloFindBySpy.mockResolvedValue([
|
|
536
|
+
fakeSlo({
|
|
537
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
538
|
+
monitors: [MONITOR_A_ID, MONITOR_B_ID, MONITOR_C_ID],
|
|
539
|
+
autoAddedMonitors: [MONITOR_A_ID, MONITOR_B_ID],
|
|
540
|
+
}),
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
544
|
+
monitorId: MONITOR_A_ID,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
/*
|
|
548
|
+
* Only monitor A's membership was in question. B keeps its rule-attached
|
|
549
|
+
* status without the engine re-checking B's labels, and the manual C is
|
|
550
|
+
* untouched.
|
|
551
|
+
*/
|
|
552
|
+
expect(writtenIds(updateOneByIdSpy, "monitors")).toEqual(
|
|
553
|
+
[MONITOR_B_ID.toString(), MONITOR_C_ID.toString()].sort(),
|
|
554
|
+
);
|
|
555
|
+
expect(writtenIds(updateOneByIdSpy, "autoAddedMonitors")).toEqual([
|
|
556
|
+
MONITOR_B_ID.toString(),
|
|
557
|
+
]);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("never touches an SLO with no label rule - that list is curated by hand", async () => {
|
|
561
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
562
|
+
fakeMonitorWithLabels([LABEL_PRODUCTION_ID]),
|
|
563
|
+
);
|
|
564
|
+
sloFindBySpy.mockResolvedValue([
|
|
565
|
+
fakeSlo({ monitorLabels: [], monitors: [] }),
|
|
566
|
+
]);
|
|
567
|
+
|
|
568
|
+
const results: Array<SloMonitorSyncResult> =
|
|
569
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
570
|
+
monitorId: MONITOR_A_ID,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
574
|
+
expect(results).toEqual([]);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("does not detach a monitor a human attached to a rule-driven SLO", async () => {
|
|
578
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
579
|
+
fakeMonitorWithLabels([LABEL_STAGING_ID]),
|
|
580
|
+
);
|
|
581
|
+
sloFindBySpy.mockResolvedValue([
|
|
582
|
+
fakeSlo({
|
|
583
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
584
|
+
monitors: [MONITOR_A_ID],
|
|
585
|
+
autoAddedMonitors: [],
|
|
586
|
+
}),
|
|
587
|
+
]);
|
|
588
|
+
|
|
589
|
+
const results: Array<SloMonitorSyncResult> =
|
|
590
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
591
|
+
monitorId: MONITOR_A_ID,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
595
|
+
expect(results).toEqual([]);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("does not adopt a hand-attached monitor into the rule when it starts matching", async () => {
|
|
599
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
600
|
+
fakeMonitorWithLabels([LABEL_PRODUCTION_ID]),
|
|
601
|
+
);
|
|
602
|
+
sloFindBySpy.mockResolvedValue([
|
|
603
|
+
fakeSlo({
|
|
604
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
605
|
+
monitors: [MONITOR_A_ID],
|
|
606
|
+
autoAddedMonitors: [],
|
|
607
|
+
}),
|
|
608
|
+
]);
|
|
609
|
+
|
|
610
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
611
|
+
monitorId: MONITOR_A_ID,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// Already attached and still manual: nothing to write, nothing to adopt.
|
|
615
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("applies every rule in the project, not just the first one that matches", async () => {
|
|
619
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
620
|
+
fakeMonitorWithLabels([LABEL_PRODUCTION_ID]),
|
|
621
|
+
);
|
|
622
|
+
sloFindBySpy.mockResolvedValue([
|
|
623
|
+
fakeSlo({ id: SLO_ID, monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
624
|
+
fakeSlo({ id: OTHER_SLO_ID, monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
const results: Array<SloMonitorSyncResult> =
|
|
628
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
629
|
+
monitorId: MONITOR_A_ID,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(2);
|
|
633
|
+
expect(writtenSloId(updateOneByIdSpy, 0)).toBe(SLO_ID.toString());
|
|
634
|
+
expect(writtenSloId(updateOneByIdSpy, 1)).toBe(OTHER_SLO_ID.toString());
|
|
635
|
+
expect(results).toHaveLength(2);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("carries on with the remaining SLOs when one of them fails to write", async () => {
|
|
639
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
640
|
+
fakeMonitorWithLabels([LABEL_PRODUCTION_ID]),
|
|
641
|
+
);
|
|
642
|
+
sloFindBySpy.mockResolvedValue([
|
|
643
|
+
fakeSlo({ id: SLO_ID, monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
644
|
+
fakeSlo({ id: OTHER_SLO_ID, monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
645
|
+
]);
|
|
646
|
+
updateOneByIdSpy
|
|
647
|
+
.mockRejectedValueOnce(new Error("db down"))
|
|
648
|
+
.mockResolvedValueOnce(1);
|
|
649
|
+
|
|
650
|
+
const results: Array<SloMonitorSyncResult> =
|
|
651
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
652
|
+
monitorId: MONITOR_A_ID,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(2);
|
|
656
|
+
expect(results).toHaveLength(1);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("looks up the SLOs of the monitor's own project", async () => {
|
|
660
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
661
|
+
fakeMonitorWithLabels([LABEL_PRODUCTION_ID], {
|
|
662
|
+
projectId: OTHER_PROJECT_ID,
|
|
663
|
+
}),
|
|
664
|
+
);
|
|
665
|
+
sloFindBySpy.mockResolvedValue([]);
|
|
666
|
+
|
|
667
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
668
|
+
monitorId: MONITOR_A_ID,
|
|
669
|
+
projectId: PROJECT_ID,
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// The monitor row wins over the caller's hint - it cannot be stale.
|
|
673
|
+
expect(sloFindBySpy.mock.calls[0]![0]!.query.projectId).toEqual(
|
|
674
|
+
OTHER_PROJECT_ID,
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("falls back to the caller's project when the monitor row does not carry one", async () => {
|
|
679
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
680
|
+
fakeMonitorWithLabels([LABEL_PRODUCTION_ID], { projectId: null }),
|
|
681
|
+
);
|
|
682
|
+
sloFindBySpy.mockResolvedValue([]);
|
|
683
|
+
|
|
684
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
685
|
+
monitorId: MONITOR_A_ID,
|
|
686
|
+
projectId: PROJECT_ID,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
expect(sloFindBySpy.mock.calls[0]![0]!.query.projectId).toEqual(PROJECT_ID);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("does nothing for a monitor that no longer exists", async () => {
|
|
693
|
+
monitorFindOneByIdSpy.mockResolvedValue(null);
|
|
694
|
+
|
|
695
|
+
const results: Array<SloMonitorSyncResult> =
|
|
696
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
697
|
+
monitorId: MONITOR_A_ID,
|
|
698
|
+
projectId: PROJECT_ID,
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
expect(sloFindBySpy).not.toHaveBeenCalled();
|
|
702
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
703
|
+
expect(results).toEqual([]);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("does nothing when no project can be determined at all", async () => {
|
|
707
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
708
|
+
fakeMonitorWithLabels([LABEL_PRODUCTION_ID], { projectId: null }),
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
const results: Array<SloMonitorSyncResult> =
|
|
712
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor({
|
|
713
|
+
monitorId: MONITOR_A_ID,
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
expect(sloFindBySpy).not.toHaveBeenCalled();
|
|
717
|
+
expect(results).toEqual([]);
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
describe("ServiceLevelObjectiveMonitorRuleEngineService.doesLabelSetMatchRule", () => {
|
|
722
|
+
const cases: Array<{
|
|
723
|
+
label: string;
|
|
724
|
+
labelIds: Array<string>;
|
|
725
|
+
ruleLabelIds: Array<string>;
|
|
726
|
+
expected: boolean;
|
|
727
|
+
}> = [
|
|
728
|
+
{
|
|
729
|
+
label: "one shared label is enough",
|
|
730
|
+
labelIds: ["a", "b"],
|
|
731
|
+
ruleLabelIds: ["b", "c"],
|
|
732
|
+
expected: true,
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
label: "no overlap does not match",
|
|
736
|
+
labelIds: ["a"],
|
|
737
|
+
ruleLabelIds: ["b"],
|
|
738
|
+
expected: false,
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
label: "a monitor with no labels never matches",
|
|
742
|
+
labelIds: [],
|
|
743
|
+
ruleLabelIds: ["a"],
|
|
744
|
+
expected: false,
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
label: "an empty rule matches nothing, not everything",
|
|
748
|
+
labelIds: ["a"],
|
|
749
|
+
ruleLabelIds: [],
|
|
750
|
+
expected: false,
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
label: "both empty is still no match",
|
|
754
|
+
labelIds: [],
|
|
755
|
+
ruleLabelIds: [],
|
|
756
|
+
expected: false,
|
|
757
|
+
},
|
|
758
|
+
];
|
|
759
|
+
|
|
760
|
+
for (const testCase of cases) {
|
|
761
|
+
it(testCase.label, () => {
|
|
762
|
+
expect(
|
|
763
|
+
ServiceLevelObjectiveMonitorRuleEngineService.doesLabelSetMatchRule({
|
|
764
|
+
labelIds: new Set(testCase.labelIds),
|
|
765
|
+
ruleLabelIds: new Set(testCase.ruleLabelIds),
|
|
766
|
+
}),
|
|
767
|
+
).toBe(testCase.expected);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
});
|