@oneuptime/common 14.0.6 → 14.0.7
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/InventoryItem.ts +23 -12
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertService.ts +32 -36
- package/Server/Services/IncidentAlertService.ts +609 -2
- package/Server/Services/IncidentService.ts +134 -44
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/ScheduledMaintenanceService.ts +50 -46
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Response.ts +31 -0
- package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
- package/Tests/Server/Services/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
- package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
- package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
- package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- package/Utils/Incident/IncidentFromAlerts.ts +78 -0
- package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.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/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +96 -42
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +30 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@ import ProjectService from "./ProjectService";
|
|
|
20
20
|
import { IsBillingEnabled } from "../EnvironmentConfig";
|
|
21
21
|
import { applyAlertRelatedRecordPrivacyFilter } from "../Utils/Alert/AlertPrivacyFilter";
|
|
22
22
|
import { applyIncidentRelatedRecordPrivacyFilter } from "../Utils/Incident/IncidentPrivacyFilter";
|
|
23
|
+
import AlertStateChangeAuthorization from "../Utils/Alert/AlertStateChangeAuthorization";
|
|
23
24
|
import PostgresErrorTranslator from "../Utils/Database/PostgresErrorTranslator";
|
|
24
25
|
import ProjectScopedReferenceValidator from "../Utils/Database/ProjectScopedReferenceValidator";
|
|
25
26
|
import RelationIdUtil from "../Utils/Database/RelationIdUtil";
|
|
@@ -44,6 +45,7 @@ import ForbiddenException from "../../Types/Exception/ForbiddenException";
|
|
|
44
45
|
import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
|
|
45
46
|
import NotFoundException from "../../Types/Exception/NotFoundException";
|
|
46
47
|
import {
|
|
48
|
+
INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY,
|
|
47
49
|
INCIDENT_ALERT_ALREADY_LINKED_MESSAGE,
|
|
48
50
|
INCIDENT_ALERT_IDS_TO_LINK_KEY,
|
|
49
51
|
MAX_ALERTS_PER_INCIDENT_LINK_ACTION,
|
|
@@ -145,6 +147,54 @@ export interface LinkAlertsToIncidentResult {
|
|
|
145
147
|
failed: Array<{ alertId: ObjectID; message: string }>;
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
export interface AcknowledgeDeclaredAlertsResult {
|
|
151
|
+
acknowledgedAlertIds: Array<ObjectID>;
|
|
152
|
+
// Already acknowledged, resolved or in a later state: left as they were.
|
|
153
|
+
alreadyAcknowledgedAlertIds: Array<ObjectID>;
|
|
154
|
+
/*
|
|
155
|
+
* Linked alerts the project's linked-alert sync moves on its own (the
|
|
156
|
+
* incident was declared straight into a state its switches act on).
|
|
157
|
+
*/
|
|
158
|
+
leftToLinkedAlertSyncAlertIds: Array<ObjectID>;
|
|
159
|
+
failed: Array<{ alertId: ObjectID; message: string }>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/*
|
|
163
|
+
* What validateAcknowledgeAlertsForNewIncident settled for a declaration
|
|
164
|
+
* that asked to acknowledge its alerts: the project's Acknowledged state,
|
|
165
|
+
* and the alerts not acknowledged yet - the ones the caller was checked for,
|
|
166
|
+
* and the only ones acknowledgeAlertsDeclaredWithIncident may write.
|
|
167
|
+
*/
|
|
168
|
+
export interface AlertsToAcknowledgeOnDeclare {
|
|
169
|
+
acknowledgedAlertStateId: ObjectID;
|
|
170
|
+
alertIdsToAcknowledge: Array<ObjectID>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* Alerts acknowledged at once when an incident is declared from many. Each
|
|
175
|
+
* acknowledgement is a full state change (and waits on its Slack / Microsoft
|
|
176
|
+
* Teams post), so one at a time could leave the last of 50 alerts escalating
|
|
177
|
+
* for minutes. Distinct alerts never share a timeline lock.
|
|
178
|
+
*/
|
|
179
|
+
const DECLARED_ALERT_ACKNOWLEDGE_CONCURRENCY: number = 5;
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* The "why" on an alert acknowledged because an incident was declared from
|
|
183
|
+
* it. It goes to the alert's feed, the alert's Slack / Microsoft Teams
|
|
184
|
+
* channels and its owners' notifications - places that must not name a
|
|
185
|
+
* private incident - so a private incident is not named at all.
|
|
186
|
+
*/
|
|
187
|
+
export function getDeclaredAlertAcknowledgementCause(data: {
|
|
188
|
+
incidentNumber: string;
|
|
189
|
+
isIncidentPrivate: boolean;
|
|
190
|
+
}): string {
|
|
191
|
+
if (data.isIncidentPrivate) {
|
|
192
|
+
return "Acknowledged because a private incident was declared from this alert.";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return `Acknowledged because ${withNumber("Incident", data.incidentNumber)} was declared from this alert.`;
|
|
196
|
+
}
|
|
197
|
+
|
|
148
198
|
/*
|
|
149
199
|
* miscDataProps key on an IncidentAlert create: the link is one of the links
|
|
150
200
|
* written while an incident is being declared from alerts. The incident then
|
|
@@ -1257,6 +1307,560 @@ export class Service extends DatabaseService<Model> {
|
|
|
1257
1307
|
return result;
|
|
1258
1308
|
}
|
|
1259
1309
|
|
|
1310
|
+
/*
|
|
1311
|
+
* Reads miscDataProps[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY] on an
|
|
1312
|
+
* incident create: whether to acknowledge the alerts the incident is
|
|
1313
|
+
* declared from once they are linked, and which. Null when not asked to
|
|
1314
|
+
* (absent, null or false). Otherwise the project's Acknowledged alert
|
|
1315
|
+
* state and the alerts that are not acknowledged yet - the only ones that
|
|
1316
|
+
* will be written, and so the only ones the caller must be allowed to
|
|
1317
|
+
* change: an alert that is already acknowledged or resolved is left alone,
|
|
1318
|
+
* so being unable to change it must not refuse the declaration.
|
|
1319
|
+
*
|
|
1320
|
+
* Called from IncidentService's onBeforeCreate after the alert ids were
|
|
1321
|
+
* validated - before the incident number is taken - so an impossible
|
|
1322
|
+
* request is refused instead of the incident being declared while its
|
|
1323
|
+
* alerts keep paging: the project has no Acknowledged alert state, or the
|
|
1324
|
+
* caller may not change the state of every alert that would be
|
|
1325
|
+
* acknowledged (see AlertStateChangeAuthorization; the acknowledgements
|
|
1326
|
+
* themselves are written as root, for exactly these alerts).
|
|
1327
|
+
*/
|
|
1328
|
+
@CaptureSpan()
|
|
1329
|
+
public async validateAcknowledgeAlertsForNewIncident(data: {
|
|
1330
|
+
projectId: ObjectID | undefined;
|
|
1331
|
+
acknowledgeAlerts: unknown;
|
|
1332
|
+
alertIds: Array<ObjectID>;
|
|
1333
|
+
props: DatabaseCommonInteractionProps;
|
|
1334
|
+
}): Promise<AlertsToAcknowledgeOnDeclare | null> {
|
|
1335
|
+
if (
|
|
1336
|
+
data.acknowledgeAlerts === undefined ||
|
|
1337
|
+
data.acknowledgeAlerts === null ||
|
|
1338
|
+
data.acknowledgeAlerts === false
|
|
1339
|
+
) {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (data.acknowledgeAlerts !== true) {
|
|
1344
|
+
throw new BadDataException(
|
|
1345
|
+
`${INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY} must be true or false.`,
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (data.alertIds.length === 0) {
|
|
1350
|
+
throw new BadDataException(
|
|
1351
|
+
`${INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY} only applies when the incident is declared from alerts: send the alert ids in ${INCIDENT_ALERT_IDS_TO_LINK_KEY}.`,
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (!data.projectId) {
|
|
1356
|
+
throw new BadDataException(
|
|
1357
|
+
"projectId is required to acknowledge alerts.",
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const projectId: ObjectID = data.projectId;
|
|
1362
|
+
|
|
1363
|
+
const acknowledgedState: AlertState | null =
|
|
1364
|
+
await AlertStateService.findOneBy({
|
|
1365
|
+
query: {
|
|
1366
|
+
projectId: projectId,
|
|
1367
|
+
isAcknowledgedState: true,
|
|
1368
|
+
},
|
|
1369
|
+
select: {
|
|
1370
|
+
_id: true,
|
|
1371
|
+
order: true,
|
|
1372
|
+
},
|
|
1373
|
+
props: {
|
|
1374
|
+
isRoot: true,
|
|
1375
|
+
},
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
if (
|
|
1379
|
+
!acknowledgedState ||
|
|
1380
|
+
!acknowledgedState._id ||
|
|
1381
|
+
acknowledgedState.order === undefined ||
|
|
1382
|
+
acknowledgedState.order === null
|
|
1383
|
+
) {
|
|
1384
|
+
throw new BadDataException(
|
|
1385
|
+
"This project has no Acknowledged alert state, so the alerts cannot be acknowledged. Declare the incident without acknowledging them, or add an Acknowledged state in the alert settings.",
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const acknowledgedOrder: number = acknowledgedState.order;
|
|
1390
|
+
|
|
1391
|
+
const alerts: Array<Alert> = await AlertService.findBy({
|
|
1392
|
+
query: {
|
|
1393
|
+
_id: QueryHelper.any(data.alertIds),
|
|
1394
|
+
projectId: projectId,
|
|
1395
|
+
},
|
|
1396
|
+
select: {
|
|
1397
|
+
_id: true,
|
|
1398
|
+
currentAlertState: {
|
|
1399
|
+
order: true,
|
|
1400
|
+
},
|
|
1401
|
+
},
|
|
1402
|
+
limit: LIMIT_PER_PROJECT,
|
|
1403
|
+
skip: 0,
|
|
1404
|
+
props: {
|
|
1405
|
+
isRoot: true,
|
|
1406
|
+
},
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
const stateOrderByAlertId: Map<string, number | undefined> = new Map();
|
|
1410
|
+
|
|
1411
|
+
for (const alert of alerts) {
|
|
1412
|
+
if (alert._id) {
|
|
1413
|
+
stateOrderByAlertId.set(
|
|
1414
|
+
normalizeId(alert._id),
|
|
1415
|
+
alert.currentAlertState?.order ?? undefined,
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Same rule as acknowledgeAlertsDeclaredWithIncident: by order.
|
|
1421
|
+
const alertIdsToAcknowledge: Array<ObjectID> = data.alertIds.filter(
|
|
1422
|
+
(alertId: ObjectID): boolean => {
|
|
1423
|
+
const key: string = normalizeId(alertId);
|
|
1424
|
+
|
|
1425
|
+
if (!stateOrderByAlertId.has(key)) {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
const order: number | undefined = stateOrderByAlertId.get(key);
|
|
1430
|
+
|
|
1431
|
+
return order === undefined || order < acknowledgedOrder;
|
|
1432
|
+
},
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
if (
|
|
1436
|
+
alertIdsToAcknowledge.length > 0 &&
|
|
1437
|
+
!data.props.isRoot &&
|
|
1438
|
+
!data.props.isMasterAdmin
|
|
1439
|
+
) {
|
|
1440
|
+
try {
|
|
1441
|
+
await AlertStateChangeAuthorization.assertCanChangeStateOfAlerts({
|
|
1442
|
+
projectId: projectId,
|
|
1443
|
+
alertIds: alertIdsToAcknowledge,
|
|
1444
|
+
props: data.props,
|
|
1445
|
+
});
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
// A lapsed session or an unpaid project keeps its own answer.
|
|
1448
|
+
if (
|
|
1449
|
+
error instanceof NotAuthorizedException ||
|
|
1450
|
+
error instanceof BadDataException
|
|
1451
|
+
) {
|
|
1452
|
+
throw new BadDataException(
|
|
1453
|
+
"You do not have permission to acknowledge one or more of these alerts. Declare the incident without acknowledging them, or ask a project admin for permission.",
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
throw error;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
return {
|
|
1462
|
+
acknowledgedAlertStateId: new ObjectID(acknowledgedState._id.toString()),
|
|
1463
|
+
alertIdsToAcknowledge: alertIdsToAcknowledge,
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/*
|
|
1468
|
+
* Acknowledges the alerts an incident was just declared from, when the
|
|
1469
|
+
* declaration asked for it (INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY). An
|
|
1470
|
+
* alert's own on-call escalation and its per-user notifications stop once
|
|
1471
|
+
* the alert is acknowledged (the workers check at their next run), so this
|
|
1472
|
+
* is what makes declaring an incident stop the alerts paging.
|
|
1473
|
+
*
|
|
1474
|
+
* Written as root - the caller's right to change the state of every one of
|
|
1475
|
+
* these alerts was checked before the incident was created - and credited
|
|
1476
|
+
* to the declaring user, as if they had pressed Acknowledge on each alert
|
|
1477
|
+
* themselves: the alert's owners are told, and the alert's feed says who
|
|
1478
|
+
* and why. A private incident is not named in that "why", because the
|
|
1479
|
+
* alert's feed entry is posted to the alert's Slack / Microsoft Teams
|
|
1480
|
+
* channels and its owners' notifications.
|
|
1481
|
+
*
|
|
1482
|
+
* Left as they are:
|
|
1483
|
+
* - alerts already acknowledged, resolved or in any later state - nothing
|
|
1484
|
+
* is ever moved backwards;
|
|
1485
|
+
* - linked alerts the project's linked-alert sync is about to move anyway
|
|
1486
|
+
* (the incident was declared straight into an acknowledged or resolved
|
|
1487
|
+
* state with the project switches on): one writer per alert, so the two
|
|
1488
|
+
* never race on its timeline.
|
|
1489
|
+
*
|
|
1490
|
+
* IncidentService runs this after the links are written and does not wait
|
|
1491
|
+
* for it. It never throws: one alert failing never stops the others.
|
|
1492
|
+
*/
|
|
1493
|
+
@CaptureSpan()
|
|
1494
|
+
public async acknowledgeAlertsDeclaredWithIncident(data: {
|
|
1495
|
+
projectId: ObjectID;
|
|
1496
|
+
incidentId: ObjectID;
|
|
1497
|
+
alertIds: Array<ObjectID>;
|
|
1498
|
+
// The alerts whose link to the incident exists (the sync runs on those).
|
|
1499
|
+
linkedAlertIds: Array<ObjectID>;
|
|
1500
|
+
acknowledgedByUserId: ObjectID | undefined;
|
|
1501
|
+
}): Promise<AcknowledgeDeclaredAlertsResult> {
|
|
1502
|
+
const result: AcknowledgeDeclaredAlertsResult = {
|
|
1503
|
+
acknowledgedAlertIds: [],
|
|
1504
|
+
alreadyAcknowledgedAlertIds: [],
|
|
1505
|
+
leftToLinkedAlertSyncAlertIds: [],
|
|
1506
|
+
failed: [],
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
const alertIds: Array<ObjectID> = uniqueIds(data.alertIds);
|
|
1510
|
+
|
|
1511
|
+
if (alertIds.length === 0) {
|
|
1512
|
+
return result;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const logAttributes: LogAttributes = {
|
|
1516
|
+
projectId: data.projectId.toString(),
|
|
1517
|
+
incidentId: data.incidentId.toString(),
|
|
1518
|
+
} as LogAttributes;
|
|
1519
|
+
|
|
1520
|
+
try {
|
|
1521
|
+
const acknowledgedState: AlertState | null =
|
|
1522
|
+
await AlertStateService.findOneBy({
|
|
1523
|
+
query: {
|
|
1524
|
+
projectId: data.projectId,
|
|
1525
|
+
isAcknowledgedState: true,
|
|
1526
|
+
},
|
|
1527
|
+
select: {
|
|
1528
|
+
_id: true,
|
|
1529
|
+
order: true,
|
|
1530
|
+
},
|
|
1531
|
+
props: {
|
|
1532
|
+
isRoot: true,
|
|
1533
|
+
},
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
if (
|
|
1537
|
+
!acknowledgedState ||
|
|
1538
|
+
!acknowledgedState._id ||
|
|
1539
|
+
acknowledgedState.order === undefined ||
|
|
1540
|
+
acknowledgedState.order === null
|
|
1541
|
+
) {
|
|
1542
|
+
const message: string =
|
|
1543
|
+
"This project has no Acknowledged alert state, so the alerts could not be acknowledged.";
|
|
1544
|
+
|
|
1545
|
+
logger.error(`IncidentAlertService: ${message}`, logAttributes);
|
|
1546
|
+
|
|
1547
|
+
for (const alertId of alertIds) {
|
|
1548
|
+
result.failed.push({ alertId: alertId, message: message });
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
return result;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const acknowledgedStateId: ObjectID = new ObjectID(
|
|
1555
|
+
acknowledgedState._id.toString(),
|
|
1556
|
+
);
|
|
1557
|
+
const acknowledgedOrder: number = acknowledgedState.order;
|
|
1558
|
+
|
|
1559
|
+
const incident: Incident | null = await IncidentService.findOneById({
|
|
1560
|
+
id: data.incidentId,
|
|
1561
|
+
select: {
|
|
1562
|
+
incidentNumber: true,
|
|
1563
|
+
incidentNumberWithPrefix: true,
|
|
1564
|
+
isPrivate: true,
|
|
1565
|
+
currentIncidentStateId: true,
|
|
1566
|
+
},
|
|
1567
|
+
props: {
|
|
1568
|
+
isRoot: true,
|
|
1569
|
+
},
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
const rootCause: string = getDeclaredAlertAcknowledgementCause({
|
|
1573
|
+
incidentNumber: formatNumber(
|
|
1574
|
+
incident?.incidentNumberWithPrefix,
|
|
1575
|
+
incident?.incidentNumber,
|
|
1576
|
+
),
|
|
1577
|
+
isIncidentPrivate: incident?.isPrivate === true,
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
const alerts: Array<Alert> = await AlertService.findBy({
|
|
1581
|
+
query: {
|
|
1582
|
+
_id: QueryHelper.any(alertIds),
|
|
1583
|
+
projectId: data.projectId,
|
|
1584
|
+
},
|
|
1585
|
+
select: {
|
|
1586
|
+
_id: true,
|
|
1587
|
+
currentAlertStateId: true,
|
|
1588
|
+
currentAlertState: {
|
|
1589
|
+
order: true,
|
|
1590
|
+
},
|
|
1591
|
+
},
|
|
1592
|
+
limit: LIMIT_PER_PROJECT,
|
|
1593
|
+
skip: 0,
|
|
1594
|
+
props: {
|
|
1595
|
+
isRoot: true,
|
|
1596
|
+
},
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
const alertById: Map<string, Alert> = new Map();
|
|
1600
|
+
|
|
1601
|
+
for (const alert of alerts) {
|
|
1602
|
+
if (alert._id) {
|
|
1603
|
+
alertById.set(normalizeId(alert._id), alert);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const ownedBySync: Set<string> =
|
|
1608
|
+
await this.getLinkedAlertsTheSyncWillMove({
|
|
1609
|
+
projectId: data.projectId,
|
|
1610
|
+
incidentId: data.incidentId,
|
|
1611
|
+
incidentStateId: incident?.currentIncidentStateId || undefined,
|
|
1612
|
+
linkedAlerts: uniqueIds(data.linkedAlertIds)
|
|
1613
|
+
.map((alertId: ObjectID): Alert | undefined => {
|
|
1614
|
+
return alertById.get(normalizeId(alertId));
|
|
1615
|
+
})
|
|
1616
|
+
.filter((alert: Alert | undefined): boolean => {
|
|
1617
|
+
return Boolean(alert);
|
|
1618
|
+
}) as Array<Alert>,
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
const alertIdsToWrite: Array<ObjectID> = [];
|
|
1622
|
+
|
|
1623
|
+
for (const alertId of alertIds) {
|
|
1624
|
+
const key: string = normalizeId(alertId);
|
|
1625
|
+
const alert: Alert | undefined = alertById.get(key);
|
|
1626
|
+
|
|
1627
|
+
if (!alert) {
|
|
1628
|
+
result.failed.push({
|
|
1629
|
+
alertId: alertId,
|
|
1630
|
+
message: "The alert could not be found in this project.",
|
|
1631
|
+
});
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const stateOrder: number | undefined =
|
|
1636
|
+
alert.currentAlertState?.order ?? undefined;
|
|
1637
|
+
|
|
1638
|
+
if (stateOrder !== undefined && stateOrder >= acknowledgedOrder) {
|
|
1639
|
+
result.alreadyAcknowledgedAlertIds.push(alertId);
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (ownedBySync.has(key)) {
|
|
1644
|
+
result.leftToLinkedAlertSyncAlertIds.push(alertId);
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
alertIdsToWrite.push(alertId);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
for (
|
|
1652
|
+
let index: number = 0;
|
|
1653
|
+
index < alertIdsToWrite.length;
|
|
1654
|
+
index += DECLARED_ALERT_ACKNOWLEDGE_CONCURRENCY
|
|
1655
|
+
) {
|
|
1656
|
+
const batch: Array<ObjectID> = alertIdsToWrite.slice(
|
|
1657
|
+
index,
|
|
1658
|
+
index + DECLARED_ALERT_ACKNOWLEDGE_CONCURRENCY,
|
|
1659
|
+
);
|
|
1660
|
+
|
|
1661
|
+
const outcomes: Array<string | null> = await Promise.all(
|
|
1662
|
+
batch.map((alertId: ObjectID): Promise<string | null> => {
|
|
1663
|
+
return this.acknowledgeDeclaredAlert({
|
|
1664
|
+
projectId: data.projectId,
|
|
1665
|
+
alertId: alertId,
|
|
1666
|
+
acknowledgedStateId: acknowledgedStateId,
|
|
1667
|
+
acknowledgedOrder: acknowledgedOrder,
|
|
1668
|
+
rootCause: rootCause,
|
|
1669
|
+
acknowledgedByUserId: data.acknowledgedByUserId,
|
|
1670
|
+
});
|
|
1671
|
+
}),
|
|
1672
|
+
);
|
|
1673
|
+
|
|
1674
|
+
batch.forEach((alertId: ObjectID, batchIndex: number) => {
|
|
1675
|
+
const failure: string | null = outcomes[batchIndex] ?? null;
|
|
1676
|
+
|
|
1677
|
+
if (failure === null) {
|
|
1678
|
+
result.acknowledgedAlertIds.push(alertId);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
result.failed.push({ alertId: alertId, message: failure });
|
|
1683
|
+
|
|
1684
|
+
logger.error(
|
|
1685
|
+
`IncidentAlertService could not acknowledge alert ${alertId.toString()} for the incident it was declared with: ${failure}`,
|
|
1686
|
+
{
|
|
1687
|
+
...logAttributes,
|
|
1688
|
+
alertId: alertId.toString(),
|
|
1689
|
+
} as LogAttributes,
|
|
1690
|
+
);
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
logger.error(
|
|
1695
|
+
`IncidentAlertService could not acknowledge the alerts an incident was declared from: ${error}`,
|
|
1696
|
+
logAttributes,
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
const settled: Set<string> = new Set(
|
|
1700
|
+
[
|
|
1701
|
+
...result.acknowledgedAlertIds,
|
|
1702
|
+
...result.alreadyAcknowledgedAlertIds,
|
|
1703
|
+
...result.leftToLinkedAlertSyncAlertIds,
|
|
1704
|
+
...result.failed.map((failure: { alertId: ObjectID }) => {
|
|
1705
|
+
return failure.alertId;
|
|
1706
|
+
}),
|
|
1707
|
+
].map((alertId: ObjectID): string => {
|
|
1708
|
+
return normalizeId(alertId);
|
|
1709
|
+
}),
|
|
1710
|
+
);
|
|
1711
|
+
|
|
1712
|
+
for (const alertId of alertIds) {
|
|
1713
|
+
if (!settled.has(normalizeId(alertId))) {
|
|
1714
|
+
result.failed.push({
|
|
1715
|
+
alertId: alertId,
|
|
1716
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
return result;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/*
|
|
1726
|
+
* The linked alerts the project's linked-alert sync (and the incident
|
|
1727
|
+
* state cascade) will move by themselves, because the incident is already
|
|
1728
|
+
* in a state their switches act on: the same plan, the same per-alert
|
|
1729
|
+
* choice. Empty when both switches are off - the default - or the incident
|
|
1730
|
+
* is before Acknowledged, which is the usual declaration.
|
|
1731
|
+
*/
|
|
1732
|
+
@CaptureSpan()
|
|
1733
|
+
private async getLinkedAlertsTheSyncWillMove(data: {
|
|
1734
|
+
projectId: ObjectID;
|
|
1735
|
+
incidentId: ObjectID;
|
|
1736
|
+
incidentStateId: ObjectID | undefined;
|
|
1737
|
+
linkedAlerts: Array<Alert>;
|
|
1738
|
+
}): Promise<Set<string>> {
|
|
1739
|
+
const willMove: Set<string> = new Set();
|
|
1740
|
+
|
|
1741
|
+
if (!data.incidentStateId || data.linkedAlerts.length === 0) {
|
|
1742
|
+
return willMove;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const switches: LinkedAlertSwitches | null =
|
|
1746
|
+
await this.getLinkedAlertSwitches(data.projectId);
|
|
1747
|
+
|
|
1748
|
+
if (!switches) {
|
|
1749
|
+
return willMove;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
const plan: LinkedAlertStatePlan | null =
|
|
1753
|
+
await this.buildLinkedAlertStatePlan({
|
|
1754
|
+
projectId: data.projectId,
|
|
1755
|
+
incidentStateId: data.incidentStateId,
|
|
1756
|
+
switches: switches,
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
if (!plan) {
|
|
1760
|
+
return willMove;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
const blockedFromResolve: Set<string> = plan.resolvedAlertState
|
|
1764
|
+
? await this.getAlertsHeldOpenByAnotherIncident({
|
|
1765
|
+
plan: plan,
|
|
1766
|
+
projectId: data.projectId,
|
|
1767
|
+
incidentId: data.incidentId,
|
|
1768
|
+
alerts: data.linkedAlerts,
|
|
1769
|
+
})
|
|
1770
|
+
: new Set();
|
|
1771
|
+
|
|
1772
|
+
for (const alert of data.linkedAlerts) {
|
|
1773
|
+
const alertId: string = alert._id ? normalizeId(alert._id) : "";
|
|
1774
|
+
|
|
1775
|
+
const alertStateOrder: number | undefined = alert.currentAlertStateId
|
|
1776
|
+
? plan.alertStateOrderById.get(normalizeId(alert.currentAlertStateId))
|
|
1777
|
+
: undefined;
|
|
1778
|
+
|
|
1779
|
+
if (!alertId || alertStateOrder === undefined) {
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
const target: AlertState | null = chooseLinkedAlertTargetState({
|
|
1784
|
+
alertStateOrder: alertStateOrder,
|
|
1785
|
+
acknowledgedAlertState: plan.acknowledgedAlertState,
|
|
1786
|
+
resolvedAlertState: plan.resolvedAlertState,
|
|
1787
|
+
isBlockedFromResolve: blockedFromResolve.has(alertId),
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
if (target) {
|
|
1791
|
+
willMove.add(alertId);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
return willMove;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/*
|
|
1799
|
+
* Acknowledges one declared alert. Null when the alert ended up
|
|
1800
|
+
* acknowledged (or past it), otherwise why not. Never throws.
|
|
1801
|
+
*
|
|
1802
|
+
* The outcome is read back from the alert rather than taken from the
|
|
1803
|
+
* write: the write may be refused because somebody acknowledged or
|
|
1804
|
+
* resolved the alert in the meantime (the outcome asked for), and a write
|
|
1805
|
+
* that "succeeded" only counts if the alert's current state moved.
|
|
1806
|
+
*/
|
|
1807
|
+
@CaptureSpan()
|
|
1808
|
+
private async acknowledgeDeclaredAlert(data: {
|
|
1809
|
+
projectId: ObjectID;
|
|
1810
|
+
alertId: ObjectID;
|
|
1811
|
+
acknowledgedStateId: ObjectID;
|
|
1812
|
+
acknowledgedOrder: number;
|
|
1813
|
+
rootCause: string;
|
|
1814
|
+
acknowledgedByUserId: ObjectID | undefined;
|
|
1815
|
+
}): Promise<string | null> {
|
|
1816
|
+
let failure: string = "The alert's state did not change to Acknowledged.";
|
|
1817
|
+
|
|
1818
|
+
try {
|
|
1819
|
+
await AlertService.changeAlertState({
|
|
1820
|
+
projectId: data.projectId,
|
|
1821
|
+
alertId: data.alertId,
|
|
1822
|
+
alertStateId: data.acknowledgedStateId,
|
|
1823
|
+
notifyOwners: true,
|
|
1824
|
+
rootCause: data.rootCause,
|
|
1825
|
+
stateChangeLog: undefined,
|
|
1826
|
+
createdByUserId: data.acknowledgedByUserId,
|
|
1827
|
+
props: {
|
|
1828
|
+
isRoot: true,
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
try {
|
|
1836
|
+
const alert: Alert | null = await AlertService.findOneBy({
|
|
1837
|
+
query: {
|
|
1838
|
+
_id: data.alertId,
|
|
1839
|
+
projectId: data.projectId,
|
|
1840
|
+
},
|
|
1841
|
+
select: {
|
|
1842
|
+
currentAlertState: {
|
|
1843
|
+
order: true,
|
|
1844
|
+
},
|
|
1845
|
+
},
|
|
1846
|
+
props: {
|
|
1847
|
+
isRoot: true,
|
|
1848
|
+
},
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
const order: number | undefined =
|
|
1852
|
+
alert?.currentAlertState?.order ?? undefined;
|
|
1853
|
+
|
|
1854
|
+
if (order !== undefined && order >= data.acknowledgedOrder) {
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
} catch {
|
|
1858
|
+
// The alert cannot be read back, so it cannot be counted as done.
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
return failure;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1260
1864
|
/*
|
|
1261
1865
|
* An incident changed state: bring its linked alerts along, when the
|
|
1262
1866
|
* project has opted in. Called fire-and-forget from
|
|
@@ -1355,8 +1959,11 @@ export class Service extends DatabaseService<Model> {
|
|
|
1355
1959
|
/*
|
|
1356
1960
|
* The link-time half: an alert was just linked to an incident that may
|
|
1357
1961
|
* already be acknowledged or resolved. Same rules as the cascade, applied to
|
|
1358
|
-
* the one alert.
|
|
1359
|
-
*
|
|
1962
|
+
* the one alert. An incident declared from alerts usually starts in its
|
|
1963
|
+
* created state, which calls for nothing; one declared straight into an
|
|
1964
|
+
* acknowledged or resolved state brings its alerts along here (and
|
|
1965
|
+
* acknowledgeAlertsDeclaredWithIncident leaves those alerts to this).
|
|
1966
|
+
* Never throws.
|
|
1360
1967
|
*/
|
|
1361
1968
|
@CaptureSpan()
|
|
1362
1969
|
public async syncAlertWithLinkedIncidentState(data: {
|