@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
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import Label from "../../Models/DatabaseModels/Label";
|
|
2
|
+
import Monitor from "../../Models/DatabaseModels/Monitor";
|
|
3
|
+
import StatusPageMonitorRule from "../../Models/DatabaseModels/StatusPageMonitorRule";
|
|
4
|
+
import StatusPageResource from "../../Models/DatabaseModels/StatusPageResource";
|
|
5
|
+
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
6
|
+
import ObjectID from "../../Types/ObjectID";
|
|
7
|
+
import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
|
|
8
|
+
import RulePatternMatchUtil from "../../Utils/Rules/RulePatternMatchUtil";
|
|
9
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
10
|
+
import logger, { LogAttributes } from "../Utils/Logger";
|
|
11
|
+
import MonitorService from "./MonitorService";
|
|
12
|
+
import StatusPageMonitorRuleService from "./StatusPageMonitorRuleService";
|
|
13
|
+
import StatusPageResourceService from "./StatusPageResourceService";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a sync did, so callers (and tests) can assert on the outcome without
|
|
17
|
+
* re-reading the status page.
|
|
18
|
+
*/
|
|
19
|
+
export interface StatusPageMonitorRuleSyncResult {
|
|
20
|
+
monitorIdsAdded: Array<string>;
|
|
21
|
+
statusPageResourceIdsRemoved: Array<string>;
|
|
22
|
+
statusPageResourceIdsUpdated: Array<string>;
|
|
23
|
+
/*
|
|
24
|
+
* The monitors whose resources this pass removed. Same event as
|
|
25
|
+
* statusPageResourceIdsRemoved, keyed by monitor instead of by resource,
|
|
26
|
+
* because the caller's next question is "does another rule still want this
|
|
27
|
+
* monitor" and that is asked by monitor id.
|
|
28
|
+
*/
|
|
29
|
+
monitorIdsReleased: Array<string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function emptyResult(): StatusPageMonitorRuleSyncResult {
|
|
33
|
+
return {
|
|
34
|
+
monitorIdsAdded: [],
|
|
35
|
+
statusPageResourceIdsRemoved: [],
|
|
36
|
+
statusPageResourceIdsUpdated: [],
|
|
37
|
+
monitorIdsReleased: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toIdSet(
|
|
42
|
+
items: Array<{ id?: ObjectID | null }> | undefined,
|
|
43
|
+
): Set<string> {
|
|
44
|
+
const ids: Set<string> = new Set<string>();
|
|
45
|
+
|
|
46
|
+
for (const item of items || []) {
|
|
47
|
+
const id: string = item?.id?.toString() || "";
|
|
48
|
+
|
|
49
|
+
if (id) {
|
|
50
|
+
ids.add(id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return ids;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Everything the engine needs to know about one rule to decide membership.
|
|
59
|
+
* `findBy` hands back partial models, so this is the shape actually selected
|
|
60
|
+
* rather than the full entity.
|
|
61
|
+
*/
|
|
62
|
+
const RULE_SELECT: {
|
|
63
|
+
[key: string]: boolean | { [key: string]: boolean };
|
|
64
|
+
} = {
|
|
65
|
+
_id: true,
|
|
66
|
+
projectId: true,
|
|
67
|
+
statusPageId: true,
|
|
68
|
+
statusPageGroupId: true,
|
|
69
|
+
isEnabled: true,
|
|
70
|
+
monitorLabels: { _id: true },
|
|
71
|
+
monitorNamePattern: true,
|
|
72
|
+
monitorDescriptionPattern: true,
|
|
73
|
+
showCurrentStatus: true,
|
|
74
|
+
showUptimePercent: true,
|
|
75
|
+
uptimePercentPrecision: true,
|
|
76
|
+
showStatusHistoryChart: true,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Keeps a status page's resources in step with its monitor rules.
|
|
81
|
+
*
|
|
82
|
+
* A status page can say "show every monitor labelled Production in the Core
|
|
83
|
+
* Platform group" instead of adding monitors one at a time and typing a
|
|
84
|
+
* display name for each. That promise has two halves, and both have to hold:
|
|
85
|
+
*
|
|
86
|
+
* - a monitor that starts matching gets added to the group the rule names,
|
|
87
|
+
* and a monitor that stops matching gets removed again;
|
|
88
|
+
*
|
|
89
|
+
* - a resource a human added by hand is never touched — not adopted into a
|
|
90
|
+
* rule when it happens to match, and never removed when it stops matching
|
|
91
|
+
* or when the rule is deleted.
|
|
92
|
+
*
|
|
93
|
+
* StatusPageResource.statusPageMonitorRuleId is what makes the second half
|
|
94
|
+
* possible: it records which rule owns a resource, and only owned resources
|
|
95
|
+
* are ever removed.
|
|
96
|
+
*
|
|
97
|
+
* Membership can be invalidated from either side, so there are two entry
|
|
98
|
+
* points:
|
|
99
|
+
*
|
|
100
|
+
* - the rule side (the rule was created or edited) -> syncResourcesForRule
|
|
101
|
+
* - the monitor side (its labels or name changed) -> syncRulesForMonitor
|
|
102
|
+
*/
|
|
103
|
+
export class StatusPageMonitorRuleEngineServiceClass {
|
|
104
|
+
/**
|
|
105
|
+
* Re-evaluates one rule against every monitor in its project. Use when the
|
|
106
|
+
* rule itself changed — it is the authoritative, self-healing path and
|
|
107
|
+
* repairs any drift it finds.
|
|
108
|
+
*/
|
|
109
|
+
@CaptureSpan()
|
|
110
|
+
public async syncResourcesForRule(data: {
|
|
111
|
+
statusPageMonitorRuleId: ObjectID;
|
|
112
|
+
}): Promise<StatusPageMonitorRuleSyncResult> {
|
|
113
|
+
const rule: StatusPageMonitorRule | null =
|
|
114
|
+
await StatusPageMonitorRuleService.findOneById({
|
|
115
|
+
id: data.statusPageMonitorRuleId,
|
|
116
|
+
select: RULE_SELECT as any,
|
|
117
|
+
props: {
|
|
118
|
+
isRoot: true,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!rule || !rule.id || !rule.projectId || !rule.statusPageId) {
|
|
123
|
+
return emptyResult();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/*
|
|
127
|
+
* A disabled rule matches nothing, which detaches everything it had added.
|
|
128
|
+
* Toggling a rule off therefore undoes it rather than leaving its monitors
|
|
129
|
+
* stranded on a page nobody remembers configuring.
|
|
130
|
+
*/
|
|
131
|
+
const matchedMonitors: Array<Monitor> = rule.isEnabled
|
|
132
|
+
? await this.findMatchingMonitors({ rule: rule })
|
|
133
|
+
: [];
|
|
134
|
+
|
|
135
|
+
const result: StatusPageMonitorRuleSyncResult = await this.writeMembership({
|
|
136
|
+
rule: rule,
|
|
137
|
+
matchedMonitors: matchedMonitors,
|
|
138
|
+
/*
|
|
139
|
+
* This entry point runs because the rule itself changed, so it is the
|
|
140
|
+
* one that re-homes and re-styles the resources the rule already owns.
|
|
141
|
+
* The monitor-side path deliberately does not: nothing about the rule
|
|
142
|
+
* moved there, and reconciling on an unrelated monitor edit would
|
|
143
|
+
* silently revert per-resource tweaks at a surprising moment.
|
|
144
|
+
*/
|
|
145
|
+
reconcileOwnedResources: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* A monitor this rule just released may still be claimed by another rule
|
|
150
|
+
* on the same page — narrowing this rule does not mean the monitor should
|
|
151
|
+
* vanish from the page. Re-running the monitor-side sync for each released
|
|
152
|
+
* monitor lets whichever other rule still wants it pick it up.
|
|
153
|
+
*
|
|
154
|
+
* Bounded: syncRulesForMonitor only calls writeMembership, never back into
|
|
155
|
+
* this method, so there is no cycle.
|
|
156
|
+
*/
|
|
157
|
+
for (const monitorId of result.monitorIdsReleased) {
|
|
158
|
+
try {
|
|
159
|
+
await this.syncRulesForMonitor({
|
|
160
|
+
monitorId: new ObjectID(monitorId),
|
|
161
|
+
projectId: rule.projectId,
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
logger.error(
|
|
165
|
+
`Error re-homing monitor ${monitorId} after status page monitor rule ${rule.id.toString()} released it: ${error}`,
|
|
166
|
+
{ projectId: rule.projectId.toString() } as LogAttributes,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Re-evaluates every rule in the project against a single monitor. Use when
|
|
176
|
+
* the monitor changed: it costs one query per project regardless of how many
|
|
177
|
+
* monitors the rules match.
|
|
178
|
+
*/
|
|
179
|
+
@CaptureSpan()
|
|
180
|
+
public async syncRulesForMonitor(data: {
|
|
181
|
+
monitorId: ObjectID;
|
|
182
|
+
projectId?: ObjectID | undefined;
|
|
183
|
+
}): Promise<Array<StatusPageMonitorRuleSyncResult>> {
|
|
184
|
+
const monitor: Monitor | null = await MonitorService.findOneById({
|
|
185
|
+
id: data.monitorId,
|
|
186
|
+
select: {
|
|
187
|
+
_id: true,
|
|
188
|
+
projectId: true,
|
|
189
|
+
name: true,
|
|
190
|
+
description: true,
|
|
191
|
+
labels: {
|
|
192
|
+
_id: true,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
props: {
|
|
196
|
+
isRoot: true,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (!monitor || !monitor.id) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const projectId: ObjectID | undefined =
|
|
205
|
+
monitor.projectId || data.projectId || undefined;
|
|
206
|
+
|
|
207
|
+
if (!projectId) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const rules: Array<StatusPageMonitorRule> =
|
|
212
|
+
await StatusPageMonitorRuleService.findBy({
|
|
213
|
+
query: {
|
|
214
|
+
projectId: projectId,
|
|
215
|
+
isEnabled: true,
|
|
216
|
+
},
|
|
217
|
+
select: RULE_SELECT as any,
|
|
218
|
+
limit: LIMIT_PER_PROJECT,
|
|
219
|
+
skip: 0,
|
|
220
|
+
props: {
|
|
221
|
+
isRoot: true,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const results: Array<StatusPageMonitorRuleSyncResult> = [];
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* Two passes, removals before additions, and the order matters.
|
|
229
|
+
*
|
|
230
|
+
* Two rules on the same page can match the same monitor; only the first
|
|
231
|
+
* one to see it creates the resource, and the page shows it once. If the
|
|
232
|
+
* monitor then stops matching the OWNING rule but still matches the other,
|
|
233
|
+
* a single interleaved pass gives an order-dependent answer: evaluate the
|
|
234
|
+
* still-matching rule first and it skips the add (the monitor is still on
|
|
235
|
+
* the page), then the owning rule deletes — and the monitor silently
|
|
236
|
+
* disappears from a public page even though a rule still claims it.
|
|
237
|
+
*
|
|
238
|
+
* Draining the removals first means the addition pass sees the page as it
|
|
239
|
+
* will actually be, so whichever rule still claims the monitor picks it up.
|
|
240
|
+
*/
|
|
241
|
+
for (const skipAdds of [true, false]) {
|
|
242
|
+
for (const rule of rules) {
|
|
243
|
+
if (!rule.id || !rule.statusPageId) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const doesMatch: boolean = this.doesMonitorMatchRule({
|
|
248
|
+
monitor: monitor,
|
|
249
|
+
rule: rule,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
254
|
+
await this.writeMembership({
|
|
255
|
+
rule: rule,
|
|
256
|
+
matchedMonitors: doesMatch ? [monitor] : [],
|
|
257
|
+
/*
|
|
258
|
+
* Only this monitor's membership is in question. Every other
|
|
259
|
+
* resource the rule owns is left exactly where it is.
|
|
260
|
+
*/
|
|
261
|
+
restrictToMonitorId: monitor.id,
|
|
262
|
+
skipAdds: skipAdds,
|
|
263
|
+
skipRemoves: !skipAdds,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
result.monitorIdsAdded.length > 0 ||
|
|
268
|
+
result.statusPageResourceIdsRemoved.length > 0
|
|
269
|
+
) {
|
|
270
|
+
results.push(result);
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
/*
|
|
274
|
+
* One rule failing must not stop the others: the monitor has already
|
|
275
|
+
* changed, and half a sync is better than none.
|
|
276
|
+
*/
|
|
277
|
+
logger.error(
|
|
278
|
+
`Error syncing status page monitor rule ${rule.id.toString()} for monitor ${monitor.id.toString()}: ${error}`,
|
|
279
|
+
{
|
|
280
|
+
projectId: projectId.toString(),
|
|
281
|
+
monitorId: monitor.id.toString(),
|
|
282
|
+
} as LogAttributes,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return results;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Removes every resource a rule added, without touching anything else on the
|
|
293
|
+
* page. Called before the rule row goes away so deleting a rule undoes it.
|
|
294
|
+
*/
|
|
295
|
+
@CaptureSpan()
|
|
296
|
+
public async removeResourcesAddedByRule(data: {
|
|
297
|
+
statusPageMonitorRuleId: ObjectID;
|
|
298
|
+
}): Promise<Array<string>> {
|
|
299
|
+
const ownedResources: Array<StatusPageResource> =
|
|
300
|
+
await StatusPageResourceService.findBy({
|
|
301
|
+
query: {
|
|
302
|
+
statusPageMonitorRuleId: data.statusPageMonitorRuleId,
|
|
303
|
+
},
|
|
304
|
+
select: {
|
|
305
|
+
_id: true,
|
|
306
|
+
},
|
|
307
|
+
limit: LIMIT_PER_PROJECT,
|
|
308
|
+
skip: 0,
|
|
309
|
+
props: {
|
|
310
|
+
isRoot: true,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const removedIds: Array<string> = [];
|
|
315
|
+
|
|
316
|
+
for (const resource of ownedResources) {
|
|
317
|
+
if (!resource.id) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await StatusPageResourceService.deleteOneById({
|
|
322
|
+
id: resource.id,
|
|
323
|
+
props: {
|
|
324
|
+
isRoot: true,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
removedIds.push(resource.id.toString());
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return removedIds;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Does this monitor satisfy every criterion the rule specifies?
|
|
336
|
+
*
|
|
337
|
+
* Criteria are ANDed and empty ones are skipped, which mirrors how every
|
|
338
|
+
* other rule in the product reads. A rule with no criteria at all matches
|
|
339
|
+
* nothing — silently putting the entire project on a public status page is
|
|
340
|
+
* not a reasonable reading of an empty form. Users who genuinely want that
|
|
341
|
+
* write `.*` as the name pattern.
|
|
342
|
+
*
|
|
343
|
+
* Pattern matching goes through RulePatternMatchUtil, the same matcher the
|
|
344
|
+
* network device rules use, so a pattern means the same thing everywhere in
|
|
345
|
+
* the product: a case-insensitive regex, or a `*` wildcard glob. Rolling a
|
|
346
|
+
* private regex test here would have reintroduced #2940 — a user typing
|
|
347
|
+
* `*api*` (a glob, not a regex) gets a rule that silently matches nothing.
|
|
348
|
+
*/
|
|
349
|
+
public doesMonitorMatchRule(data: {
|
|
350
|
+
monitor: Monitor;
|
|
351
|
+
rule: StatusPageMonitorRule;
|
|
352
|
+
}): boolean {
|
|
353
|
+
const { monitor, rule } = data;
|
|
354
|
+
|
|
355
|
+
const hasLabelCriteria: boolean = Boolean(
|
|
356
|
+
rule.monitorLabels && rule.monitorLabels.length > 0,
|
|
357
|
+
);
|
|
358
|
+
const hasNameCriteria: boolean = Boolean(rule.monitorNamePattern);
|
|
359
|
+
const hasDescriptionCriteria: boolean = Boolean(
|
|
360
|
+
rule.monitorDescriptionPattern,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
if (!hasLabelCriteria && !hasNameCriteria && !hasDescriptionCriteria) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (hasLabelCriteria) {
|
|
368
|
+
const ruleLabelIds: Set<string> = toIdSet(rule.monitorLabels);
|
|
369
|
+
const monitorLabelIds: Set<string> = toIdSet(monitor.labels);
|
|
370
|
+
|
|
371
|
+
let hasAnyLabel: boolean = false;
|
|
372
|
+
|
|
373
|
+
for (const labelId of monitorLabelIds) {
|
|
374
|
+
if (ruleLabelIds.has(labelId)) {
|
|
375
|
+
hasAnyLabel = true;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!hasAnyLabel) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (hasNameCriteria) {
|
|
386
|
+
if (
|
|
387
|
+
!RulePatternMatchUtil.matches(monitor.name, rule.monitorNamePattern)
|
|
388
|
+
) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (hasDescriptionCriteria) {
|
|
394
|
+
if (
|
|
395
|
+
!RulePatternMatchUtil.matches(
|
|
396
|
+
monitor.description,
|
|
397
|
+
rule.monitorDescriptionPattern,
|
|
398
|
+
)
|
|
399
|
+
) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Every monitor in the rule's project that the rule matches.
|
|
409
|
+
*
|
|
410
|
+
* The label filter is pushed into the query when there is one, because it is
|
|
411
|
+
* the criterion that usually narrows the project the most. The regexes are
|
|
412
|
+
* then applied in memory — Postgres regex semantics and JavaScript's are not
|
|
413
|
+
* the same dialect, and the pattern a user typed has to mean one thing.
|
|
414
|
+
*/
|
|
415
|
+
@CaptureSpan()
|
|
416
|
+
private async findMatchingMonitors(data: {
|
|
417
|
+
rule: StatusPageMonitorRule;
|
|
418
|
+
}): Promise<Array<Monitor>> {
|
|
419
|
+
const { rule } = data;
|
|
420
|
+
|
|
421
|
+
if (!rule.projectId) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const hasLabelCriteria: boolean = Boolean(
|
|
426
|
+
rule.monitorLabels && rule.monitorLabels.length > 0,
|
|
427
|
+
);
|
|
428
|
+
const hasNameCriteria: boolean = Boolean(rule.monitorNamePattern);
|
|
429
|
+
const hasDescriptionCriteria: boolean = Boolean(
|
|
430
|
+
rule.monitorDescriptionPattern,
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (!hasLabelCriteria && !hasNameCriteria && !hasDescriptionCriteria) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const query: {
|
|
438
|
+
projectId: ObjectID;
|
|
439
|
+
labels?: Array<ObjectID>;
|
|
440
|
+
} = {
|
|
441
|
+
projectId: rule.projectId,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
if (hasLabelCriteria) {
|
|
445
|
+
query.labels = (rule.monitorLabels || [])
|
|
446
|
+
.filter((label: Label) => {
|
|
447
|
+
return Boolean(label.id);
|
|
448
|
+
})
|
|
449
|
+
.map((label: Label) => {
|
|
450
|
+
return label.id!;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const monitors: Array<Monitor> = await MonitorService.findBy({
|
|
455
|
+
query: query as any,
|
|
456
|
+
select: {
|
|
457
|
+
_id: true,
|
|
458
|
+
name: true,
|
|
459
|
+
description: true,
|
|
460
|
+
labels: {
|
|
461
|
+
_id: true,
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
limit: LIMIT_PER_PROJECT,
|
|
465
|
+
skip: 0,
|
|
466
|
+
props: {
|
|
467
|
+
isRoot: true,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return monitors.filter((monitor: Monitor) => {
|
|
472
|
+
return this.doesMonitorMatchRule({ monitor: monitor, rule: rule });
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Applies a computed match set to one rule and persists the difference.
|
|
478
|
+
*
|
|
479
|
+
* `matchedMonitors` is what the rule claims. A claimed monitor that has no
|
|
480
|
+
* resource on the page yet gets one; a resource the rule owns whose monitor
|
|
481
|
+
* it no longer claims is deleted. Resources that are not rule-owned are
|
|
482
|
+
* invisible to both halves, which is what keeps manual resources safe — and
|
|
483
|
+
* a monitor a human already added is not added a second time, so the page
|
|
484
|
+
* never shows a duplicate.
|
|
485
|
+
*
|
|
486
|
+
* `restrictToMonitorId` narrows both halves to a single monitor, for the
|
|
487
|
+
* monitor-side entry point where every other monitor's membership is
|
|
488
|
+
* unchanged and re-deriving it would be wasted work.
|
|
489
|
+
*
|
|
490
|
+
* `reconcileOwnedResources` additionally drags resources the rule already
|
|
491
|
+
* owns back into line with the rule — see reconcileResource. Only the
|
|
492
|
+
* rule-side entry point asks for it.
|
|
493
|
+
*/
|
|
494
|
+
private async writeMembership(data: {
|
|
495
|
+
rule: StatusPageMonitorRule;
|
|
496
|
+
matchedMonitors: Array<Monitor>;
|
|
497
|
+
restrictToMonitorId?: ObjectID | undefined;
|
|
498
|
+
reconcileOwnedResources?: boolean | undefined;
|
|
499
|
+
skipAdds?: boolean | undefined;
|
|
500
|
+
skipRemoves?: boolean | undefined;
|
|
501
|
+
}): Promise<StatusPageMonitorRuleSyncResult> {
|
|
502
|
+
const { rule, matchedMonitors, restrictToMonitorId } = data;
|
|
503
|
+
|
|
504
|
+
if (!rule.id || !rule.statusPageId || !rule.projectId) {
|
|
505
|
+
return emptyResult();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const existingResources: Array<StatusPageResource> =
|
|
509
|
+
await StatusPageResourceService.findBy({
|
|
510
|
+
query: {
|
|
511
|
+
statusPageId: rule.statusPageId,
|
|
512
|
+
},
|
|
513
|
+
select: {
|
|
514
|
+
_id: true,
|
|
515
|
+
monitorId: true,
|
|
516
|
+
statusPageMonitorRuleId: true,
|
|
517
|
+
statusPageGroupId: true,
|
|
518
|
+
showCurrentStatus: true,
|
|
519
|
+
showUptimePercent: true,
|
|
520
|
+
uptimePercentPrecision: true,
|
|
521
|
+
showStatusHistoryChart: true,
|
|
522
|
+
},
|
|
523
|
+
limit: LIMIT_PER_PROJECT,
|
|
524
|
+
skip: 0,
|
|
525
|
+
props: {
|
|
526
|
+
isRoot: true,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const monitorIdsOnPage: Set<string> = new Set<string>();
|
|
531
|
+
const ownedResourcesByMonitorId: Map<string, StatusPageResource> = new Map<
|
|
532
|
+
string,
|
|
533
|
+
StatusPageResource
|
|
534
|
+
>();
|
|
535
|
+
|
|
536
|
+
for (const resource of existingResources) {
|
|
537
|
+
const monitorId: string = resource.monitorId?.toString() || "";
|
|
538
|
+
|
|
539
|
+
if (!monitorId) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
monitorIdsOnPage.add(monitorId);
|
|
544
|
+
|
|
545
|
+
if (resource.statusPageMonitorRuleId?.toString() === rule.id.toString()) {
|
|
546
|
+
ownedResourcesByMonitorId.set(monitorId, resource);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const matchedMonitorIds: Set<string> = toIdSet(matchedMonitors);
|
|
551
|
+
|
|
552
|
+
const monitorIdsAdded: Array<string> = [];
|
|
553
|
+
const statusPageResourceIdsRemoved: Array<string> = [];
|
|
554
|
+
const statusPageResourceIdsUpdated: Array<string> = [];
|
|
555
|
+
const monitorIdsReleased: Array<string> = [];
|
|
556
|
+
|
|
557
|
+
// Remove: resources this rule owns whose monitor it no longer claims.
|
|
558
|
+
if (!data.skipRemoves) {
|
|
559
|
+
for (const [monitorId, resource] of ownedResourcesByMonitorId) {
|
|
560
|
+
if (matchedMonitorIds.has(monitorId)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (
|
|
565
|
+
restrictToMonitorId &&
|
|
566
|
+
restrictToMonitorId.toString() !== monitorId
|
|
567
|
+
) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (!resource.id) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
await StatusPageResourceService.deleteOneById({
|
|
576
|
+
id: resource.id,
|
|
577
|
+
props: {
|
|
578
|
+
isRoot: true,
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
statusPageResourceIdsRemoved.push(resource.id.toString());
|
|
583
|
+
monitorIdsReleased.push(monitorId);
|
|
584
|
+
monitorIdsOnPage.delete(monitorId);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/*
|
|
589
|
+
* Add: matched monitors that are not on the page at all yet.
|
|
590
|
+
*
|
|
591
|
+
* "Not on the page" and not merely "not owned by this rule" — a monitor a
|
|
592
|
+
* human already added, or that another rule added, must not get a second
|
|
593
|
+
* resource, because the public page would then list it twice.
|
|
594
|
+
*/
|
|
595
|
+
if (!data.skipAdds) {
|
|
596
|
+
for (const monitor of matchedMonitors) {
|
|
597
|
+
const monitorId: string = monitor.id?.toString() || "";
|
|
598
|
+
|
|
599
|
+
if (!monitorId || monitorIdsOnPage.has(monitorId)) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
await this.createResource({ rule: rule, monitor: monitor });
|
|
604
|
+
monitorIdsAdded.push(monitorId);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/*
|
|
609
|
+
* Reconcile: resources the rule still owns and still claims are dragged
|
|
610
|
+
* back into line with the rule. Without this, editing "Add Monitors To
|
|
611
|
+
* Group" (or any display option) silently does nothing to the monitors the
|
|
612
|
+
* rule already added - it would only apply to ones it adds in future,
|
|
613
|
+
* which is not what changing a rule looks like it should do.
|
|
614
|
+
*/
|
|
615
|
+
if (data.reconcileOwnedResources) {
|
|
616
|
+
for (const [monitorId, resource] of ownedResourcesByMonitorId) {
|
|
617
|
+
if (!matchedMonitorIds.has(monitorId) || !resource.id) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (await this.reconcileResource({ rule: rule, resource: resource })) {
|
|
622
|
+
statusPageResourceIdsUpdated.push(resource.id.toString());
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (
|
|
628
|
+
monitorIdsAdded.length > 0 ||
|
|
629
|
+
statusPageResourceIdsRemoved.length > 0 ||
|
|
630
|
+
statusPageResourceIdsUpdated.length > 0
|
|
631
|
+
) {
|
|
632
|
+
logger.debug(
|
|
633
|
+
`Status page monitor rule ${rule.id.toString()} synced status page ${rule.statusPageId.toString()}: +${monitorIdsAdded.length} / -${statusPageResourceIdsRemoved.length} / ~${statusPageResourceIdsUpdated.length} resources`,
|
|
634
|
+
{ projectId: rule.projectId.toString() } as LogAttributes,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
monitorIdsAdded: monitorIdsAdded,
|
|
640
|
+
statusPageResourceIdsRemoved: statusPageResourceIdsRemoved,
|
|
641
|
+
statusPageResourceIdsUpdated: statusPageResourceIdsUpdated,
|
|
642
|
+
monitorIdsReleased: monitorIdsReleased,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Brings one rule-owned resource back into line with its rule, and reports
|
|
648
|
+
* whether anything actually changed.
|
|
649
|
+
*
|
|
650
|
+
* Only the fields the rule owns are considered — the group it places
|
|
651
|
+
* monitors in and the display options it sets. displayName is deliberately
|
|
652
|
+
* not among them: it is seeded from the monitor at creation time and then
|
|
653
|
+
* belongs to the status page, so renaming a monitor does not rewrite a
|
|
654
|
+
* display name someone may have polished for customers.
|
|
655
|
+
*
|
|
656
|
+
* Writes nothing when the resource already agrees, so re-running a rule that
|
|
657
|
+
* did not change costs a read and no writes.
|
|
658
|
+
*/
|
|
659
|
+
private async reconcileResource(data: {
|
|
660
|
+
rule: StatusPageMonitorRule;
|
|
661
|
+
resource: StatusPageResource;
|
|
662
|
+
}): Promise<boolean> {
|
|
663
|
+
const { rule, resource } = data;
|
|
664
|
+
|
|
665
|
+
if (!resource.id) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const desiredGroupId: string = rule.statusPageGroupId?.toString() || "";
|
|
670
|
+
const currentGroupId: string = resource.statusPageGroupId?.toString() || "";
|
|
671
|
+
|
|
672
|
+
const desired: {
|
|
673
|
+
statusPageGroupId: ObjectID | null;
|
|
674
|
+
showCurrentStatus: boolean;
|
|
675
|
+
showUptimePercent: boolean;
|
|
676
|
+
showStatusHistoryChart: boolean;
|
|
677
|
+
uptimePercentPrecision: UptimePrecision | null;
|
|
678
|
+
} = {
|
|
679
|
+
statusPageGroupId: rule.statusPageGroupId || null,
|
|
680
|
+
showCurrentStatus: rule.showCurrentStatus ?? true,
|
|
681
|
+
showUptimePercent: rule.showUptimePercent ?? true,
|
|
682
|
+
showStatusHistoryChart: rule.showStatusHistoryChart ?? true,
|
|
683
|
+
uptimePercentPrecision: rule.uptimePercentPrecision || null,
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const isSame: boolean =
|
|
687
|
+
desiredGroupId === currentGroupId &&
|
|
688
|
+
desired.showCurrentStatus === (resource.showCurrentStatus ?? true) &&
|
|
689
|
+
desired.showUptimePercent === (resource.showUptimePercent ?? true) &&
|
|
690
|
+
desired.showStatusHistoryChart ===
|
|
691
|
+
(resource.showStatusHistoryChart ?? true) &&
|
|
692
|
+
desired.uptimePercentPrecision ===
|
|
693
|
+
(resource.uptimePercentPrecision || null);
|
|
694
|
+
|
|
695
|
+
if (isSame) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
await StatusPageResourceService.updateOneById({
|
|
700
|
+
id: resource.id,
|
|
701
|
+
data: desired as never,
|
|
702
|
+
props: {
|
|
703
|
+
isRoot: true,
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private async createResource(data: {
|
|
711
|
+
rule: StatusPageMonitorRule;
|
|
712
|
+
monitor: Monitor;
|
|
713
|
+
}): Promise<void> {
|
|
714
|
+
const { rule, monitor } = data;
|
|
715
|
+
|
|
716
|
+
const resource: StatusPageResource = new StatusPageResource();
|
|
717
|
+
|
|
718
|
+
resource.projectId = rule.projectId!;
|
|
719
|
+
resource.statusPageId = rule.statusPageId!;
|
|
720
|
+
resource.monitorId = monitor.id!;
|
|
721
|
+
resource.statusPageMonitorRuleId = rule.id!;
|
|
722
|
+
|
|
723
|
+
if (rule.statusPageGroupId) {
|
|
724
|
+
resource.statusPageGroupId = rule.statusPageGroupId;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/*
|
|
728
|
+
* displayName is required on the resource and the whole point of the rule
|
|
729
|
+
* is not having to type one, so it mirrors the monitor. Renaming the
|
|
730
|
+
* monitor later does not rewrite it — the display name belongs to the
|
|
731
|
+
* status page from the moment the resource exists.
|
|
732
|
+
*/
|
|
733
|
+
resource.displayName = monitor.name || "";
|
|
734
|
+
|
|
735
|
+
resource.showCurrentStatus = rule.showCurrentStatus ?? true;
|
|
736
|
+
resource.showUptimePercent = rule.showUptimePercent ?? true;
|
|
737
|
+
resource.showStatusHistoryChart = rule.showStatusHistoryChart ?? true;
|
|
738
|
+
|
|
739
|
+
if (rule.uptimePercentPrecision) {
|
|
740
|
+
resource.uptimePercentPrecision = rule.uptimePercentPrecision;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
await StatusPageResourceService.create({
|
|
744
|
+
data: resource,
|
|
745
|
+
props: {
|
|
746
|
+
isRoot: true,
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export default new StatusPageMonitorRuleEngineServiceClass();
|