[8.x] [ResponseOps][OnWeek] Investigate isolating flapping feature in the code (#212454) (#213825)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ResponseOps][OnWeek] Investigate isolating flapping feature in the
code (#212454)](https://github.com/elastic/kibana/pull/212454)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Alexi
Doak","email":"109488926+doakalexi@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-03-10T20:01:10Z","message":"[ResponseOps][OnWeek]
Investigate isolating flapping feature in the code (#212454)\n\nPart of
https://github.com/elastic/kibana/issues/194222\n\n## Summary\n\nThis PR
moves the flapping and alert delay features into two new\nfunctions in
the alerts client `determineFlappingAlerts`
and\n`determineDelayedAlerts`. They will be called in the
rule_type_runner\nafter `processAlerts`.\n- I removed `recoveredCurrent`
and `activeCurrent` 😌 . This PR\nsimplifies them to be activeAlerts,
recoveredAlerts,\ntrackedRecoveredAlerts, and trackedActiveAlerts.
trackedRecoveredAlerts\nand trackedActiveAlerts are the alerts that will
be stored in the task\nstate.\n- I also updated the logic so that the
AAD docs and state match to help\nwith ongoing work to use AAD docs
instead of the state.\n- I removed an optimization for the task state to
stop tracking a\nrecovered alert if it wasn't flapping and doesn't have
state changes. We\nwon't need this optimization with using AAD docs
instead of the state.\n\n### Checklist\n\nCheck the PR satisfies
following conditions. \n\n- [ ] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"eed986162baf239db04fe117e1f045c28ad43c91","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","backport:version","v9.1.0","v8.19.0"],"title":"[ResponseOps][OnWeek]
Investigate isolating flapping feature in the
code","number":212454,"url":"https://github.com/elastic/kibana/pull/212454","mergeCommit":{"message":"[ResponseOps][OnWeek]
Investigate isolating flapping feature in the code (#212454)\n\nPart of
https://github.com/elastic/kibana/issues/194222\n\n## Summary\n\nThis PR
moves the flapping and alert delay features into two new\nfunctions in
the alerts client `determineFlappingAlerts`
and\n`determineDelayedAlerts`. They will be called in the
rule_type_runner\nafter `processAlerts`.\n- I removed `recoveredCurrent`
and `activeCurrent` 😌 . This PR\nsimplifies them to be activeAlerts,
recoveredAlerts,\ntrackedRecoveredAlerts, and trackedActiveAlerts.
trackedRecoveredAlerts\nand trackedActiveAlerts are the alerts that will
be stored in the task\nstate.\n- I also updated the logic so that the
AAD docs and state match to help\nwith ongoing work to use AAD docs
instead of the state.\n- I removed an optimization for the task state to
stop tracking a\nrecovered alert if it wasn't flapping and doesn't have
state changes. We\nwon't need this optimization with using AAD docs
instead of the state.\n\n### Checklist\n\nCheck the PR satisfies
following conditions. \n\n- [ ] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"eed986162baf239db04fe117e1f045c28ad43c91"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/212454","number":212454,"mergeCommit":{"message":"[ResponseOps][OnWeek]
Investigate isolating flapping feature in the code (#212454)\n\nPart of
https://github.com/elastic/kibana/issues/194222\n\n## Summary\n\nThis PR
moves the flapping and alert delay features into two new\nfunctions in
the alerts client `determineFlappingAlerts`
and\n`determineDelayedAlerts`. They will be called in the
rule_type_runner\nafter `processAlerts`.\n- I removed `recoveredCurrent`
and `activeCurrent` 😌 . This PR\nsimplifies them to be activeAlerts,
recoveredAlerts,\ntrackedRecoveredAlerts, and trackedActiveAlerts.
trackedRecoveredAlerts\nand trackedActiveAlerts are the alerts that will
be stored in the task\nstate.\n- I also updated the logic so that the
AAD docs and state match to help\nwith ongoing work to use AAD docs
instead of the state.\n- I removed an optimization for the task state to
stop tracking a\nrecovered alert if it wasn't flapping and doesn't have
state changes. We\nwon't need this optimization with using AAD docs
instead of the state.\n\n### Checklist\n\nCheck the PR satisfies
following conditions. \n\n- [ ] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"eed986162baf239db04fe117e1f045c28ad43c91"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Alexi Doak 2025-03-11 07:15:39 -07:00 committed by GitHub
parent a9dda8c819
commit 1ee1b84d55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 2039 additions and 2245 deletions

View file

@ -189,7 +189,7 @@ describe('createAlertFactory()', () => {
test('returns recovered alerts when setsRecoveryContext is true', () => {
(processAlerts as jest.Mock).mockReturnValueOnce({
currentRecoveredAlerts: {
recoveredAlerts: {
z: {
id: 'z',
state: { foo: true },

View file

@ -10,7 +10,6 @@ import { cloneDeep } from 'lodash';
import { AlertInstanceContext, AlertInstanceState } from '../types';
import { Alert, PublicAlert } from './alert';
import { processAlerts } from '../lib';
import { DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings';
export interface AlertFactory<
State extends AlertInstanceState,
@ -142,23 +141,17 @@ export function createAlertFactory<
return [];
}
const { currentRecoveredAlerts } = processAlerts<
State,
Context,
ActionGroupIds,
ActionGroupIds
>({
alerts,
existingAlerts: originalAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit,
alertLimit: maxAlerts,
autoRecoverAlerts,
// flappingSettings.enabled is false, as we only want to use this function to get the recovered alerts
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
return Object.keys(currentRecoveredAlerts ?? {}).map(
(alertId: string) => currentRecoveredAlerts[alertId]
const { recoveredAlerts } = processAlerts<State, Context, ActionGroupIds, ActionGroupIds>(
{
alerts,
existingAlerts: originalAlerts,
hasReachedAlertLimit,
alertLimit: maxAlerts,
autoRecoverAlerts,
}
);
return Object.keys(recoveredAlerts ?? {}).map(
(alertId: string) => recoveredAlerts[alertId]
);
},
};

View file

@ -15,13 +15,15 @@ const createAlertsClientMock = () => {
getMaintenanceWindowScopedQueryAlerts: jest.fn(),
getTrackedAlerts: jest.fn(),
getProcessedAlerts: jest.fn(),
getAlertsToSerialize: jest.fn(),
getRawAlertInstancesForState: jest.fn(),
hasReachedAlertLimit: jest.fn(),
checkLimitUsage: jest.fn(),
persistAlerts: jest.fn(),
getSummarizedAlerts: jest.fn(),
factory: jest.fn(),
client: jest.fn(),
determineDelayedAlerts: jest.fn(),
determineFlappingAlerts: jest.fn(),
};
});
};

View file

@ -57,7 +57,7 @@ import { AlertsClient, AlertsClientParams } from './alerts_client';
import {
GetSummarizedAlertsParams,
GetMaintenanceWindowScopedQueryAlertsParams,
ProcessAlertsOpts,
DetermineDelayedAlertsOpts,
LogAlertsOpts,
} from './types';
import { legacyAlertsClientMock } from './legacy_alerts_client.mock';
@ -323,7 +323,7 @@ const logTags = { tags: ['test.rule-type', '1', 'alerts-client'] };
describe('Alerts Client', () => {
let alertsClientParams: AlertsClientParams;
let processAlertsOpts: ProcessAlertsOpts;
let determineDelayedAlertsOpts: DetermineDelayedAlertsOpts;
let logAlertsOpts: LogAlertsOpts;
beforeAll(() => {
@ -375,9 +375,8 @@ describe('Alerts Client', () => {
],
maintenanceWindowsWithoutScopedQueryIds: ['test-id1', 'test-id2'],
});
processAlertsOpts = {
determineDelayedAlertsOpts = {
ruleRunMetricsStore,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
alertDelay: 0,
};
logAlertsOpts = { shouldLogAlerts: false, ruleRunMetricsStore };
@ -563,14 +562,16 @@ describe('Alerts Client', () => {
alertExecutorService.create('1').scheduleActions('default');
alertExecutorService.create('2').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
const { alertsToReturn } = alertsClient.getAlertsToSerialize();
const uuid1 = alertsToReturn['1'].meta?.uuid;
const uuid2 = alertsToReturn['2'].meta?.uuid;
const { rawActiveAlerts } = alertsClient.getRawAlertInstancesForState();
const uuid1 = rawActiveAlerts['1'].meta?.uuid;
const uuid2 = rawActiveAlerts['2'].meta?.uuid;
expect(clusterClient.bulk).toHaveBeenCalledWith({
index: '.alerts-test.alerts-default',
@ -610,14 +611,16 @@ describe('Alerts Client', () => {
alertExecutorService.create('1').scheduleActions('default');
alertExecutorService.create('2').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
const { alertsToReturn } = alertsClient.getAlertsToSerialize();
const uuid1 = alertsToReturn['1'].meta?.uuid;
const uuid2 = alertsToReturn['2'].meta?.uuid;
const { rawActiveAlerts } = alertsClient.getRawAlertInstancesForState();
const uuid1 = rawActiveAlerts['1'].meta?.uuid;
const uuid2 = rawActiveAlerts['2'].meta?.uuid;
expect(clusterClient.bulk).toHaveBeenCalledWith({
index: '.alerts-test.alerts-default',
@ -656,7 +659,9 @@ describe('Alerts Client', () => {
const alertExecutorService = alertsClient.factory();
alertExecutorService.create('1').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
@ -704,13 +709,15 @@ describe('Alerts Client', () => {
alertExecutorService.create('1').scheduleActions('default');
alertExecutorService.create('2').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
const { alertsToReturn } = alertsClient.getAlertsToSerialize();
const uuid2 = alertsToReturn['2'].meta?.uuid;
const { rawActiveAlerts } = alertsClient.getRawAlertInstancesForState();
const uuid2 = rawActiveAlerts['2'].meta?.uuid;
expect(clusterClient.bulk).toHaveBeenCalledWith({
index: '.alerts-test.alerts-default',
@ -777,13 +784,15 @@ describe('Alerts Client', () => {
alertExecutorService.create('1').scheduleActions('default');
alertExecutorService.create('2').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
const { alertsToReturn } = alertsClient.getAlertsToSerialize();
const uuid2 = alertsToReturn['2'].meta?.uuid;
const { rawActiveAlerts } = alertsClient.getRawAlertInstancesForState();
const uuid2 = rawActiveAlerts['2'].meta?.uuid;
expect(clusterClient.bulk).toHaveBeenCalledWith({
index: '.alerts-test.alerts-default',
@ -905,14 +914,16 @@ describe('Alerts Client', () => {
alertExecutorService.create('1').scheduleActions('default');
alertExecutorService.create('2').scheduleActions('default'); // will be skipped as getProcessedAlerts does not return it
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
expect(spy).toHaveBeenCalledTimes(5);
expect(spy).toHaveBeenNthCalledWith(1, 'active');
expect(spy).toHaveBeenNthCalledWith(2, 'recoveredCurrent');
expect(spy).toHaveBeenNthCalledWith(2, 'recovered');
expect(spy).toHaveBeenNthCalledWith(3, 'new');
expect(logger.error).toHaveBeenCalledWith(
@ -986,13 +997,15 @@ describe('Alerts Client', () => {
alertExecutorService.create('2').scheduleActions('default');
alertExecutorService.create('3').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
const { alertsToReturn } = alertsClient.getAlertsToSerialize();
const uuid3 = alertsToReturn['3'].meta?.uuid;
const { rawActiveAlerts } = alertsClient.getRawAlertInstancesForState();
const uuid3 = rawActiveAlerts['3'].meta?.uuid;
expect(clusterClient.bulk).toHaveBeenCalledWith({
index: '.alerts-test.alerts-default',
@ -1085,13 +1098,15 @@ describe('Alerts Client', () => {
alertExecutorService.create('2').scheduleActions('default');
alertExecutorService.create('3').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
const { alertsToReturn } = alertsClient.getAlertsToSerialize();
const uuid3 = alertsToReturn['3'].meta?.uuid;
const { rawActiveAlerts } = alertsClient.getRawAlertInstancesForState();
const uuid3 = rawActiveAlerts['3'].meta?.uuid;
expect(clusterClient.bulk).toHaveBeenCalledWith({
index: '.alerts-test.alerts-default',
@ -1244,13 +1259,15 @@ describe('Alerts Client', () => {
alertExecutorService.create('2').scheduleActions('default');
alertExecutorService.create('3').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
const { alertsToReturn } = alertsClient.getAlertsToSerialize();
const uuid3 = alertsToReturn['3'].meta?.uuid;
const { rawActiveAlerts } = alertsClient.getRawAlertInstancesForState();
const uuid3 = rawActiveAlerts['3'].meta?.uuid;
expect(clusterClient.bulk).toHaveBeenCalledWith({
index: '.alerts-test.alerts-default',
@ -1362,13 +1379,15 @@ describe('Alerts Client', () => {
alertExecutorService.create('2').scheduleActions('default');
alertExecutorService.create('3').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
const { alertsToReturn } = alertsClient.getAlertsToSerialize();
const uuid3 = alertsToReturn['3'].meta?.uuid;
const { rawActiveAlerts } = alertsClient.getRawAlertInstancesForState();
const uuid3 = rawActiveAlerts['3'].meta?.uuid;
expect(clusterClient.bulk).toHaveBeenCalledWith({
index: '.alerts-test.alerts-default',
@ -1444,7 +1463,9 @@ describe('Alerts Client', () => {
// Report no alerts
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
@ -1509,7 +1530,9 @@ describe('Alerts Client', () => {
alertExecutorService.create('1').scheduleActions('default');
alertExecutorService.create('2').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
@ -1569,7 +1592,9 @@ describe('Alerts Client', () => {
alertExecutorService.create('1').scheduleActions('default');
alertExecutorService.create('2').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
@ -1628,7 +1653,9 @@ describe('Alerts Client', () => {
alertExecutorService.create('1').scheduleActions('default');
alertExecutorService.create('2').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
@ -1714,7 +1741,9 @@ describe('Alerts Client', () => {
const alertExecutorService = alertsClient.factory();
alertExecutorService.create('1').scheduleActions('default');
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await expect(alertsClient.persistAlerts()).rejects.toThrowError(
@ -2536,14 +2565,16 @@ describe('Alerts Client', () => {
payload: { count: 2, url: `https://url2` },
});
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
const { alertsToReturn } = alertsClient.getAlertsToSerialize();
const uuid1 = alertsToReturn['1'].meta?.uuid;
const uuid2 = alertsToReturn['2'].meta?.uuid;
const { rawActiveAlerts } = alertsClient.getRawAlertInstancesForState();
const uuid1 = rawActiveAlerts['1'].meta?.uuid;
const uuid2 = rawActiveAlerts['2'].meta?.uuid;
expect(clusterClient.bulk).toHaveBeenCalledWith({
index: '.alerts-test.alerts-default',
@ -2814,7 +2845,9 @@ describe('Alerts Client', () => {
payload: { count: 100, url: `https://elastic.co` },
});
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
@ -2915,7 +2948,9 @@ describe('Alerts Client', () => {
payload: { count: 100, url: `https://elastic.co` },
});
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();
@ -3012,7 +3047,9 @@ describe('Alerts Client', () => {
payload: { count: 100, url: `https://elastic.co` },
});
await alertsClient.processAlerts(processAlertsOpts);
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts(determineDelayedAlertsOpts);
alertsClient.logAlerts(logAlertsOpts);
await alertsClient.persistAlerts();

View file

@ -37,7 +37,7 @@ import {
IIndexPatternString,
} from '../alerts_service/resource_installer_utils';
import { CreateAlertsClientParams } from '../alerts_service/alerts_service';
import type { AlertRule, LogAlertsOpts, ProcessAlertsOpts, SearchResult } from './types';
import type { AlertRule, LogAlertsOpts, SearchResult, DetermineDelayedAlertsOpts } from './types';
import {
IAlertsClient,
InitializeExecutionOpts,
@ -318,8 +318,16 @@ export class AlertsClient<
return this.legacyAlertsClient.checkLimitUsage();
}
public async processAlerts(opts: ProcessAlertsOpts) {
await this.legacyAlertsClient.processAlerts(opts);
public async processAlerts() {
await this.legacyAlertsClient.processAlerts();
}
public determineFlappingAlerts() {
this.legacyAlertsClient.determineFlappingAlerts();
}
public determineDelayedAlerts(opts: DetermineDelayedAlertsOpts) {
this.legacyAlertsClient.determineDelayedAlerts(opts);
}
public logAlerts(opts: LogAlertsOpts) {
@ -327,7 +335,7 @@ export class AlertsClient<
}
public getProcessedAlerts(
type: 'new' | 'active' | 'activeCurrent' | 'recovered' | 'recoveredCurrent'
type: 'new' | 'active' | 'trackedActiveAlerts' | 'recovered' | 'trackedRecoveredAlerts'
) {
return this.legacyAlertsClient.getProcessedAlerts(type);
}
@ -339,15 +347,8 @@ export class AlertsClient<
return await this.updatePersistedAlertsWithMaintenanceWindowIds();
}
public getAlertsToSerialize() {
// The flapping value that is persisted inside the task manager state (and used in the next execution)
// is different than the value that should be written to the alert document. For this reason, we call
// getAlertsToSerialize() twice, once before building and bulk indexing alert docs and once after to return
// the value for task state serialization
// This will be a blocker if ever we want to stop serializing alert data inside the task state and just use
// the fetched alert document.
return this.legacyAlertsClient.getAlertsToSerialize();
public getRawAlertInstancesForState() {
return this.legacyAlertsClient.getRawAlertInstancesForState();
}
public factory() {
@ -423,18 +424,17 @@ export class AlertsClient<
const currentTime = this.startedAtString ?? new Date().toISOString();
const esClient = await this.options.elasticsearchClientPromise;
const { alertsToReturn, recoveredAlertsToReturn } =
this.legacyAlertsClient.getAlertsToSerialize(false);
const { rawActiveAlerts, rawRecoveredAlerts } = this.getRawAlertInstancesForState();
const activeAlerts = this.legacyAlertsClient.getProcessedAlerts('active');
const currentRecoveredAlerts = this.legacyAlertsClient.getProcessedAlerts('recoveredCurrent');
const recoveredAlerts = this.legacyAlertsClient.getProcessedAlerts('recovered');
// TODO - Lifecycle alerts set some other fields based on alert status
// Example: workflow status - default to 'open' if not set
// event action: new alert = 'new', active alert: 'active', otherwise 'close'
const activeAlertsToIndex: Array<Alert & AlertData> = [];
for (const id of keys(alertsToReturn)) {
for (const id of keys(rawActiveAlerts)) {
// See if there's an existing active alert document
if (!!activeAlerts[id]) {
if (
@ -498,12 +498,12 @@ export class AlertsClient<
}
const recoveredAlertsToIndex: Array<Alert & AlertData> = [];
for (const id of keys(recoveredAlertsToReturn)) {
for (const id of keys(rawRecoveredAlerts)) {
// See if there's an existing alert document
// If there is not, log an error because there should be
if (Object.hasOwn(this.fetchedAlerts.data, id)) {
recoveredAlertsToIndex.push(
currentRecoveredAlerts[id]
recoveredAlerts[id]
? buildRecoveredAlert<
AlertData,
LegacyState,
@ -512,7 +512,7 @@ export class AlertsClient<
RecoveryActionGroupId
>({
alert: this.fetchedAlerts.data[id],
legacyAlert: currentRecoveredAlerts[id],
legacyAlert: recoveredAlerts[id],
rule: this.rule,
runTimestamp: this.runTimestampString,
timestamp: currentTime,
@ -522,7 +522,7 @@ export class AlertsClient<
})
: buildUpdatedRecoveredAlert<AlertData>({
alert: this.fetchedAlerts.data[id],
legacyRawAlert: recoveredAlertsToReturn[id],
legacyRawAlert: rawRecoveredAlerts[id],
runTimestamp: this.runTimestampString,
timestamp: currentTime,
rule: this.rule,

View file

@ -13,13 +13,15 @@ const createLegacyAlertsClientMock = () => {
getProcessedAlerts: jest.fn(),
processAlerts: jest.fn(),
logAlerts: jest.fn(),
getAlertsToSerialize: jest.fn(),
getRawAlertInstancesForState: jest.fn(),
hasReachedAlertLimit: jest.fn(),
checkLimitUsage: jest.fn(),
persistAlerts: jest.fn(),
getAlert: jest.fn(),
factory: jest.fn(),
client: jest.fn(),
determineDelayedAlerts: jest.fn(),
determineFlappingAlerts: jest.fn(),
};
});
};

View file

@ -11,14 +11,15 @@ import { LegacyAlertsClient } from './legacy_alerts_client';
import { createAlertFactory, getPublicAlertFactory } from '../alert/create_alert_factory';
import { Alert } from '../alert/alert';
import { ruleRunMetricsStoreMock } from '../lib/rule_run_metrics_store.mock';
import { getAlertsForNotification, processAlerts } from '../lib';
import { trimRecoveredAlerts } from '../lib/trim_recovered_alerts';
import { processAlerts } from '../lib';
import { DEFAULT_FLAPPING_SETTINGS } from '../../common/rules_settings';
import { schema } from '@kbn/config-schema';
import { maintenanceWindowsServiceMock } from '../task_runner/maintenance_windows/maintenance_windows_service.mock';
import { getMockMaintenanceWindow } from '../data/maintenance_window/test_helpers';
import { KibanaRequest } from '@kbn/core/server';
import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock';
import { determineFlappingAlerts } from '../lib/flapping/determine_flapping_alerts';
import { determineDelayedAlerts } from '../lib/determine_delayed_alerts';
const maintenanceWindowsService = maintenanceWindowsServiceMock.create();
const scheduleActions = jest.fn();
@ -69,15 +70,15 @@ jest.mock('../lib', () => {
};
});
jest.mock('../lib/trim_recovered_alerts', () => {
jest.mock('../lib/flapping/determine_flapping_alerts', () => {
return {
trimRecoveredAlerts: jest.fn(),
determineFlappingAlerts: jest.fn(),
};
});
jest.mock('../lib/get_alerts_for_notification', () => {
jest.mock('../lib/determine_delayed_alerts', () => {
return {
getAlertsForNotification: jest.fn(),
determineDelayedAlerts: jest.fn(),
};
});
@ -258,7 +259,7 @@ describe('Legacy Alerts Client', () => {
expect(alertsClient.getMaxAlertLimit()).toBe(1000);
});
test('processAlerts() should call processAlerts, trimRecoveredAlerts and getAlertsForNotifications', async () => {
test('processAlerts() should call processAlerts', async () => {
maintenanceWindowsService.getMaintenanceWindows.mockReturnValue({
maintenanceWindows: [
{
@ -284,24 +285,6 @@ describe('Legacy Alerts Client', () => {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
currentRecoveredAlerts: {},
recoveredAlerts: {},
});
(trimRecoveredAlerts as jest.Mock).mockReturnValue({
trimmedAlertsRecovered: {},
earlyRecoveredAlerts: {},
});
(getAlertsForNotification as jest.Mock).mockReturnValue({
newAlerts: {},
activeAlerts: {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
currentActiveAlerts: {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
currentRecoveredAlerts: {},
recoveredAlerts: {},
});
const alertsClient = new LegacyAlertsClient({
@ -315,11 +298,7 @@ describe('Legacy Alerts Client', () => {
await alertsClient.initializeExecution(defaultExecutionOpts);
await alertsClient.processAlerts({
ruleRunMetricsStore,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
alertDelay: 5,
});
await alertsClient.processAlerts();
expect(processAlerts).toHaveBeenCalledWith({
alerts: {
@ -330,34 +309,12 @@ describe('Legacy Alerts Client', () => {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 1000,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
startedAt: null,
});
expect(trimRecoveredAlerts).toHaveBeenCalledWith(logger, {}, 1000);
expect(getAlertsForNotification).toHaveBeenCalledWith(
{
enabled: true,
lookBackWindow: 20,
statusChangeThreshold: 4,
},
'default',
5,
{},
{
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
{},
{},
null
);
expect(alertsClient.getProcessedAlerts('active')).toEqual({
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
@ -398,24 +355,6 @@ describe('Legacy Alerts Client', () => {
activeAlerts: {
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
currentRecoveredAlerts: {},
recoveredAlerts: {},
});
(trimRecoveredAlerts as jest.Mock).mockReturnValue({
trimmedAlertsRecovered: {},
earlyRecoveredAlerts: {},
});
(getAlertsForNotification as jest.Mock).mockReturnValue({
newAlerts: {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
},
activeAlerts: {
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
currentActiveAlerts: {
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
currentRecoveredAlerts: {},
recoveredAlerts: {},
});
const alertsClient = new LegacyAlertsClient({
@ -434,11 +373,7 @@ describe('Legacy Alerts Client', () => {
},
});
await alertsClient.processAlerts({
ruleRunMetricsStore,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
alertDelay: 5,
});
await alertsClient.processAlerts();
expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({
eventLogger: alertingEventLogger,
@ -446,28 +381,6 @@ describe('Legacy Alerts Client', () => {
ruleTypeCategory: 'test',
spaceId: 'space1',
});
expect(getAlertsForNotification).toHaveBeenCalledWith(
{
enabled: true,
lookBackWindow: 20,
statusChangeThreshold: 4,
},
'default',
5,
{
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', {
...testAlert1,
meta: { ...testAlert1.meta, maintenanceWindowIds: ['test-id1', 'test-id2'] },
}),
},
{
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
{},
{},
null
);
});
test('isTrackedAlert() should return true if alert was active in a previous execution, false otherwise', async () => {
@ -485,4 +398,96 @@ describe('Legacy Alerts Client', () => {
expect(alertsClient.isTrackedAlert('2')).toBe(true);
expect(alertsClient.isTrackedAlert('3')).toBe(false);
});
test('determineFlappingAlerts() should call determineFlappingAlerts', async () => {
(determineFlappingAlerts as jest.Mock).mockReturnValue({
newAlerts: {},
activeAlerts: {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
trackedActiveAlerts: {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
trackedRecoveredAlerts: {},
recoveredAlerts: {},
});
const alertsClient = new LegacyAlertsClient({
alertingEventLogger,
logger,
request: fakeRequest,
spaceId: 'space1',
ruleType,
maintenanceWindowsService,
});
await alertsClient.initializeExecution(defaultExecutionOpts);
alertsClient.determineFlappingAlerts();
expect(determineFlappingAlerts).toHaveBeenCalledWith({
logger,
newAlerts: {},
activeAlerts: {},
recoveredAlerts: {},
flappingSettings: {
enabled: true,
lookBackWindow: 20,
statusChangeThreshold: 4,
},
previouslyRecoveredAlerts: {},
actionGroupId: 'default',
maxAlerts: 1000,
});
expect(alertsClient.getProcessedAlerts('active')).toEqual({
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
});
});
test('determineDelayedAlerts() should call determineDelayedAlerts', async () => {
(determineDelayedAlerts as jest.Mock).mockReturnValue({
newAlerts: {},
activeAlerts: {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
},
trackedActiveAlerts: {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
},
trackedRecoveredAlerts: {},
recoveredAlerts: {},
});
const alertsClient = new LegacyAlertsClient({
alertingEventLogger,
logger,
request: fakeRequest,
spaceId: 'space1',
ruleType,
maintenanceWindowsService,
});
await alertsClient.initializeExecution(defaultExecutionOpts);
alertsClient.determineDelayedAlerts({
ruleRunMetricsStore,
alertDelay: 5,
});
expect(determineDelayedAlerts).toHaveBeenCalledWith({
newAlerts: {},
activeAlerts: {},
trackedActiveAlerts: {},
recoveredAlerts: {},
trackedRecoveredAlerts: {},
alertDelay: 5,
ruleRunMetricsStore,
startedAt: null,
});
expect(alertsClient.getProcessedAlerts('active')).toEqual({
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
});
});
});

View file

@ -5,20 +5,14 @@
* 2.0.
*/
import { KibanaRequest, Logger } from '@kbn/core/server';
import { cloneDeep, keys, merge } from 'lodash';
import { cloneDeep, keys } from 'lodash';
import { Alert } from '../alert/alert';
import {
AlertFactory,
createAlertFactory,
getPublicAlertFactory,
} from '../alert/create_alert_factory';
import {
determineAlertsToReturn,
processAlerts,
setFlapping,
getAlertsForNotification,
} from '../lib';
import { trimRecoveredAlerts } from '../lib/trim_recovered_alerts';
import { toRawAlertInstances, processAlerts } from '../lib';
import { logAlerts } from '../task_runner/log_alerts';
import { AlertInstanceContext, AlertInstanceState, WithoutReservedActionGroups } from '../types';
import {
@ -28,14 +22,16 @@ import {
import {
IAlertsClient,
InitializeExecutionOpts,
ProcessAlertsOpts,
LogAlertsOpts,
TrackedAlerts,
DetermineDelayedAlertsOpts,
} from './types';
import { DEFAULT_MAX_ALERTS } from '../config';
import { UntypedNormalizedRuleType } from '../rule_type_registry';
import { MaintenanceWindowsService } from '../task_runner/maintenance_windows';
import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event_logger';
import { determineFlappingAlerts } from '../lib/flapping/determine_flapping_alerts';
import { determineDelayedAlerts } from '../lib/determine_delayed_alerts';
export interface LegacyAlertsClientParams {
alertingEventLogger: AlertingEventLogger;
@ -70,9 +66,9 @@ export class LegacyAlertsClient<
private processedAlerts: {
new: Record<string, Alert<State, Context, ActionGroupIds>>;
active: Record<string, Alert<State, Context, ActionGroupIds>>;
activeCurrent: Record<string, Alert<State, Context, ActionGroupIds>>;
trackedActiveAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
recovered: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
recoveredCurrent: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
trackedRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
};
private alertFactory?: AlertFactory<
@ -85,9 +81,9 @@ export class LegacyAlertsClient<
this.processedAlerts = {
new: {},
active: {},
activeCurrent: {},
trackedActiveAlerts: {},
recovered: {},
recoveredCurrent: {},
trackedRecoveredAlerts: {},
};
}
@ -145,24 +141,17 @@ export class LegacyAlertsClient<
return !!this.trackedAlerts.active[id];
}
public async processAlerts({
flappingSettings,
alertDelay,
ruleRunMetricsStore,
}: ProcessAlertsOpts) {
public async processAlerts() {
const {
newAlerts: processedAlertsNew,
activeAlerts: processedAlertsActive,
currentRecoveredAlerts: processedAlertsRecoveredCurrent,
recoveredAlerts: processedAlertsRecovered,
} = processAlerts<State, Context, ActionGroupIds, RecoveryActionGroupId>({
alerts: this.reportedAlerts,
existingAlerts: this.trackedAlerts.active,
previouslyRecoveredAlerts: this.trackedAlerts.recovered,
hasReachedAlertLimit: this.alertFactory!.hasReachedAlertLimit(),
alertLimit: this.maxAlerts,
autoRecoverAlerts: this.options.ruleType.autoRecoverAlerts ?? true,
flappingSettings,
startedAt: this.startedAtString,
});
@ -191,30 +180,11 @@ export class LegacyAlertsClient<
}
}
const { trimmedAlertsRecovered, earlyRecoveredAlerts } = trimRecoveredAlerts(
this.options.logger,
processedAlertsRecovered,
this.maxAlerts
);
const alerts = getAlertsForNotification<State, Context, ActionGroupIds, RecoveryActionGroupId>(
flappingSettings,
this.options.ruleType.defaultActionGroupId,
alertDelay,
processedAlertsNew,
processedAlertsActive,
trimmedAlertsRecovered,
processedAlertsRecoveredCurrent,
this.startedAtString
);
ruleRunMetricsStore.setNumberOfDelayedAlerts(alerts.delayedAlertsCount);
alerts.currentRecoveredAlerts = merge(alerts.currentRecoveredAlerts, earlyRecoveredAlerts);
this.processedAlerts.new = alerts.newAlerts;
this.processedAlerts.active = alerts.activeAlerts;
this.processedAlerts.activeCurrent = alerts.currentActiveAlerts;
this.processedAlerts.recovered = alerts.recoveredAlerts;
this.processedAlerts.recoveredCurrent = alerts.currentRecoveredAlerts;
this.processedAlerts.new = processedAlertsNew;
this.processedAlerts.active = processedAlertsActive;
this.processedAlerts.trackedActiveAlerts = processedAlertsActive;
this.processedAlerts.recovered = processedAlertsRecovered;
this.processedAlerts.trackedRecoveredAlerts = processedAlertsRecovered;
}
public logAlerts({ ruleRunMetricsStore, shouldLogAlerts }: LogAlertsOpts) {
@ -222,8 +192,8 @@ export class LegacyAlertsClient<
logger: this.options.logger,
alertingEventLogger: this.options.alertingEventLogger,
newAlerts: this.processedAlerts.new,
activeAlerts: this.processedAlerts.activeCurrent,
recoveredAlerts: this.processedAlerts.recoveredCurrent,
activeAlerts: this.processedAlerts.active,
recoveredAlerts: this.processedAlerts.recovered,
ruleLogPrefix: this.ruleLogPrefix,
ruleRunMetricsStore,
canSetRecoveryContext: this.options.ruleType.doesSetRecoveryContext ?? false,
@ -232,7 +202,7 @@ export class LegacyAlertsClient<
}
public getProcessedAlerts(
type: 'new' | 'active' | 'activeCurrent' | 'recovered' | 'recoveredCurrent'
type: 'new' | 'active' | 'trackedActiveAlerts' | 'recovered' | 'trackedRecoveredAlerts'
) {
if (Object.hasOwn(this.processedAlerts, type)) {
return this.processedAlerts[type];
@ -241,21 +211,53 @@ export class LegacyAlertsClient<
return {};
}
public getAlertsToSerialize(shouldSetFlappingAndOptimize: boolean = true) {
if (shouldSetFlappingAndOptimize) {
setFlapping<State, Context, ActionGroupIds, RecoveryActionGroupId>(
this.flappingSettings,
this.processedAlerts.active,
this.processedAlerts.recovered
);
}
return determineAlertsToReturn<State, Context, ActionGroupIds, RecoveryActionGroupId>(
this.processedAlerts.active,
this.processedAlerts.recovered,
shouldSetFlappingAndOptimize
public getRawAlertInstancesForState() {
return toRawAlertInstances<State, Context, ActionGroupIds, RecoveryActionGroupId>(
this.processedAlerts.trackedActiveAlerts,
this.processedAlerts.trackedRecoveredAlerts
);
}
public determineFlappingAlerts() {
if (this.flappingSettings.enabled) {
const alerts = determineFlappingAlerts({
logger: this.options.logger,
newAlerts: this.processedAlerts.new,
activeAlerts: this.processedAlerts.active,
recoveredAlerts: this.processedAlerts.recovered,
flappingSettings: this.flappingSettings,
previouslyRecoveredAlerts: this.trackedAlerts.recovered,
actionGroupId: this.options.ruleType.defaultActionGroupId,
maxAlerts: this.maxAlerts,
});
this.processedAlerts.new = alerts.newAlerts;
this.processedAlerts.active = alerts.activeAlerts;
this.processedAlerts.trackedActiveAlerts = alerts.trackedActiveAlerts;
this.processedAlerts.recovered = alerts.recoveredAlerts;
this.processedAlerts.trackedRecoveredAlerts = alerts.trackedRecoveredAlerts;
}
}
public determineDelayedAlerts(opts: DetermineDelayedAlertsOpts) {
const alerts = determineDelayedAlerts({
newAlerts: this.processedAlerts.new,
activeAlerts: this.processedAlerts.active,
trackedActiveAlerts: this.processedAlerts.trackedActiveAlerts,
recoveredAlerts: this.processedAlerts.recovered,
trackedRecoveredAlerts: this.processedAlerts.trackedRecoveredAlerts,
alertDelay: opts.alertDelay,
startedAt: this.startedAtString,
ruleRunMetricsStore: opts.ruleRunMetricsStore,
});
this.processedAlerts.new = alerts.newAlerts;
this.processedAlerts.active = alerts.activeAlerts;
this.processedAlerts.trackedActiveAlerts = alerts.trackedActiveAlerts;
this.processedAlerts.recovered = alerts.recoveredAlerts;
this.processedAlerts.trackedRecoveredAlerts = alerts.trackedRecoveredAlerts;
}
public hasReachedAlertLimit(): boolean {
return this.alertFactory!.hasReachedAlertLimit();
}

View file

@ -74,20 +74,20 @@ export interface IAlertsClient<
initializeExecution(opts: InitializeExecutionOpts): Promise<void>;
hasReachedAlertLimit(): boolean;
checkLimitUsage(): void;
processAlerts(opts: ProcessAlertsOpts): void;
processAlerts(): void;
logAlerts(opts: LogAlertsOpts): void;
getProcessedAlerts(
type: 'new' | 'active' | 'activeCurrent'
type: 'new' | 'active' | 'trackedActiveAlerts'
): Record<string, LegacyAlert<State, Context, ActionGroupIds>> | {};
getProcessedAlerts(
type: 'recovered' | 'recoveredCurrent'
type: 'recovered' | 'trackedRecoveredAlerts'
): Record<string, LegacyAlert<State, Context, RecoveryActionGroupId>> | {};
persistAlerts(): Promise<{ alertIds: string[]; maintenanceWindowIds: string[] } | null>;
isTrackedAlert(id: string): boolean;
getSummarizedAlerts?(params: GetSummarizedAlertsParams): Promise<SummarizedAlerts>;
getAlertsToSerialize(): {
alertsToReturn: Record<string, RawAlertInstance>;
recoveredAlertsToReturn: Record<string, RawAlertInstance>;
getRawAlertInstancesForState(): {
rawActiveAlerts: Record<string, RawAlertInstance>;
rawRecoveredAlerts: Record<string, RawAlertInstance>;
};
factory(): PublicAlertFactory<
State,
@ -100,6 +100,8 @@ export interface IAlertsClient<
Context,
WithoutReservedActionGroups<ActionGroupIds, RecoveryActionGroupId>
> | null;
determineFlappingAlerts(): void;
determineDelayedAlerts(opts: DetermineDelayedAlertsOpts): void;
}
export interface ProcessAndLogAlertsOpts {
@ -111,12 +113,10 @@ export interface ProcessAndLogAlertsOpts {
alertDelay: number;
}
export interface ProcessAlertsOpts {
flappingSettings: RulesSettingsFlappingProperties;
export interface DetermineDelayedAlertsOpts {
alertDelay: number;
ruleRunMetricsStore: RuleRunMetricsStore;
}
export interface LogAlertsOpts {
shouldLogAlerts: boolean;
ruleRunMetricsStore: RuleRunMetricsStore;

View file

@ -1,44 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { keys, size } from 'lodash';
import { Alert } from '../alert';
import { determineAlertsToReturn } from './determine_alerts_to_return';
describe('determineAlertsToReturn', () => {
const flapping = new Array(16).fill(false).concat([true, true, true, true]);
const notFlapping = new Array(20).fill(false);
describe('determineAlertsToReturn', () => {
test('should return all active alerts regardless of flapping', () => {
const activeAlerts = {
'1': new Alert('1', { meta: { flappingHistory: flapping } }),
'2': new Alert('2', { meta: { flappingHistory: [false, false] } }),
};
const { alertsToReturn } = determineAlertsToReturn(activeAlerts, {});
expect(size(alertsToReturn)).toEqual(2);
});
test('should return all flapping recovered alerts', () => {
const recoveredAlerts = {
'1': new Alert('1', { meta: { flappingHistory: flapping } }),
'2': new Alert('2', { meta: { flappingHistory: notFlapping } }),
};
const { recoveredAlertsToReturn } = determineAlertsToReturn({}, recoveredAlerts);
expect(keys(recoveredAlertsToReturn)).toEqual(['1']);
});
test('should return all recovered alerts if the optimization flag is set to false', () => {
const recoveredAlerts = {
'1': new Alert('1', { meta: { flappingHistory: flapping } }),
'2': new Alert('2', { meta: { flappingHistory: notFlapping } }),
};
const { recoveredAlertsToReturn } = determineAlertsToReturn({}, recoveredAlerts, false);
expect(keys(recoveredAlertsToReturn)).toEqual(['1', '2']);
});
});
});

View file

@ -1,53 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { keys } from 'lodash';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext, RawAlertInstance } from '../types';
// determines which alerts to return in the state
export function determineAlertsToReturn<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>(
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>> = {},
shouldOptimizeTaskState: boolean = true
): {
alertsToReturn: Record<string, RawAlertInstance>;
recoveredAlertsToReturn: Record<string, RawAlertInstance>;
} {
const alertsToReturn: Record<string, RawAlertInstance> = {};
const recoveredAlertsToReturn: Record<string, RawAlertInstance> = {};
// return all active alerts regardless of whether or not the alert is flapping
for (const id of keys(activeAlerts)) {
alertsToReturn[id] = activeAlerts[id].toRaw();
}
for (const id of keys(recoveredAlerts)) {
const alert = recoveredAlerts[id];
if (shouldOptimizeTaskState) {
// return recovered alerts if they are flapping or if the flapping array is not at capacity
// this is a space saving effort that will stop tracking a recovered alert if it wasn't flapping and doesn't have state changes
// in the last max capcity number of executions
const flapping = alert.getFlapping();
const flappingHistory: boolean[] = alert.getFlappingHistory() || [];
const numStateChanges = flappingHistory.filter((f) => f).length;
if (flapping) {
recoveredAlertsToReturn[id] = alert.toRaw(true);
} else if (numStateChanges > 0) {
recoveredAlertsToReturn[id] = alert.toRaw(true);
}
} else {
recoveredAlertsToReturn[id] = alert.toRaw(true);
}
}
return { alertsToReturn, recoveredAlertsToReturn };
}

View file

@ -0,0 +1,250 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { determineDelayedAlerts } from './determine_delayed_alerts';
import { Alert } from '../alert';
import { alertsWithAnyUUID } from '../test_utils';
import { ruleRunMetricsStoreMock } from './rule_run_metrics_store.mock';
describe('determineDelayedAlerts', () => {
const ruleRunMetricsStore = ruleRunMetricsStoreMock.create();
test('should increment activeCount for all active alerts', () => {
const alert1 = new Alert('1', {
meta: { activeCount: 1, uuid: 'uuid-1' },
});
const alert2 = new Alert('2', { meta: { uuid: 'uuid-2' } });
const { newAlerts, activeAlerts, trackedActiveAlerts } = determineDelayedAlerts({
newAlerts: {
'1': alert1,
},
activeAlerts: {
'1': alert1,
'2': alert2,
},
trackedActiveAlerts: {
'1': alert1,
'2': alert2,
},
recoveredAlerts: {},
trackedRecoveredAlerts: {},
alertDelay: 0,
startedAt: null,
ruleRunMetricsStore,
});
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 2,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
expect(trackedActiveAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 2,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 1,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 2,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 1,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(ruleRunMetricsStore.setNumberOfDelayedAlerts).toHaveBeenCalledWith(0);
});
test('should reset activeCount for all recovered alerts', () => {
const alert1 = new Alert('1', { meta: { activeCount: 3 } });
const alert3 = new Alert('3');
const { recoveredAlerts, trackedRecoveredAlerts } = determineDelayedAlerts({
newAlerts: {},
activeAlerts: {},
trackedActiveAlerts: {},
recoveredAlerts: { '1': alert1, '3': alert3 },
trackedRecoveredAlerts: { '1': alert1, '3': alert3 },
alertDelay: 0,
startedAt: null,
ruleRunMetricsStore,
});
expect(alertsWithAnyUUID(trackedRecoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 0,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
"3": Object {
"meta": Object {
"activeCount": 0,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(alertsWithAnyUUID(recoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 0,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
"3": Object {
"meta": Object {
"activeCount": 0,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(ruleRunMetricsStore.setNumberOfDelayedAlerts).toHaveBeenCalledWith(0);
});
test('should remove the alert from newAlerts and should not return the alert in activeAlerts if the activeCount is less than the rule alertDelay', () => {
const alert1 = new Alert('1', {
meta: { activeCount: 1, uuid: 'uuid-1' },
});
const alert2 = new Alert('2', { meta: { uuid: 'uuid-2' } });
const { newAlerts, activeAlerts, trackedActiveAlerts } = determineDelayedAlerts({
newAlerts: { '1': alert1 },
activeAlerts: { '1': alert1, '2': alert2 },
trackedActiveAlerts: { '1': alert1, '2': alert2 },
recoveredAlerts: {},
trackedRecoveredAlerts: {},
alertDelay: 5,
startedAt: null,
ruleRunMetricsStore,
});
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(trackedActiveAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 2,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 1,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
expect(ruleRunMetricsStore.setNumberOfDelayedAlerts).toHaveBeenCalledWith(2);
});
test('should remove the alert from recoveredAlerts and should not return the alert in trackedRecoveredAlerts if the activeCount is less than the rule alertDelay', () => {
const alert1 = new Alert('1', {
meta: { activeCount: 1, uuid: 'uuid-1' },
});
const alert2 = new Alert('2', { meta: { uuid: 'uuid-2' } });
const { recoveredAlerts, trackedRecoveredAlerts } = determineDelayedAlerts({
newAlerts: {},
activeAlerts: {},
trackedActiveAlerts: {},
recoveredAlerts: { '1': alert1, '2': alert2 },
trackedRecoveredAlerts: { '1': alert1, '2': alert2 },
alertDelay: 5,
startedAt: null,
ruleRunMetricsStore,
});
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
expect(trackedRecoveredAlerts).toMatchInlineSnapshot(`Object {}`);
expect(ruleRunMetricsStore.setNumberOfDelayedAlerts).toHaveBeenCalledWith(0);
});
test('should update active alert to look like a new alert if the activeCount is equal to the rule alertDelay', () => {
const alert2 = new Alert('2', { meta: { uuid: 'uuid-2' } });
const { newAlerts, activeAlerts, trackedActiveAlerts } = determineDelayedAlerts({
newAlerts: {},
activeAlerts: { '2': alert2 },
trackedActiveAlerts: { '2': alert2 },
recoveredAlerts: {},
trackedRecoveredAlerts: {},
alertDelay: 1,
startedAt: null,
ruleRunMetricsStore,
});
expect(newAlerts['2'].getState().duration).toBe('0');
expect(newAlerts['2'].getState().start).toBeTruthy();
expect(trackedActiveAlerts['2'].getState().duration).toBe('0');
expect(trackedActiveAlerts['2'].getState().start).toBeTruthy();
expect(activeAlerts['2'].getState().duration).toBe('0');
expect(activeAlerts['2'].getState().start).toBeTruthy();
expect(ruleRunMetricsStore.setNumberOfDelayedAlerts).toHaveBeenCalledWith(0);
});
});

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { keys } from 'lodash';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext } from '../types';
import { RuleRunMetricsStore } from './rule_run_metrics_store';
interface DetermineDelayedAlertsOpts<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
> {
newAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
trackedActiveAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
trackedRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
alertDelay: number;
startedAt?: string | null;
ruleRunMetricsStore: RuleRunMetricsStore;
}
export function determineDelayedAlerts<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>({
newAlerts,
activeAlerts,
trackedActiveAlerts,
recoveredAlerts,
trackedRecoveredAlerts,
alertDelay,
startedAt,
ruleRunMetricsStore,
}: DetermineDelayedAlertsOpts<State, Context, ActionGroupIds, RecoveryActionGroupId>) {
let delayedAlertsCount = 0;
for (const id of keys(activeAlerts)) {
const alert = activeAlerts[id];
alert.incrementActiveCount();
// do not trigger an action notification if the number of consecutive
// active alerts is less than the rule alertDelay threshold
if (alert.getActiveCount() < alertDelay) {
// remove from new alerts and active alerts
delete newAlerts[id];
delete activeAlerts[id];
delayedAlertsCount += 1;
} else {
// if the active count is equal to the alertDelay it is considered a new alert
if (alert.getActiveCount() === alertDelay) {
const currentTime = startedAt ?? new Date().toISOString();
const state = alert.getState();
// keep the state and update the start time and duration
alert.replaceState({ ...state, start: currentTime, duration: '0' });
newAlerts[id] = alert;
}
}
}
for (const id of keys(recoveredAlerts)) {
const alert = recoveredAlerts[id];
// if alert has not reached the alertDelay threshold don't recover the alert
if (alert.getActiveCount() < alertDelay) {
// remove from recovered alerts
delete recoveredAlerts[id];
delete trackedRecoveredAlerts[id];
}
alert.resetActiveCount();
}
ruleRunMetricsStore.setNumberOfDelayedAlerts(delayedAlertsCount);
return {
newAlerts,
activeAlerts,
trackedActiveAlerts,
recoveredAlerts,
trackedRecoveredAlerts,
};
}

View file

@ -0,0 +1,332 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { DEFAULT_FLAPPING_SETTINGS } from '../../../common/rules_settings';
import { Alert } from '../../alert';
import { alertsWithAnyUUID } from '../../test_utils';
import {
delayRecoveredFlappingAlerts,
getEarlyRecoveredAlertIds,
} from './delay_recovered_flapping_alerts';
describe('delayRecoveredFlappingAlerts', () => {
const logger = loggingSystemMock.createLogger();
test('should set pendingRecoveredCount to zero for all active alerts', () => {
const alert1 = new Alert('1', {
meta: { flapping: true, pendingRecoveredCount: 3, uuid: 'uuid-1' },
});
const alert2 = new Alert('2', { meta: { flapping: false, uuid: 'uuid-2' } });
const { newAlerts, activeAlerts, trackedActiveAlerts } = delayRecoveredFlappingAlerts(
logger,
DEFAULT_FLAPPING_SETTINGS,
'default',
1000,
{
// new alerts
'1': alert1,
},
{
// active alerts
'1': alert1,
'2': alert2,
},
{
// tracked active alerts
'1': alert1,
'2': alert2,
},
{}, // recovered alerts
{} // tracked recovered alerts
);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"flapping": false,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(trackedActiveAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"flapping": false,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
});
test('should return flapping pending recovered alerts as active alerts and current active alerts', () => {
const alert1 = new Alert('1', { meta: { flapping: true, pendingRecoveredCount: 3 } });
const alert2 = new Alert('2', { meta: { flapping: false } });
const alert3 = new Alert('3', { meta: { flapping: true } });
const {
newAlerts,
activeAlerts,
trackedActiveAlerts,
recoveredAlerts,
trackedRecoveredAlerts,
} = delayRecoveredFlappingAlerts(
logger,
DEFAULT_FLAPPING_SETTINGS,
'default',
1000,
{}, // new alerts
{}, // active alerts
{}, // tracked active alerts
{
// recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
},
{
// tracked recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
}
);
expect(alertsWithAnyUUID(newAlerts)).toMatchInlineSnapshot(`Object {}`);
expect(alertsWithAnyUUID(trackedActiveAlerts)).toMatchInlineSnapshot(`
Object {
"3": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 1,
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(Object.values(trackedActiveAlerts).map((a) => a.getScheduledActionOptions()))
.toMatchInlineSnapshot(`
Array [
Object {
"actionGroup": "default",
"context": Object {},
"state": Object {},
},
]
`);
expect(alertsWithAnyUUID(activeAlerts)).toMatchInlineSnapshot(`
Object {
"3": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 1,
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(Object.values(activeAlerts).map((a) => a.getScheduledActionOptions()))
.toMatchInlineSnapshot(`
Array [
Object {
"actionGroup": "default",
"context": Object {},
"state": Object {},
},
]
`);
expect(alertsWithAnyUUID(trackedRecoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
"2": Object {
"meta": Object {
"flapping": false,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(alertsWithAnyUUID(recoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
"2": Object {
"meta": Object {
"flapping": false,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
});
describe('getEarlyRecoveredAlertIds', () => {
const alert1 = new Alert('1', { meta: { flappingHistory: [true, true, true, true] } });
const alert2 = new Alert('2', { meta: { flappingHistory: new Array(20).fill(false) } });
const alert3 = new Alert('3', { meta: { flappingHistory: [true, true] } });
test('should remove longest recovered alerts', () => {
const { recoveredAlerts, trackedRecoveredAlerts } = delayRecoveredFlappingAlerts(
logger,
DEFAULT_FLAPPING_SETTINGS,
'default',
2,
{}, // new alerts
{}, // active alerts
{}, // tracked active alerts
{
// recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
},
{
// tracked recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
}
);
expect(Object.keys(recoveredAlerts).length).toBe(3);
expect(recoveredAlerts['2'].getFlapping()).toBe(false);
expect(Object.keys(trackedRecoveredAlerts).length).toBe(2);
});
test('should not remove alerts if the num of recovered alerts is not at the limit', () => {
const { recoveredAlerts, trackedRecoveredAlerts } = delayRecoveredFlappingAlerts(
logger,
DEFAULT_FLAPPING_SETTINGS,
'default',
3,
{}, // new alerts
{}, // active alerts
{}, // tracked active alerts
{
// recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
},
{
// tracked recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
}
);
expect(Object.keys(recoveredAlerts).length).toBe(3);
expect(recoveredAlerts['2'].getFlapping()).toBe(false);
expect(Object.keys(trackedRecoveredAlerts).length).toBe(3);
});
test('getEarlyRecoveredAlertIds should return longest recovered alerts', () => {
const alertIds = getEarlyRecoveredAlertIds(
logger,
{
// tracked recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
},
2
);
expect(alertIds).toEqual(['2']);
expect(logger.warn).toBeCalledWith(
'Recovered alerts have exceeded the max alert limit of 2 : dropping 1 alert.'
);
});
test('getEarlyRecoveredAlertIds should not return alerts if the num of recovered alerts is not at the limit', () => {
const trimmedAlerts = getEarlyRecoveredAlertIds(
logger,
{
// tracked recovered alerts
'1': alert1,
'2': alert2,
},
2
);
expect(trimmedAlerts).toEqual([]);
});
});
});

View file

@ -0,0 +1,121 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { keys, map } from 'lodash';
import { Logger } from '@kbn/logging';
import { RulesSettingsFlappingProperties } from '../../../common/rules_settings';
import { Alert } from '../../alert';
import { AlertInstanceState, AlertInstanceContext } from '../../types';
export function delayRecoveredFlappingAlerts<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>(
logger: Logger,
flappingSettings: RulesSettingsFlappingProperties,
actionGroupId: string,
maxAlerts: number,
newAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
trackedActiveAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>> = {},
trackedRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>> = {}
) {
for (const id of keys(activeAlerts)) {
const alert = activeAlerts[id];
alert.resetPendingRecoveredCount();
}
for (const id of keys(recoveredAlerts)) {
const alert = recoveredAlerts[id];
const flapping = alert.getFlapping();
if (flapping) {
alert.incrementPendingRecoveredCount();
if (alert.getPendingRecoveredCount() < flappingSettings.statusChangeThreshold) {
// keep the context and previous actionGroupId if available
const context = alert.getContext();
const lastActionGroupId = alert.getLastScheduledActions()?.group;
const newAlert = new Alert<State, Context, ActionGroupIds>(id, alert.toRaw());
// unset the end time in the alert state
const state = newAlert.getState();
delete state.end;
newAlert.replaceState(state);
// schedule actions for the new active alert
newAlert.scheduleActions(
(lastActionGroupId ? lastActionGroupId : actionGroupId) as ActionGroupIds,
context
);
activeAlerts[id] = newAlert;
trackedActiveAlerts[id] = newAlert;
// remove from recovered alerts
delete recoveredAlerts[id];
delete trackedRecoveredAlerts[id];
} else {
alert.resetPendingRecoveredCount();
}
}
}
const earlyRecoveredAlertIds = getEarlyRecoveredAlertIds(
logger,
trackedRecoveredAlerts,
maxAlerts
);
for (const id of earlyRecoveredAlertIds) {
const alert = trackedRecoveredAlerts[id];
alert.setFlapping(false);
recoveredAlerts[id] = alert;
delete trackedRecoveredAlerts[id];
}
return {
newAlerts,
activeAlerts,
trackedActiveAlerts,
recoveredAlerts,
trackedRecoveredAlerts,
};
}
export function getEarlyRecoveredAlertIds<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
RecoveryActionGroupId extends string
>(
logger: Logger,
trackedRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>>,
maxAlerts: number
) {
const alerts = map(trackedRecoveredAlerts, (alert, id) => {
return {
id,
flappingHistory: alert.getFlappingHistory() || [],
};
});
let earlyRecoveredAlertIds: string[] = [];
if (alerts.length > maxAlerts) {
alerts.sort((a, b) => {
return a.flappingHistory.length - b.flappingHistory.length;
});
earlyRecoveredAlertIds = alerts.slice(maxAlerts).map((alert) => alert.id);
logger.warn(
`Recovered alerts have exceeded the max alert limit of ${maxAlerts} : dropping ${
earlyRecoveredAlertIds.length
} ${earlyRecoveredAlertIds.length > 1 ? 'alerts' : 'alert'}.`
);
}
return earlyRecoveredAlertIds;
}

View file

@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Logger } from '@kbn/logging';
import { Alert } from '../../alert';
import { AlertInstanceState, AlertInstanceContext } from '../../types';
import { RulesSettingsFlappingProperties } from '../../../common/rules_settings';
import { setFlapping } from './set_flapping';
import { setFlappingHistoryAndTrackedAlerts } from './set_flapping_history_and_tracked_alerts';
import { delayRecoveredFlappingAlerts } from './delay_recovered_flapping_alerts';
interface DetermineFlappingAlertsOpts<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
> {
logger: Logger;
newAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
flappingSettings: RulesSettingsFlappingProperties;
previouslyRecoveredAlerts: Record<string, Alert<State, Context>>;
actionGroupId: string;
maxAlerts: number;
}
export function determineFlappingAlerts<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>({
logger,
newAlerts,
activeAlerts,
recoveredAlerts,
flappingSettings,
previouslyRecoveredAlerts,
actionGroupId,
maxAlerts,
}: DetermineFlappingAlertsOpts<State, Context, ActionGroupIds, RecoveryActionGroupId>) {
setFlapping<State, Context, ActionGroupIds, RecoveryActionGroupId>(
flappingSettings,
activeAlerts,
recoveredAlerts
);
let alerts = setFlappingHistoryAndTrackedAlerts<
State,
Context,
ActionGroupIds,
RecoveryActionGroupId
>(flappingSettings, newAlerts, activeAlerts, recoveredAlerts, previouslyRecoveredAlerts);
alerts = delayRecoveredFlappingAlerts<State, Context, ActionGroupIds, RecoveryActionGroupId>(
logger,
flappingSettings,
actionGroupId,
maxAlerts,
alerts.newAlerts,
alerts.activeAlerts,
alerts.trackedActiveAlerts,
alerts.recoveredAlerts,
alerts.trackedRecoveredAlerts
);
return alerts;
}

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings';
import {
DEFAULT_FLAPPING_SETTINGS,
DISABLE_FLAPPING_SETTINGS,
} from '../../../common/rules_settings';
import { atCapacity, updateFlappingHistory, isFlapping } from './flapping_utils';
describe('flapping utils', () => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { RulesSettingsFlappingProperties } from '../../common/rules_settings';
import { RulesSettingsFlappingProperties } from '../../../common/rules_settings';
export function updateFlappingHistory(
flappingSettings: RulesSettingsFlappingProperties,

View file

@ -6,10 +6,13 @@
*/
import { pick } from 'lodash';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common';
import { Alert } from '../../alert';
import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../../common';
import { setFlapping, isAlertFlapping } from './set_flapping';
import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings';
import {
DEFAULT_FLAPPING_SETTINGS,
DISABLE_FLAPPING_SETTINGS,
} from '../../../common/rules_settings';
describe('setFlapping', () => {
const flapping = new Array(16).fill(false).concat([true, true, true, true]);

View file

@ -6,10 +6,10 @@
*/
import { keys } from 'lodash';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext } from '../types';
import { Alert } from '../../alert';
import { AlertInstanceState, AlertInstanceContext } from '../../types';
import { isFlapping } from './flapping_utils';
import { RulesSettingsFlappingProperties } from '../../common/rules_settings';
import { RulesSettingsFlappingProperties } from '../../../common/rules_settings';
export function setFlapping<
State extends AlertInstanceState,

View file

@ -0,0 +1,481 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import sinon from 'sinon';
import { cloneDeep } from 'lodash';
import {
updateAlertFlappingHistory,
setFlappingHistoryAndTrackedAlerts,
} from './set_flapping_history_and_tracked_alerts';
import { Alert } from '../../alert';
import { AlertInstanceState, AlertInstanceContext } from '../../types';
import {
DEFAULT_FLAPPING_SETTINGS,
DISABLE_FLAPPING_SETTINGS,
} from '../../../common/rules_settings';
describe('setFlappingHistoryAndTrackedAlerts', () => {
let clock: sinon.SinonFakeTimers;
beforeAll(() => {
clock = sinon.useFakeTimers();
});
beforeEach(() => {
clock.reset();
});
afterAll(() => clock.restore());
test('if new alert, set flapping state to true', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { uuid: 'uuid-1' },
state: {
duration: '0',
start: '1970-01-01T00:00:00.000Z',
},
});
const alerts = cloneDeep({ '1': activeAlert });
alerts['1'].scheduleActions('default' as never, { foo: '1' });
const { activeAlerts, newAlerts, trackedActiveAlerts, recoveredAlerts } =
setFlappingHistoryAndTrackedAlerts(
DEFAULT_FLAPPING_SETTINGS,
alerts, // new alerts
alerts, // active alerts
{}, // recovered alerts
{} // previously recovered alerts
);
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(trackedActiveAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
test('if alert is still active, set flapping state to false', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
const alerts = cloneDeep({ '1': activeAlert });
alerts['1'].scheduleActions('default' as never, { foo: '1' });
const { activeAlerts, newAlerts, trackedActiveAlerts, recoveredAlerts } =
setFlappingHistoryAndTrackedAlerts(
DEFAULT_FLAPPING_SETTINGS,
{}, // new alerts
alerts, // active alerts
{}, // recovered alerts
{} // previously recovered alerts
);
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
expect(trackedActiveAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
test('if alert is active and previously recovered, set flapping state to true', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { uuid: 'uuid-1' },
state: {
duration: '0',
start: '1970-01-01T00:00:00.000Z',
},
});
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-2' },
});
const alerts = cloneDeep({ '1': activeAlert });
alerts['1'].scheduleActions('default' as never, { foo: '1' });
alerts['1'].setFlappingHistory([false]);
const { activeAlerts, newAlerts, trackedActiveAlerts, recoveredAlerts } =
setFlappingHistoryAndTrackedAlerts(
DEFAULT_FLAPPING_SETTINGS,
alerts, // new alerts
alerts, // active alerts
{}, // recovered alerts
{ '1': recoveredAlert } // previously recovered alerts
);
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(trackedActiveAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
test('if alert is recovered and previously active, set flapping state to true', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
activeAlert.scheduleActions('default' as never, { foo: '1' });
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
const alerts = cloneDeep({ '1': recoveredAlert });
const { activeAlerts, newAlerts, recoveredAlerts, trackedRecoveredAlerts } =
setFlappingHistoryAndTrackedAlerts(
DEFAULT_FLAPPING_SETTINGS,
{}, // new alerts
{}, // active alerts
alerts, // recovered alerts
{} // previously recovered alerts
);
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(recoveredAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
expect(trackedRecoveredAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
});
test('if alert is still recovered, set flapping state to false', () => {
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
const alerts = cloneDeep({ '1': recoveredAlert });
const { activeAlerts, newAlerts, recoveredAlerts, trackedRecoveredAlerts } =
setFlappingHistoryAndTrackedAlerts(
DEFAULT_FLAPPING_SETTINGS,
{}, // new alerts
{}, // active alerts
{}, // recovered alerts
alerts // previously recovered alerts
);
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
expect(trackedRecoveredAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
});
test('if setFlapping is false should not update flappingHistory', () => {
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { uuid: 'uuid-1' },
state: {
duration: '0',
start: '1970-01-01T00:00:00.000Z',
},
});
activeAlert1.scheduleActions('default' as never, { foo: '1' });
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('2', {
meta: { flappingHistory: [false], uuid: 'uuid-2' },
});
activeAlert2.scheduleActions('default' as never, { foo: '1' });
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('3', {
meta: { flappingHistory: [false], uuid: 'uuid-3' },
});
const previouslyRecoveredAlerts = cloneDeep({ '3': recoveredAlert });
const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 });
const {
newAlerts,
activeAlerts,
trackedActiveAlerts,
recoveredAlerts,
trackedRecoveredAlerts,
} = setFlappingHistoryAndTrackedAlerts(
DISABLE_FLAPPING_SETTINGS,
cloneDeep({ '1': activeAlert1 }), // new alerts
alerts, // active alerts
{}, // recovered alerts
previouslyRecoveredAlerts
);
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
"2": Object {
"meta": Object {
"flappingHistory": Array [
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(trackedActiveAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
"2": Object {
"meta": Object {
"flappingHistory": Array [
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
expect(trackedRecoveredAlerts).toMatchInlineSnapshot(`
Object {
"3": Object {
"meta": Object {
"flappingHistory": Array [
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-3",
},
"state": Object {},
},
}
`);
});
describe('updateAlertFlappingHistory', () => {
test('correctly updates flappingHistory', () => {
const alert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false, false] },
});
updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true);
expect(alert.getFlappingHistory()).toEqual([false, false, true]);
});
test('correctly updates flappingHistory while maintaining a fixed size', () => {
const flappingHistory = new Array(20).fill(false);
const alert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory },
});
updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true);
const fh = alert.getFlappingHistory() || [];
expect(fh.length).toEqual(20);
const result = new Array(19).fill(false);
expect(fh).toEqual(result.concat(true));
});
test('correctly updates flappingHistory while maintaining if array is larger than fixed size', () => {
const flappingHistory = new Array(23).fill(false);
const alert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory },
});
updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true);
const fh = alert.getFlappingHistory() || [];
expect(fh.length).toEqual(20);
const result = new Array(19).fill(false);
expect(fh).toEqual(result.concat(true));
});
});
});

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { keys } from 'lodash';
import { Alert } from '../../alert';
import { AlertInstanceState, AlertInstanceContext } from '../../types';
import { updateFlappingHistory } from './flapping_utils';
import { RulesSettingsFlappingProperties } from '../../../common/rules_settings';
export function setFlappingHistoryAndTrackedAlerts<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupIds extends string
>(
flappingSettings: RulesSettingsFlappingProperties,
newAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupIds>> = {},
previouslyRecoveredAlerts: Record<string, Alert<State, Context>> = {}
): {
newAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
trackedActiveAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupIds>>;
trackedRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupIds>>;
} {
const trackedActiveAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {};
const trackedRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupIds>> = {};
const previouslyRecoveredAlertIds = new Set(Object.keys(previouslyRecoveredAlerts));
for (const id of keys(activeAlerts)) {
const alert = activeAlerts[id];
trackedActiveAlerts[id] = alert;
// this alert was not active in the previous run
if (newAlerts[id]) {
if (previouslyRecoveredAlertIds.has(id)) {
// this alert has flapped from recovered to active
const previousFlappingHistory = previouslyRecoveredAlerts[id].getFlappingHistory();
alert.setFlappingHistory(previousFlappingHistory);
previouslyRecoveredAlertIds.delete(id);
}
updateAlertFlappingHistory(flappingSettings, newAlerts[id], true);
} else {
// this alert is still active
updateAlertFlappingHistory(flappingSettings, alert, false);
}
}
for (const id of keys(recoveredAlerts)) {
const alert = recoveredAlerts[id];
trackedRecoveredAlerts[id] = alert;
// this alert has flapped from active to recovered
updateAlertFlappingHistory(flappingSettings, alert, true);
}
for (const id of previouslyRecoveredAlertIds) {
const alert = previouslyRecoveredAlerts[id];
trackedRecoveredAlerts[id] = alert;
// this alert is still recovered
updateAlertFlappingHistory(flappingSettings, alert, false);
}
return { newAlerts, activeAlerts, trackedActiveAlerts, recoveredAlerts, trackedRecoveredAlerts };
}
export function updateAlertFlappingHistory<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>(
flappingSettings: RulesSettingsFlappingProperties,
alert: Alert<State, Context, ActionGroupIds | RecoveryActionGroupId>,
state: boolean
) {
const updatedFlappingHistory = updateFlappingHistory(
flappingSettings,
alert.getFlappingHistory() || [],
state
);
alert.setFlappingHistory(updatedFlappingHistory);
}

View file

@ -1,613 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings';
import { getAlertsForNotification } from '.';
import { Alert } from '../alert';
import { alertsWithAnyUUID } from '../test_utils';
describe('getAlertsForNotification', () => {
test('should set pendingRecoveredCount to zero for all active alerts', () => {
const alert1 = new Alert('1', {
meta: { flapping: true, pendingRecoveredCount: 3, uuid: 'uuid-1' },
});
const alert2 = new Alert('2', { meta: { flapping: false, uuid: 'uuid-2' } });
const { newAlerts, activeAlerts } = getAlertsForNotification(
DEFAULT_FLAPPING_SETTINGS,
'default',
0,
{
// new alerts
'1': alert1,
},
{
// active alerts
'1': alert1,
'2': alert2,
},
{},
{}
);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 1,
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 1,
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 1,
"flapping": false,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
});
test('should return flapping pending recovered alerts as active alerts and current active alerts', () => {
const alert1 = new Alert('1', { meta: { flapping: true, pendingRecoveredCount: 3 } });
const alert2 = new Alert('2', { meta: { flapping: false } });
const alert3 = new Alert('3', { meta: { flapping: true } });
const {
newAlerts,
activeAlerts,
currentActiveAlerts,
recoveredAlerts,
currentRecoveredAlerts,
} = getAlertsForNotification(
DEFAULT_FLAPPING_SETTINGS,
'default',
0,
{},
{},
{
// recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
},
{
// current recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
}
);
expect(alertsWithAnyUUID(newAlerts)).toMatchInlineSnapshot(`Object {}`);
expect(alertsWithAnyUUID(activeAlerts)).toMatchInlineSnapshot(`
Object {
"3": Object {
"meta": Object {
"activeCount": 0,
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 1,
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(Object.values(activeAlerts).map((a) => a.getScheduledActionOptions()))
.toMatchInlineSnapshot(`
Array [
Object {
"actionGroup": "default",
"context": Object {},
"state": Object {},
},
]
`);
expect(alertsWithAnyUUID(currentActiveAlerts)).toMatchInlineSnapshot(`
Object {
"3": Object {
"meta": Object {
"activeCount": 0,
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 1,
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(Object.values(currentActiveAlerts).map((a) => a.getScheduledActionOptions()))
.toMatchInlineSnapshot(`
Array [
Object {
"actionGroup": "default",
"context": Object {},
"state": Object {},
},
]
`);
expect(alertsWithAnyUUID(recoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 0,
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 0,
"flapping": false,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(alertsWithAnyUUID(currentRecoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 0,
"flapping": true,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 0,
"flapping": false,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
});
test('should reset counts and not modify alerts if flapping is disabled', () => {
const alert1 = new Alert('1', {
meta: { flapping: true, flappingHistory: [true, false, true], pendingRecoveredCount: 3 },
});
const alert2 = new Alert('2', {
meta: { flapping: false, flappingHistory: [true, false, true] },
});
const alert3 = new Alert('3', {
meta: { flapping: true, flappingHistory: [true, false, true] },
});
const { newAlerts, activeAlerts, recoveredAlerts, currentRecoveredAlerts } =
getAlertsForNotification(
DISABLE_FLAPPING_SETTINGS,
'default',
0,
{},
{},
{
// recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
},
{
// current recovered alerts
'1': alert1,
'2': alert2,
'3': alert3,
}
);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
expect(alertsWithAnyUUID(recoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 0,
"flapping": true,
"flappingHistory": Array [
true,
false,
true,
],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 0,
"flapping": false,
"flappingHistory": Array [
true,
false,
true,
],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
"3": Object {
"meta": Object {
"activeCount": 0,
"flapping": true,
"flappingHistory": Array [
true,
false,
true,
],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(alertsWithAnyUUID(currentRecoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 0,
"flapping": true,
"flappingHistory": Array [
true,
false,
true,
],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 0,
"flapping": false,
"flappingHistory": Array [
true,
false,
true,
],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
"3": Object {
"meta": Object {
"activeCount": 0,
"flapping": true,
"flappingHistory": Array [
true,
false,
true,
],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
});
test('should increment activeCount for all active alerts', () => {
const alert1 = new Alert('1', {
meta: { activeCount: 1, uuid: 'uuid-1' },
});
const alert2 = new Alert('2', { meta: { uuid: 'uuid-2' } });
const { newAlerts, activeAlerts, currentActiveAlerts, delayedAlertsCount } =
getAlertsForNotification(
DEFAULT_FLAPPING_SETTINGS,
'default',
0,
{
// new alerts
'1': alert1,
},
{
// active alerts
'1': alert1,
'2': alert2,
},
{},
{}
);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 2,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 2,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 1,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(currentActiveAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 2,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 1,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(delayedAlertsCount).toBe(0);
});
test('should reset activeCount for all recovered alerts', () => {
const alert1 = new Alert('1', { meta: { activeCount: 3 } });
const alert3 = new Alert('3');
const { recoveredAlerts, currentRecoveredAlerts, delayedAlertsCount } =
getAlertsForNotification(
DEFAULT_FLAPPING_SETTINGS,
'default',
0,
{},
{},
{
// recovered alerts
'1': alert1,
'3': alert3,
},
{
// current recovered alerts
'1': alert1,
'3': alert3,
}
);
expect(alertsWithAnyUUID(recoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 0,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
"3": Object {
"meta": Object {
"activeCount": 0,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(alertsWithAnyUUID(currentRecoveredAlerts)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 0,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
"3": Object {
"meta": Object {
"activeCount": 0,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": Any<String>,
},
"state": Object {},
},
}
`);
expect(delayedAlertsCount).toBe(0);
});
test('should remove the alert from newAlerts and should not return the alert in currentActiveAlerts if the activeCount is less than the rule alertDelay', () => {
const alert1 = new Alert('1', {
meta: { activeCount: 1, uuid: 'uuid-1' },
});
const alert2 = new Alert('2', { meta: { uuid: 'uuid-2' } });
const { newAlerts, activeAlerts, currentActiveAlerts, delayedAlertsCount } =
getAlertsForNotification(
DEFAULT_FLAPPING_SETTINGS,
'default',
5,
{
// new alerts
'1': alert1,
},
{
// active alerts
'1': alert1,
'2': alert2,
},
{},
{}
);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"activeCount": 2,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"activeCount": 1,
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"pendingRecoveredCount": 0,
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(currentActiveAlerts).toMatchInlineSnapshot(`Object {}`);
expect(delayedAlertsCount).toBe(2);
});
test('should remove the alert from recoveredAlerts and should not return the alert in currentRecoveredAlerts if the activeCount is less than the rule alertDelay', () => {
const alert1 = new Alert('1', {
meta: { activeCount: 1, uuid: 'uuid-1' },
});
const alert2 = new Alert('2', { meta: { uuid: 'uuid-2' } });
const { recoveredAlerts, currentRecoveredAlerts, delayedAlertsCount } =
getAlertsForNotification(
DEFAULT_FLAPPING_SETTINGS,
'default',
5,
{},
{},
{
// recovered alerts
'1': alert1,
'2': alert2,
},
{
// current recovered alerts
'1': alert1,
'2': alert2,
}
);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
expect(currentRecoveredAlerts).toMatchInlineSnapshot(`Object {}`);
expect(delayedAlertsCount).toBe(0);
});
test('should update active alert to look like a new alert if the activeCount is equal to the rule alertDelay', () => {
const alert2 = new Alert('2', { meta: { uuid: 'uuid-2' } });
const { newAlerts, activeAlerts, currentActiveAlerts, delayedAlertsCount } =
getAlertsForNotification(
DEFAULT_FLAPPING_SETTINGS,
'default',
1,
{},
{
// active alerts
'2': alert2,
},
{},
{}
);
expect(newAlerts['2'].getState().duration).toBe('0');
expect(newAlerts['2'].getState().start).toBeTruthy();
expect(activeAlerts['2'].getState().duration).toBe('0');
expect(activeAlerts['2'].getState().start).toBeTruthy();
expect(currentActiveAlerts['2'].getState().duration).toBe('0');
expect(currentActiveAlerts['2'].getState().start).toBeTruthy();
expect(delayedAlertsCount).toBe(0);
});
});

View file

@ -1,107 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { keys } from 'lodash';
import { RulesSettingsFlappingProperties } from '../../common/rules_settings';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext } from '../types';
export function getAlertsForNotification<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>(
flappingSettings: RulesSettingsFlappingProperties,
actionGroupId: string,
alertDelay: number,
newAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>> = {},
currentRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>> = {},
startedAt?: string | null
) {
const currentActiveAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {};
let delayedAlertsCount = 0;
for (const id of keys(activeAlerts)) {
const alert = activeAlerts[id];
alert.incrementActiveCount();
alert.resetPendingRecoveredCount();
// do not trigger an action notification if the number of consecutive
// active alerts is less than the rule alertDelay threshold
if (alert.getActiveCount() < alertDelay) {
// remove from new alerts
delete newAlerts[id];
delayedAlertsCount += 1;
} else {
currentActiveAlerts[id] = alert;
// if the active count is equal to the alertDelay it is considered a new alert
if (alert.getActiveCount() === alertDelay) {
const currentTime = startedAt ?? new Date().toISOString();
const state = alert.getState();
// keep the state and update the start time and duration
alert.replaceState({ ...state, start: currentTime, duration: '0' });
newAlerts[id] = alert;
}
}
}
for (const id of keys(currentRecoveredAlerts)) {
const alert = recoveredAlerts[id];
// if alert has not reached the alertDelay threshold don't recover the alert
if (alert.getActiveCount() < alertDelay) {
// remove from recovered alerts
delete recoveredAlerts[id];
delete currentRecoveredAlerts[id];
}
alert.resetActiveCount();
if (flappingSettings.enabled) {
const flapping = alert.getFlapping();
if (flapping) {
alert.incrementPendingRecoveredCount();
if (alert.getPendingRecoveredCount() < flappingSettings.statusChangeThreshold) {
// keep the context and previous actionGroupId if available
const context = alert.getContext();
const lastActionGroupId = alert.getLastScheduledActions()?.group;
const newAlert = new Alert<State, Context, ActionGroupIds>(id, alert.toRaw());
// unset the end time in the alert state
const state = newAlert.getState();
delete state.end;
newAlert.replaceState(state);
// schedule actions for the new active alert
newAlert.scheduleActions(
(lastActionGroupId ? lastActionGroupId : actionGroupId) as ActionGroupIds,
context
);
activeAlerts[id] = newAlert;
currentActiveAlerts[id] = newAlert;
// remove from recovered alerts
delete recoveredAlerts[id];
delete currentRecoveredAlerts[id];
} else {
alert.resetPendingRecoveredCount();
}
}
} else {
alert.resetPendingRecoveredCount();
}
}
return {
newAlerts,
activeAlerts,
currentActiveAlerts,
recoveredAlerts,
currentRecoveredAlerts,
delayedAlertsCount,
};
}

View file

@ -41,11 +41,7 @@ export { isRuleSnoozed, getRuleSnoozeEndTime } from './is_rule_snoozed';
export { convertRuleIdsToKueryNode } from './convert_rule_ids_to_kuery_node';
export { convertEsSortToEventLogSort } from './convert_es_sort_to_event_log_sort';
export * from './snooze';
export { setFlapping } from './set_flapping';
export { determineAlertsToReturn } from './determine_alerts_to_return';
export { updateFlappingHistory, isFlapping } from './flapping_utils';
export { getAlertsForNotification } from './get_alerts_for_notification';
export { trimRecoveredAlerts } from './trim_recovered_alerts';
export { toRawAlertInstances } from './to_raw_alert_instances';
export { createGetAlertIndicesAliasFn } from './create_get_alert_indices_alias';
export type { GetAlertIndicesAlias } from './create_get_alert_indices_alias';
export { getEsRequestTimeout } from './get_es_request_timeout';

View file

@ -7,10 +7,9 @@
import sinon from 'sinon';
import { cloneDeep } from 'lodash';
import { processAlerts, updateAlertFlappingHistory } from './process_alerts';
import { processAlerts } from './process_alerts';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext } from '../types';
import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings';
describe('processAlerts', () => {
let clock: sinon.SinonFakeTimers;
@ -30,17 +29,12 @@ describe('processAlerts', () => {
const newAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1');
const existingAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('2');
const existingAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('3', {});
const existingRecoveredAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('4');
const existingAlerts = {
'2': existingAlert1,
'3': existingAlert2,
};
const previouslyRecoveredAlerts = {
'4': existingRecoveredAlert1,
};
const updatedAlerts = {
...cloneDeep(existingAlerts),
'1': newAlert,
@ -53,11 +47,9 @@ describe('processAlerts', () => {
const { newAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts,
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(newAlerts).toEqual({ '1': newAlert });
@ -91,11 +83,9 @@ describe('processAlerts', () => {
const { newAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(newAlerts).toEqual({ '1': newAlert1, '2': newAlert2 });
@ -141,11 +131,9 @@ describe('processAlerts', () => {
const { newAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
startedAt: '2023-10-03T20:03:08.716Z',
});
@ -188,11 +176,10 @@ describe('processAlerts', () => {
const { activeAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -226,11 +213,10 @@ describe('processAlerts', () => {
const { activeAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -274,11 +260,9 @@ describe('processAlerts', () => {
const { activeAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -332,11 +316,9 @@ describe('processAlerts', () => {
const { activeAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -397,11 +379,9 @@ describe('processAlerts', () => {
const { activeAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -458,17 +438,11 @@ describe('processAlerts', () => {
const { activeAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts,
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(
Object.keys(activeAlerts).map((id) => ({ [id]: activeAlerts[id].getFlappingHistory() }))
).toEqual([{ '1': [true] }, { '2': [true] }, { '3': [false] }, { '4': [false] }]);
const previouslyRecoveredAlert1State = activeAlerts['1'].getState();
const previouslyRecoveredAlert2State = activeAlerts['2'].getState();
@ -514,18 +488,12 @@ describe('processAlerts', () => {
const { activeAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts,
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
startedAt: '2023-10-03T20:03:08.716Z',
});
expect(
Object.keys(activeAlerts).map((id) => ({ [id]: activeAlerts[id].getFlappingHistory() }))
).toEqual([{ '1': [true] }, { '2': [true] }, { '3': [false] }, { '4': [false] }]);
const previouslyRecoveredAlert1State = activeAlerts['1'].getState();
const previouslyRecoveredAlert2State = activeAlerts['2'].getState();
@ -558,11 +526,9 @@ describe('processAlerts', () => {
const { recoveredAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'] });
@ -586,11 +552,9 @@ describe('processAlerts', () => {
const { recoveredAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({});
@ -616,11 +580,9 @@ describe('processAlerts', () => {
const { recoveredAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] });
@ -658,11 +620,10 @@ describe('processAlerts', () => {
const { recoveredAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
startedAt: '2023-10-03T20:03:08.716Z',
});
@ -698,11 +659,9 @@ describe('processAlerts', () => {
const { recoveredAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] });
@ -720,33 +679,6 @@ describe('processAlerts', () => {
expect(recoveredAlert2State.end).not.toBeDefined();
});
test('considers alert recovered if it was previously recovered and not active', () => {
const recoveredAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1');
const recoveredAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('2');
const previouslyRecoveredAlerts = {
'1': recoveredAlert1,
'2': recoveredAlert2,
};
const updatedAlerts = cloneDeep(previouslyRecoveredAlerts);
updatedAlerts['1'].setFlappingHistory([false]);
updatedAlerts['2'].setFlappingHistory([false]);
const { recoveredAlerts } = processAlerts({
alerts: {},
existingAlerts: {},
previouslyRecoveredAlerts,
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual(updatedAlerts);
});
test('should skip recovery calculations if autoRecoverAlerts = false', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1');
const recoveredAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('2');
@ -767,11 +699,9 @@ describe('processAlerts', () => {
const { recoveredAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({});
@ -813,11 +743,9 @@ describe('processAlerts', () => {
const { recoveredAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: true,
alertLimit: 7,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({});
@ -849,11 +777,9 @@ describe('processAlerts', () => {
const { activeAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: true,
alertLimit: 7,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -909,11 +835,9 @@ describe('processAlerts', () => {
const { activeAlerts, newAlerts } = processAlerts({
alerts: updatedAlerts,
existingAlerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: true,
alertLimit: MAX_ALERTS,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(Object.keys(activeAlerts).length).toEqual(MAX_ALERTS);
@ -932,615 +856,4 @@ describe('processAlerts', () => {
});
});
});
describe('updating flappingHistory', () => {
test('if new alert, set flapping state to true', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { uuid: 'uuid-1' },
});
const alerts = cloneDeep({ '1': activeAlert });
alerts['1'].scheduleActions('default' as never, { foo: '1' });
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts,
existingAlerts: {},
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
test('if alert is still active, set flapping state to false', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
const alerts = cloneDeep({ '1': activeAlert });
alerts['1'].scheduleActions('default' as never, { foo: '1' });
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts,
existingAlerts: alerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
test('if alert is active and previously recovered, set flapping state to true', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { uuid: 'uuid-1' },
});
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-2' },
});
const alerts = cloneDeep({ '1': activeAlert });
alerts['1'].scheduleActions('default' as never, { foo: '1' });
alerts['1'].setFlappingHistory([false]);
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts,
existingAlerts: {},
previouslyRecoveredAlerts: { '1': recoveredAlert },
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
test('if alert is recovered and previously active, set flapping state to true', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
activeAlert.scheduleActions('default' as never, { foo: '1' });
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
const alerts = cloneDeep({ '1': recoveredAlert });
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts,
existingAlerts: { '1': activeAlert },
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(recoveredAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
});
test('if alert is still recovered, set flapping state to false', () => {
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
const alerts = cloneDeep({ '1': recoveredAlert });
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts: {},
existingAlerts: {},
previouslyRecoveredAlerts: alerts,
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(recoveredAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
});
test('if setFlapping is false should not update flappingHistory', () => {
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { uuid: 'uuid-1' },
});
activeAlert1.scheduleActions('default' as never, { foo: '1' });
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('2', {
meta: { flappingHistory: [false], uuid: 'uuid-2' },
});
activeAlert2.scheduleActions('default' as never, { foo: '1' });
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('3', {
meta: { flappingHistory: [false], uuid: 'uuid-3' },
});
const previouslyRecoveredAlerts = cloneDeep({ '3': recoveredAlert });
const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 });
const existingAlerts = cloneDeep({ '2': activeAlert2 });
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts,
existingAlerts,
previouslyRecoveredAlerts,
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
"2": Object {
"meta": Object {
"flappingHistory": Array [
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(recoveredAlerts).toMatchInlineSnapshot(`
Object {
"3": Object {
"meta": Object {
"flappingHistory": Array [
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-3",
},
"state": Object {},
},
}
`);
});
describe('when hasReachedAlertLimit is true', () => {
test('if alert is still active, set flapping state to false', () => {
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
const alerts = cloneDeep({ '1': activeAlert });
alerts['1'].scheduleActions('default' as never, { foo: '1' });
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts,
existingAlerts: alerts,
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: true,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
test('if new alert, set flapping state to true', () => {
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
activeAlert1.scheduleActions('default' as never, { foo: '1' });
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('2', {
meta: { flappingHistory: [false], uuid: 'uuid-2' },
});
activeAlert2.scheduleActions('default' as never, { foo: '1' });
const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 });
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts,
existingAlerts: { '1': activeAlert1 },
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: true,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"2": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
test('if alert is active and previously recovered, set flapping state to true', () => {
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
activeAlert1.scheduleActions('default' as never, { foo: '1' });
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { uuid: 'uuid-2' },
});
activeAlert2.scheduleActions('default' as never, { foo: '1' });
const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 });
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts,
existingAlerts: {},
previouslyRecoveredAlerts: { '1': activeAlert1 },
hasReachedAlertLimit: true,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
"2": Object {
"meta": Object {
"flappingHistory": Array [
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
"2": Object {
"meta": Object {
"flappingHistory": Array [
true,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
test('if setFlapping is false should not update flappingHistory', () => {
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false], uuid: 'uuid-1' },
});
activeAlert1.scheduleActions('default' as never, { foo: '1' });
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { uuid: 'uuid-2' },
});
activeAlert2.scheduleActions('default' as never, { foo: '1' });
const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 });
const { activeAlerts, newAlerts, recoveredAlerts } = processAlerts({
alerts,
existingAlerts: { '1': activeAlert1 },
previouslyRecoveredAlerts: {},
hasReachedAlertLimit: true,
alertLimit: 10,
autoRecoverAlerts: true,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flappingHistory": Array [
false,
],
"maintenanceWindowIds": Array [],
"uuid": "uuid-1",
},
"state": Object {},
},
"2": Object {
"meta": Object {
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(newAlerts).toMatchInlineSnapshot(`
Object {
"2": Object {
"meta": Object {
"flappingHistory": Array [],
"maintenanceWindowIds": Array [],
"uuid": "uuid-2",
},
"state": Object {
"duration": "0",
"start": "1970-01-01T00:00:00.000Z",
},
},
}
`);
expect(recoveredAlerts).toMatchInlineSnapshot(`Object {}`);
});
});
});
describe('updateAlertFlappingHistory function', () => {
test('correctly updates flappingHistory', () => {
const alert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false, false] },
});
updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true);
expect(alert.getFlappingHistory()).toEqual([false, false, true]);
});
test('correctly updates flappingHistory while maintaining a fixed size', () => {
const flappingHistory = new Array(20).fill(false);
const alert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory },
});
updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true);
const fh = alert.getFlappingHistory() || [];
expect(fh.length).toEqual(20);
const result = new Array(19).fill(false);
expect(fh).toEqual(result.concat(true));
});
test('correctly updates flappingHistory while maintaining if array is larger than fixed size', () => {
const flappingHistory = new Array(23).fill(false);
const alert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory },
});
updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true);
const fh = alert.getFlappingHistory() || [];
expect(fh.length).toEqual(20);
const result = new Array(19).fill(false);
expect(fh).toEqual(result.concat(true));
});
});
});

View file

@ -9,8 +9,6 @@ import { millisToNanos } from '@kbn/event-log-plugin/server';
import { cloneDeep } from 'lodash';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext } from '../types';
import { updateFlappingHistory } from './flapping_utils';
import { RulesSettingsFlappingProperties } from '../../common/rules_settings';
interface ProcessAlertsOpts<
State extends AlertInstanceState,
@ -18,12 +16,10 @@ interface ProcessAlertsOpts<
> {
alerts: Record<string, Alert<State, Context>>;
existingAlerts: Record<string, Alert<State, Context>>;
previouslyRecoveredAlerts: Record<string, Alert<State, Context>>;
hasReachedAlertLimit: boolean;
alertLimit: number;
autoRecoverAlerts: boolean;
startedAt?: string | null;
flappingSettings: RulesSettingsFlappingProperties;
}
interface ProcessAlertsResult<
State extends AlertInstanceState,
@ -33,8 +29,6 @@ interface ProcessAlertsResult<
> {
newAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>>;
// recovered alerts in the current rule run that were previously active
currentRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
}
@ -46,11 +40,9 @@ export function processAlerts<
>({
alerts,
existingAlerts,
previouslyRecoveredAlerts,
hasReachedAlertLimit,
alertLimit,
autoRecoverAlerts,
flappingSettings,
startedAt,
}: ProcessAlertsOpts<State, Context>): ProcessAlertsResult<
State,
@ -59,22 +51,8 @@ export function processAlerts<
RecoveryActionGroupId
> {
return hasReachedAlertLimit
? processAlertsLimitReached(
alerts,
existingAlerts,
previouslyRecoveredAlerts,
alertLimit,
flappingSettings,
startedAt
)
: processAlertsHelper(
alerts,
existingAlerts,
previouslyRecoveredAlerts,
autoRecoverAlerts,
flappingSettings,
startedAt
);
? processAlertsLimitReached(alerts, existingAlerts, alertLimit, startedAt)
: processAlertsHelper(alerts, existingAlerts, autoRecoverAlerts, startedAt);
}
function processAlertsHelper<
@ -85,18 +63,14 @@ function processAlertsHelper<
>(
alerts: Record<string, Alert<State, Context>>,
existingAlerts: Record<string, Alert<State, Context>>,
previouslyRecoveredAlerts: Record<string, Alert<State, Context>>,
autoRecoverAlerts: boolean,
flappingSettings: RulesSettingsFlappingProperties,
startedAt?: string | null
): ProcessAlertsResult<State, Context, ActionGroupIds, RecoveryActionGroupId> {
const existingAlertIds = new Set(Object.keys(existingAlerts));
const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts));
const currentTime = startedAt ?? new Date().toISOString();
const newAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {};
const activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {};
const currentRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>> = {};
const recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>> = {};
for (const id in alerts) {
@ -110,15 +84,6 @@ function processAlertsHelper<
newAlerts[id] = alerts[id];
const state = newAlerts[id].getState();
newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' });
if (flappingSettings.enabled) {
if (previouslyRecoveredAlertsIds.has(id)) {
// this alert has flapped from recovered to active
newAlerts[id].setFlappingHistory(previouslyRecoveredAlerts[id].getFlappingHistory());
previouslyRecoveredAlertsIds.delete(id);
}
updateAlertFlappingHistory(flappingSettings, newAlerts[id], true);
}
} else {
// this alert did exist in previous run
// calculate duration to date for active alerts
@ -132,15 +97,9 @@ function processAlertsHelper<
...(state.start ? { start: state.start } : {}),
...(duration !== undefined ? { duration } : {}),
});
// this alert is still active
if (flappingSettings.enabled) {
updateAlertFlappingHistory(flappingSettings, activeAlerts[id], false);
}
}
} else if (existingAlertIds.has(id) && autoRecoverAlerts) {
recoveredAlerts[id] = alerts[id];
currentRecoveredAlerts[id] = alerts[id];
// Inject end time into alert state of recovered alerts
const state = recoveredAlerts[id].getState();
@ -152,23 +111,11 @@ function processAlertsHelper<
...(duration ? { duration } : {}),
...(state.start ? { end: currentTime } : {}),
});
// this alert has flapped from active to recovered
if (flappingSettings.enabled) {
updateAlertFlappingHistory(flappingSettings, recoveredAlerts[id], true);
}
}
}
}
// alerts are still recovered
for (const id of previouslyRecoveredAlertsIds) {
recoveredAlerts[id] = previouslyRecoveredAlerts[id];
if (flappingSettings.enabled) {
updateAlertFlappingHistory(flappingSettings, recoveredAlerts[id], false);
}
}
return { recoveredAlerts, currentRecoveredAlerts, newAlerts, activeAlerts };
return { recoveredAlerts, newAlerts, activeAlerts };
}
function processAlertsLimitReached<
@ -179,13 +126,10 @@ function processAlertsLimitReached<
>(
alerts: Record<string, Alert<State, Context>>,
existingAlerts: Record<string, Alert<State, Context>>,
previouslyRecoveredAlerts: Record<string, Alert<State, Context>>,
alertLimit: number,
flappingSettings: RulesSettingsFlappingProperties,
startedAt?: string | null
): ProcessAlertsResult<State, Context, ActionGroupIds, RecoveryActionGroupId> {
const existingAlertIds = new Set(Object.keys(existingAlerts));
const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts));
// When the alert limit has been reached,
// - skip determination of recovered alerts
@ -215,11 +159,6 @@ function processAlertsLimitReached<
...(state.start ? { start: state.start } : {}),
...(duration !== undefined ? { duration } : {}),
});
// this alert is still active
if (flappingSettings.enabled) {
updateAlertFlappingHistory(flappingSettings, activeAlerts[id], false);
}
}
}
@ -229,7 +168,7 @@ function processAlertsLimitReached<
// if we don't have capacity for new alerts, return
if (!hasCapacityForNewAlerts()) {
return { recoveredAlerts: {}, currentRecoveredAlerts: {}, newAlerts: {}, activeAlerts };
return { recoveredAlerts: {}, newAlerts: {}, activeAlerts };
}
// look for new alerts and add until we hit capacity
@ -243,37 +182,11 @@ function processAlertsLimitReached<
const state = newAlerts[id].getState();
newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' });
if (flappingSettings.enabled) {
if (previouslyRecoveredAlertsIds.has(id)) {
// this alert has flapped from recovered to active
newAlerts[id].setFlappingHistory(previouslyRecoveredAlerts[id].getFlappingHistory());
}
updateAlertFlappingHistory(flappingSettings, newAlerts[id], true);
}
if (!hasCapacityForNewAlerts()) {
break;
}
}
}
}
return { recoveredAlerts: {}, currentRecoveredAlerts: {}, newAlerts, activeAlerts };
}
export function updateAlertFlappingHistory<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>(
flappingSettings: RulesSettingsFlappingProperties,
alert: Alert<State, Context, ActionGroupIds | RecoveryActionGroupId>,
state: boolean
) {
const updatedFlappingHistory = updateFlappingHistory(
flappingSettings,
alert.getFlappingHistory() || [],
state
);
alert.setFlappingHistory(updatedFlappingHistory);
return { recoveredAlerts: {}, newAlerts, activeAlerts };
}

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { keys, size } from 'lodash';
import { Alert } from '../alert';
import { toRawAlertInstances } from './to_raw_alert_instances';
describe('toRawAlertInstances', () => {
const flapping = new Array(16).fill(false).concat([true, true, true, true]);
const notFlapping = new Array(20).fill(false);
describe('toRawAlertInstances', () => {
test('should return all active alerts', () => {
const activeAlerts = {
'1': new Alert('1', { meta: { flappingHistory: flapping } }),
'2': new Alert('2', { meta: { flappingHistory: [false, false] } }),
};
const { rawActiveAlerts } = toRawAlertInstances(activeAlerts, {});
expect(size(rawActiveAlerts)).toEqual(2);
});
test('should return all recovered alerts', () => {
const recoveredAlerts = {
'1': new Alert('1', { meta: { flappingHistory: flapping } }),
'2': new Alert('2', { meta: { flappingHistory: notFlapping } }),
};
const { rawRecoveredAlerts } = toRawAlertInstances({}, recoveredAlerts);
expect(keys(rawRecoveredAlerts)).toEqual(['1', '2']);
});
});
});

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { keys } from 'lodash';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext, RawAlertInstance } from '../types';
export function toRawAlertInstances<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>(
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupId>> = {}
): {
rawActiveAlerts: Record<string, RawAlertInstance>;
rawRecoveredAlerts: Record<string, RawAlertInstance>;
} {
const rawActiveAlerts: Record<string, RawAlertInstance> = {};
const rawRecoveredAlerts: Record<string, RawAlertInstance> = {};
for (const id of keys(activeAlerts)) {
rawActiveAlerts[id] = activeAlerts[id].toRaw();
}
for (const id of keys(recoveredAlerts)) {
const alert = recoveredAlerts[id];
rawRecoveredAlerts[id] = alert.toRaw(true);
}
return { rawActiveAlerts, rawRecoveredAlerts };
}

View file

@ -1,82 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { Alert } from '../alert';
import { getEarlyRecoveredAlerts, trimRecoveredAlerts } from './trim_recovered_alerts';
describe('trimRecoveredAlerts', () => {
const logger = loggingSystemMock.createLogger();
const alert1 = new Alert('1', { meta: { flappingHistory: [true, true, true, true] } });
const alert2 = new Alert('2', { meta: { flappingHistory: new Array(20).fill(false) } });
const alert3 = new Alert('3', { meta: { flappingHistory: [true, true] } });
const alert4 = {
key: '4',
flappingHistory: [true, true, true, true],
};
const alert5 = {
key: '5',
flappingHistory: new Array(20).fill(false),
};
const alert6 = {
key: '6',
flappingHistory: [true, true],
};
test('should remove longest recovered alerts', () => {
const recoveredAlerts = {
'1': alert1,
'2': alert2,
'3': alert3,
};
const trimmedAlerts = trimRecoveredAlerts(logger, recoveredAlerts, 2);
expect(trimmedAlerts).toEqual({
trimmedAlertsRecovered: { 1: alert1, 3: alert3 },
earlyRecoveredAlerts: {
2: new Alert('2', {
meta: {
flappingHistory: new Array(20).fill(false),
flapping: false,
uuid: expect.any(String),
},
}),
},
});
});
test('should not remove alerts if the num of recovered alerts is not at the limit', () => {
const recoveredAlerts = {
'1': alert1,
'2': alert2,
'3': alert3,
};
const trimmedAlerts = trimRecoveredAlerts(logger, recoveredAlerts, 3);
expect(trimmedAlerts).toEqual({
trimmedAlertsRecovered: recoveredAlerts,
earlyRecoveredAlerts: {},
});
});
test('getEarlyRecoveredAlerts should return longest recovered alerts', () => {
const recoveredAlerts = [alert4, alert5, alert6];
const trimmedAlerts = getEarlyRecoveredAlerts(logger, recoveredAlerts, 2);
expect(trimmedAlerts).toEqual([alert5]);
expect(logger.warn).toBeCalledWith(
'Recovered alerts have exceeded the max alert limit of 2 : dropping 1 alert.'
);
});
test('getEarlyRecoveredAlerts should not return alerts if the num of recovered alerts is not at the limit', () => {
const recoveredAlerts = [alert4, alert5];
const trimmedAlerts = getEarlyRecoveredAlerts(logger, recoveredAlerts, 2);
expect(trimmedAlerts).toEqual([]);
});
});

View file

@ -1,76 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Logger } from '@kbn/logging';
import { map } from 'lodash';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext } from '../types';
interface TrimmedRecoveredAlertsResult<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
RecoveryActionGroupIds extends string
> {
trimmedAlertsRecovered: Record<string, Alert<State, Context, RecoveryActionGroupIds>>;
earlyRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupIds>>;
}
export interface TrimRecoveredOpts {
key: string;
flappingHistory: boolean[];
}
export function trimRecoveredAlerts<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
RecoveryActionGroupIds extends string
>(
logger: Logger,
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupIds>> = {},
maxAlerts: number
): TrimmedRecoveredAlertsResult<State, Context, RecoveryActionGroupIds> {
const alerts = map(recoveredAlerts, (value, key) => {
return {
key,
flappingHistory: value.getFlappingHistory() || [],
};
});
const earlyRecoveredAlertOpts = getEarlyRecoveredAlerts(logger, alerts, maxAlerts);
const earlyRecoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupIds>> = {};
earlyRecoveredAlertOpts.forEach((opt) => {
const alert = recoveredAlerts[opt.key];
alert.setFlapping(false);
earlyRecoveredAlerts[opt.key] = recoveredAlerts[opt.key];
delete recoveredAlerts[opt.key];
});
return {
trimmedAlertsRecovered: recoveredAlerts,
earlyRecoveredAlerts,
};
}
export function getEarlyRecoveredAlerts(
logger: Logger,
recoveredAlerts: TrimRecoveredOpts[],
maxAlerts: number
) {
let earlyRecoveredAlerts: TrimRecoveredOpts[] = [];
if (recoveredAlerts.length > maxAlerts) {
recoveredAlerts.sort((a, b) => {
return a.flappingHistory.length - b.flappingHistory.length;
});
earlyRecoveredAlerts = recoveredAlerts.slice(maxAlerts);
logger.warn(
`Recovered alerts have exceeded the max alert limit of ${maxAlerts} : dropping ${
earlyRecoveredAlerts.length
} ${earlyRecoveredAlerts.length > 1 ? 'alerts' : 'alert'}.`
);
}
return earlyRecoveredAlerts;
}

View file

@ -109,7 +109,7 @@ describe('Action Scheduler', () => {
test('schedules execution per selected action', async () => {
const alerts = generateAlert({ id: 1 });
const actionScheduler = new ActionScheduler(getSchedulerContext());
await actionScheduler.run({ activeCurrentAlerts: alerts, recoveredCurrentAlerts: {} });
await actionScheduler.run({ activeAlerts: alerts, recoveredAlerts: {} });
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(1);
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(1);
@ -212,8 +212,8 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(1);
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(2);
@ -280,8 +280,8 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 2 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2 }),
recoveredAlerts: {},
});
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(0);
@ -295,8 +295,8 @@ describe('Action Scheduler', () => {
});
await actionSchedulerForPreconfiguredAction.run({
activeCurrentAlerts: generateAlert({ id: 2 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2 }),
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
});
@ -338,8 +338,8 @@ describe('Action Scheduler', () => {
try {
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredAlerts: {},
});
} catch (err) {
expect(getErrorSource(err)).toBe(TaskErrorSource.USER);
@ -349,8 +349,8 @@ describe('Action Scheduler', () => {
test('limits actionsPlugin.execute per action group', async () => {
const actionScheduler = new ActionScheduler(getSchedulerContext());
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 2, group: 'other-group' }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2, group: 'other-group' }),
recoveredAlerts: {},
});
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(0);
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(0);
@ -360,8 +360,8 @@ describe('Action Scheduler', () => {
test('context attribute gets parameterized', async () => {
const actionScheduler = new ActionScheduler(getSchedulerContext());
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 2, context: { value: 'context-val' } }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2, context: { value: 'context-val' } }),
recoveredAlerts: {},
});
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(1);
expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toBe(1);
@ -409,8 +409,8 @@ describe('Action Scheduler', () => {
test('state attribute gets parameterized', async () => {
const actionScheduler = new ActionScheduler(getSchedulerContext());
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
expect(actionsClient.bulkEnqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
@ -456,11 +456,11 @@ describe('Action Scheduler', () => {
test(`logs an error when action group isn't part of actionGroups available for the ruleType`, async () => {
const actionScheduler = new ActionScheduler(getSchedulerContext());
await actionScheduler.run({
activeCurrentAlerts: generateAlert({
activeAlerts: generateAlert({
id: 2,
group: 'invalid-group' as 'default' | 'other-group',
}),
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(defaultSchedulerContext.logger.error).toHaveBeenCalledWith(
@ -540,8 +540,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredAlerts: {},
});
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(2);
@ -644,8 +644,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredAlerts: {},
});
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(4);
@ -731,8 +731,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2, state: { value: 'state-val' } }),
recoveredAlerts: {},
});
expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toBe(2);
@ -768,8 +768,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: {},
recoveredCurrentAlerts: generateRecoveredAlert({ id: 1 }),
activeAlerts: {},
recoveredAlerts: generateRecoveredAlert({ id: 1 }),
});
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
@ -838,8 +838,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: {},
recoveredCurrentAlerts: generateRecoveredAlert({ id: 1 }),
activeAlerts: {},
recoveredAlerts: generateRecoveredAlert({ id: 1 }),
});
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(0);
@ -861,8 +861,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
clock.tick(30000);
@ -894,11 +894,11 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({
activeAlerts: generateAlert({
id: 1,
throttledActions: { '111-111': { date: new Date(DATE_1970).toISOString() } },
}),
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
clock.tick(30000);
@ -930,8 +930,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1, lastScheduledActionsGroup: 'recovered' }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1, lastScheduledActionsGroup: 'recovered' }),
recoveredAlerts: {},
});
clock.tick(30000);
@ -951,8 +951,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(0);
@ -1009,8 +1009,8 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
@ -1095,8 +1095,8 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {},
recoveredCurrentAlerts: {},
activeAlerts: {},
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled();
@ -1150,8 +1150,8 @@ describe('Action Scheduler', () => {
);
const result = await actionScheduler.run({
activeCurrentAlerts: {},
recoveredCurrentAlerts: {},
activeAlerts: {},
recoveredAlerts: {},
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
@ -1251,8 +1251,8 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {},
recoveredCurrentAlerts: {},
activeAlerts: {},
recoveredAlerts: {},
});
expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1);
expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith(
@ -1316,8 +1316,8 @@ describe('Action Scheduler', () => {
);
const result = await actionScheduler.run({
activeCurrentAlerts: {},
recoveredCurrentAlerts: {},
activeAlerts: {},
recoveredAlerts: {},
});
expect(result).toEqual({
throttledSummaryActions: {
@ -1354,8 +1354,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 2 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 2 }),
recoveredAlerts: {},
});
expect(defaultSchedulerContext.logger.error).toHaveBeenCalledWith(
@ -1418,8 +1418,8 @@ describe('Action Scheduler', () => {
})
);
await actionScheduler.run({
activeCurrentAlerts: {},
recoveredCurrentAlerts: generateRecoveredAlert({ id: 1 }),
activeAlerts: {},
recoveredAlerts: generateRecoveredAlert({ id: 1 }),
});
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
@ -1547,11 +1547,11 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {
activeAlerts: {
...generateAlert({ id: 1 }),
...generateAlert({ id: 2 }),
},
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
@ -1624,11 +1624,11 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {
activeAlerts: {
...generateAlert({ id: 1 }),
...generateAlert({ id: 2 }),
},
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
@ -1695,12 +1695,12 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {
activeAlerts: {
...generateAlert({ id: 1 }),
...generateAlert({ id: 2 }),
...generateAlert({ id: 3 }),
},
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
@ -1807,12 +1807,12 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {
activeAlerts: {
...generateAlert({ id: 1, maintenanceWindowIds: ['test-id-1'] }),
...generateAlert({ id: 2, maintenanceWindowIds: ['test-id-2'] }),
...generateAlert({ id: 3, maintenanceWindowIds: ['test-id-3'] }),
},
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled();
@ -1859,12 +1859,12 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {
activeAlerts: {
...generateAlert({ id: 1, maintenanceWindowIds: ['test-id-1'] }),
...generateAlert({ id: 2, maintenanceWindowIds: ['test-id-2'] }),
...generateAlert({ id: 3, maintenanceWindowIds: ['test-id-3'] }),
},
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled();
@ -1880,12 +1880,12 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext());
await actionScheduler.run({
activeCurrentAlerts: {
activeAlerts: {
...generateAlert({ id: 1, maintenanceWindowIds: ['test-id-1'] }),
...generateAlert({ id: 2, maintenanceWindowIds: ['test-id-2'] }),
...generateAlert({ id: 3, maintenanceWindowIds: ['test-id-3'] }),
},
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled();
@ -1923,7 +1923,7 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {
activeAlerts: {
...generateAlert({
id: 1,
pendingRecoveredCount: 1,
@ -1940,7 +1940,7 @@ describe('Action Scheduler', () => {
lastScheduledActionsGroup: 'recovered',
}),
},
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled();
@ -1967,7 +1967,7 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {
activeAlerts: {
...generateAlert({
id: 1,
pendingRecoveredCount: 1,
@ -1984,7 +1984,7 @@ describe('Action Scheduler', () => {
lastScheduledActionsGroup: 'recovered',
}),
},
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
@ -2114,7 +2114,7 @@ describe('Action Scheduler', () => {
);
await actionScheduler.run({
activeCurrentAlerts: {
activeAlerts: {
...generateAlert({
id: 1,
pendingRecoveredCount: 1,
@ -2131,7 +2131,7 @@ describe('Action Scheduler', () => {
lastScheduledActionsGroup: 'recovered',
}),
},
recoveredCurrentAlerts: {},
recoveredAlerts: {},
});
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
@ -2286,8 +2286,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(execParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
@ -2322,8 +2322,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(execParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(injectActionParamsMock.mock.calls[0][0].actionParams).toEqual({
@ -2365,8 +2365,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(execParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@ -2403,8 +2403,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(execParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@ -2438,8 +2438,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(execParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@ -2473,8 +2473,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(execParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@ -2505,8 +2505,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(execParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@ -2537,8 +2537,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(execParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@ -2572,8 +2572,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(execParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@ -2643,8 +2643,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(executorParams));
const res = await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
/**
* Verifies that system actions are not throttled
@ -2770,8 +2770,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(executorParams));
const res = await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
/**
@ -2830,8 +2830,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(executorParams));
const res = await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(res).toEqual({ throttledSummaryActions: {} });
@ -2872,8 +2872,8 @@ describe('Action Scheduler', () => {
const actionScheduler = new ActionScheduler(getSchedulerContext(executorParams));
await actionScheduler.run({
activeCurrentAlerts: generateAlert({ id: 1 }),
recoveredCurrentAlerts: {},
activeAlerts: generateAlert({ id: 1 }),
recoveredAlerts: {},
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();

View file

@ -75,11 +75,11 @@ export class ActionScheduler<
}
public async run({
activeCurrentAlerts,
recoveredCurrentAlerts,
activeAlerts,
recoveredAlerts,
}: {
activeCurrentAlerts?: Record<string, Alert<State, Context, ActionGroupIds>>;
recoveredCurrentAlerts?: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
activeAlerts?: Record<string, Alert<State, Context, ActionGroupIds>>;
recoveredAlerts?: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
}): Promise<RunResult> {
const throttledSummaryActions: ThrottledActions = getSummaryActionsFromTaskState({
actions: this.context.rule.actions,
@ -90,8 +90,8 @@ export class ActionScheduler<
for (const scheduler of this.schedulers) {
allActionsToScheduleResult.push(
...(await scheduler.getActionsToSchedule({
activeCurrentAlerts,
recoveredCurrentAlerts,
activeAlerts,
recoveredAlerts,
throttledSummaryActions,
}))
);

View file

@ -223,7 +223,7 @@ describe('Per-Alert Action Scheduler', () => {
// 2 per-alert actions * 2 alerts = 4 actions to schedule
const scheduler = new PerAlertActionScheduler(getSchedulerContext());
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -252,7 +252,7 @@ describe('Per-Alert Action Scheduler', () => {
priority: TaskPriority.Low,
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -281,7 +281,7 @@ describe('Per-Alert Action Scheduler', () => {
apiKeyId: '23534ybfsdsnsdf',
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -313,7 +313,7 @@ describe('Per-Alert Action Scheduler', () => {
});
const alertsWithMaintenanceWindow = { ...newAlertWithMaintenanceWindow, ...newAlert2 };
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alertsWithMaintenanceWindow,
activeAlerts: alertsWithMaintenanceWindow,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -352,7 +352,7 @@ describe('Per-Alert Action Scheduler', () => {
});
const alertsWithInvalidActionGroup = { ...newAlertInvalidActionGroup, ...newAlert2 };
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alertsWithInvalidActionGroup,
activeAlerts: alertsWithInvalidActionGroup,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -390,7 +390,7 @@ describe('Per-Alert Action Scheduler', () => {
});
const alertsWithInvalidActionGroup = { ...newAlertInvalidActionGroup, ...newAlert2 };
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alertsWithInvalidActionGroup,
activeAlerts: alertsWithInvalidActionGroup,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -422,7 +422,7 @@ describe('Per-Alert Action Scheduler', () => {
...newAlert2,
};
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alertsWithPendingRecoveredCount,
activeAlerts: alertsWithPendingRecoveredCount,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -468,7 +468,7 @@ describe('Per-Alert Action Scheduler', () => {
...newAlert2,
};
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alertsWithPendingRecoveredCount,
activeAlerts: alertsWithPendingRecoveredCount,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -495,7 +495,7 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, mutedInstanceIds: ['2'] },
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -556,7 +556,7 @@ describe('Per-Alert Action Scheduler', () => {
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alertsWithOngoingAlert,
activeAlerts: alertsWithOngoingAlert,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -613,7 +613,7 @@ describe('Per-Alert Action Scheduler', () => {
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alertsWithOngoingAlert,
activeAlerts: alertsWithOngoingAlert,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -670,7 +670,7 @@ describe('Per-Alert Action Scheduler', () => {
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alertsWithOngoingAlert,
activeAlerts: alertsWithOngoingAlert,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -729,7 +729,7 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] },
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
@ -790,7 +790,7 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] },
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
@ -852,7 +852,7 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] },
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
@ -914,7 +914,7 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] },
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
@ -977,7 +977,7 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] },
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
@ -1038,7 +1038,7 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] },
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
@ -1081,7 +1081,7 @@ describe('Per-Alert Action Scheduler', () => {
},
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -1119,7 +1119,7 @@ describe('Per-Alert Action Scheduler', () => {
},
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@ -1154,7 +1154,7 @@ describe('Per-Alert Action Scheduler', () => {
expect(alert.getLastScheduledActions()).toBeUndefined();
expect(alert.hasScheduledActions()).toBe(true);
await scheduler.getActionsToSchedule({
activeCurrentAlerts: { '1': alert },
activeAlerts: { '1': alert },
});
expect(alert.getLastScheduledActions()).toEqual({
@ -1193,7 +1193,7 @@ describe('Per-Alert Action Scheduler', () => {
});
await scheduler.getActionsToSchedule({
activeCurrentAlerts: { '1': alert },
activeAlerts: { '1': alert },
});
expect(alert.getLastScheduledActions()).toEqual({

View file

@ -100,8 +100,8 @@ export class PerAlertActionScheduler<
}
public async getActionsToSchedule({
activeCurrentAlerts,
recoveredCurrentAlerts,
activeAlerts,
recoveredAlerts,
}: GetActionsToScheduleOpts<State, Context, ActionGroupIds, RecoveryActionGroupId>): Promise<
ActionsToSchedule[]
> {
@ -111,8 +111,8 @@ export class PerAlertActionScheduler<
}> = [];
const results: ActionsToSchedule[] = [];
const activeCurrentAlertsArray = Object.values(activeCurrentAlerts || {});
const recoveredCurrentAlertsArray = Object.values(recoveredCurrentAlerts || {});
const activeAlertsArray = Object.values(activeAlerts || {});
const recoveredAlertsArray = Object.values(recoveredAlerts || {});
for (const action of this.actions) {
let summarizedAlerts = null;
@ -140,13 +140,13 @@ export class PerAlertActionScheduler<
logNumberOfFilteredAlerts({
logger: this.context.logger,
numberOfAlerts: activeCurrentAlertsArray.length + recoveredCurrentAlertsArray.length,
numberOfAlerts: activeAlertsArray.length + recoveredAlertsArray.length,
numberOfSummarizedAlerts: summarizedAlerts.all.count,
action,
});
}
for (const alert of activeCurrentAlertsArray) {
for (const alert of activeAlertsArray) {
if (
this.isExecutableAlert({ alert, action, summarizedAlerts }) &&
this.isExecutableActiveAlert({ alert, action })
@ -157,7 +157,7 @@ export class PerAlertActionScheduler<
}
if (this.isRecoveredAction(action.group)) {
for (const alert of recoveredCurrentAlertsArray) {
for (const alert of recoveredAlertsArray) {
if (this.isExecutableAlert({ alert, action, summarizedAlerts })) {
this.addSummarizedAlerts({ alert, summarizedAlerts });
executables.push({ action, alert });

View file

@ -234,7 +234,7 @@ describe('Summary Action Scheduler', () => {
const throttledSummaryActions = {};
const scheduler = new SummaryActionScheduler(getSchedulerContext());
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions,
});
@ -285,7 +285,7 @@ describe('Summary Action Scheduler', () => {
priority: TaskPriority.Low,
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions,
});
@ -336,7 +336,7 @@ describe('Summary Action Scheduler', () => {
apiKeyId: '24534wy3wydfbs',
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions,
});
@ -388,7 +388,7 @@ describe('Summary Action Scheduler', () => {
const throttledSummaryActions = {};
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions,
});
@ -432,7 +432,7 @@ describe('Summary Action Scheduler', () => {
const throttledSummaryActions = {};
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions,
});
@ -468,7 +468,7 @@ describe('Summary Action Scheduler', () => {
const throttledSummaryActions = { '444-444': { date: '1969-12-31T13:00:00.000Z' } };
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions,
});
@ -505,7 +505,7 @@ describe('Summary Action Scheduler', () => {
const throttledSummaryActions = {};
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions,
});
@ -570,8 +570,8 @@ describe('Summary Action Scheduler', () => {
const throttledSummaryActions = {};
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
recoveredCurrentAlerts: recoveredAlert,
activeAlerts: alerts,
recoveredAlerts: recoveredAlert,
throttledSummaryActions,
});
@ -618,7 +618,7 @@ describe('Summary Action Scheduler', () => {
const throttledSummaryActions = {};
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions,
});
@ -648,7 +648,7 @@ describe('Summary Action Scheduler', () => {
try {
await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions: {},
});
} catch (err) {
@ -677,7 +677,7 @@ describe('Summary Action Scheduler', () => {
},
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions: {},
});
@ -734,7 +734,7 @@ describe('Summary Action Scheduler', () => {
},
});
const results = await scheduler.getActionsToSchedule({
activeCurrentAlerts: alerts,
activeAlerts: alerts,
throttledSummaryActions: {},
});

View file

@ -81,13 +81,13 @@ export class SummaryActionScheduler<
}
public async getActionsToSchedule({
activeCurrentAlerts,
recoveredCurrentAlerts,
activeAlerts,
recoveredAlerts,
throttledSummaryActions,
}: GetActionsToScheduleOpts<State, Context, ActionGroupIds, RecoveryActionGroupId>): Promise<
ActionsToSchedule[]
> {
const alerts = { ...activeCurrentAlerts, ...recoveredCurrentAlerts };
const alerts = { ...activeAlerts, ...recoveredAlerts };
const executables: Array<{
action: RuleAction;
summarizedAlerts: CombinedSummarizedAlerts;

View file

@ -97,8 +97,8 @@ export interface GetActionsToScheduleOpts<
ActionGroupIds extends string,
RecoveryActionGroupId extends string
> {
activeCurrentAlerts?: Record<string, Alert<State, Context, ActionGroupIds>>;
recoveredCurrentAlerts?: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
activeAlerts?: Record<string, Alert<State, Context, ActionGroupIds>>;
recoveredAlerts?: Record<string, Alert<State, Context, RecoveryActionGroupId>>;
throttledSummaryActions?: ThrottledActions;
}

View file

@ -285,8 +285,8 @@ export class AdHocTaskRunner implements CancellableTask {
});
await actionScheduler.run({
activeCurrentAlerts: alertsClient.getProcessedAlerts('activeCurrent'),
recoveredCurrentAlerts: alertsClient.getProcessedAlerts('recoveredCurrent'),
activeAlerts: alertsClient.getProcessedAlerts('active'),
recoveredAlerts: alertsClient.getProcessedAlerts('recovered'),
});
return ruleRunMetricsStore.getMetrics();

View file

@ -269,8 +269,9 @@ describe('RuleTypeRunner', () => {
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
);
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
expect(alertsClient.processAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineFlappingAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineDelayedAlerts).toHaveBeenCalledWith({
alertDelay: 0,
ruleRunMetricsStore,
});
@ -377,8 +378,9 @@ describe('RuleTypeRunner', () => {
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
);
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
expect(alertsClient.processAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineFlappingAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineDelayedAlerts).toHaveBeenCalledWith({
alertDelay: 0,
ruleRunMetricsStore,
});
@ -439,8 +441,9 @@ describe('RuleTypeRunner', () => {
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
);
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
expect(alertsClient.processAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineFlappingAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineDelayedAlerts).toHaveBeenCalledWith({
alertDelay: 0,
ruleRunMetricsStore,
});
@ -553,6 +556,8 @@ describe('RuleTypeRunner', () => {
);
expect(ruleRunMetricsStore.setSearchMetrics).not.toHaveBeenCalled();
expect(alertsClient.processAlerts).not.toHaveBeenCalled();
expect(alertsClient.determineFlappingAlerts).not.toHaveBeenCalled();
expect(alertsClient.determineDelayedAlerts).not.toHaveBeenCalled();
expect(alertsClient.persistAlerts).not.toHaveBeenCalled();
expect(alertsClient.logAlerts).not.toHaveBeenCalled();
});
@ -657,6 +662,8 @@ describe('RuleTypeRunner', () => {
);
expect(ruleRunMetricsStore.setSearchMetrics).not.toHaveBeenCalled();
expect(alertsClient.processAlerts).not.toHaveBeenCalled();
expect(alertsClient.determineFlappingAlerts).not.toHaveBeenCalled();
expect(alertsClient.determineDelayedAlerts).not.toHaveBeenCalled();
expect(alertsClient.persistAlerts).not.toHaveBeenCalled();
expect(alertsClient.logAlerts).not.toHaveBeenCalled();
});
@ -800,8 +807,9 @@ describe('RuleTypeRunner', () => {
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
);
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
expect(alertsClient.processAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineFlappingAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineDelayedAlerts).toHaveBeenCalledWith({
alertDelay: 0,
ruleRunMetricsStore,
});
@ -915,8 +923,9 @@ describe('RuleTypeRunner', () => {
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
);
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
expect(alertsClient.processAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineFlappingAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineDelayedAlerts).toHaveBeenCalledWith({
alertDelay: 0,
ruleRunMetricsStore,
});
@ -1024,11 +1033,9 @@ describe('RuleTypeRunner', () => {
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
);
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
alertDelay: 0,
ruleRunMetricsStore,
});
expect(alertsClient.processAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineFlappingAlerts).not.toHaveBeenCalled();
expect(alertsClient.determineDelayedAlerts).not.toHaveBeenCalled();
expect(alertsClient.persistAlerts).not.toHaveBeenCalled();
expect(alertsClient.logAlerts).not.toHaveBeenCalled();
});
@ -1130,8 +1137,9 @@ describe('RuleTypeRunner', () => {
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
);
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
expect(alertsClient.processAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineFlappingAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineDelayedAlerts).toHaveBeenCalledWith({
alertDelay: 0,
ruleRunMetricsStore,
});
@ -1236,8 +1244,9 @@ describe('RuleTypeRunner', () => {
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
);
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
expect(alertsClient.processAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineFlappingAlerts).toHaveBeenCalledWith();
expect(alertsClient.determineDelayedAlerts).toHaveBeenCalledWith({
alertDelay: 0,
ruleRunMetricsStore,
});

View file

@ -338,8 +338,9 @@ export class RuleTypeRunner<
await withAlertingSpan('alerting:process-alerts', () =>
this.options.timer.runWithTimer(TaskRunnerTimerSpan.ProcessAlerts, async () => {
await alertsClient.processAlerts({
flappingSettings: context.flappingSettings ?? DEFAULT_FLAPPING_SETTINGS,
await alertsClient.processAlerts();
alertsClient.determineFlappingAlerts();
alertsClient.determineDelayedAlerts({
alertDelay: alertDelay?.active ?? 0,
ruleRunMetricsStore: context.ruleRunMetricsStore,
});

View file

@ -1530,6 +1530,9 @@ describe('Task Runner', () => {
{ tags: ['1', 'test'] }
);
alertsClient.getRawAlertInstancesForState.mockResolvedValueOnce({ state: {}, meta: {} });
alertsService.createAlertsClient.mockImplementation(() => alertsClient);
testAlertingEventLogCalls({
activeAlerts: 1,
recoveredAlerts: 1,
@ -1754,7 +1757,7 @@ describe('Task Runner', () => {
recovered: { count: 0, data: [] },
});
alertsClient.getAlertsToSerialize.mockResolvedValueOnce({ state: {}, meta: {} });
alertsClient.getRawAlertInstancesForState.mockResolvedValueOnce({ state: {}, meta: {} });
alertsService.createAlertsClient.mockImplementation(() => alertsClient);
const taskRunner = new TaskRunner({

View file

@ -417,8 +417,8 @@ export class TaskRunner<
this.countUsageOfActionExecutionAfterRuleCancellation();
} else {
actionSchedulerResult = await actionScheduler.run({
activeCurrentAlerts: alertsClient.getProcessedAlerts('activeCurrent'),
recoveredCurrentAlerts: alertsClient.getProcessedAlerts('recoveredCurrent'),
activeAlerts: alertsClient.getProcessedAlerts('active'),
recoveredAlerts: alertsClient.getProcessedAlerts('recovered'),
});
}
})
@ -430,10 +430,9 @@ export class TaskRunner<
// Only serialize alerts into task state if we're auto-recovering, otherwise
// we don't need to keep this information around.
if (this.ruleType.autoRecoverAlerts) {
const { alertsToReturn: alerts, recoveredAlertsToReturn: recovered } =
alertsClient.getAlertsToSerialize();
alertsToReturn = alerts;
recoveredAlertsToReturn = recovered;
const alerts = alertsClient.getRawAlertInstancesForState();
alertsToReturn = alerts.rawActiveAlerts;
recoveredAlertsToReturn = alerts.rawRecoveredAlerts;
}
return {

View file

@ -297,9 +297,9 @@ describe('Task Runner', () => {
.spyOn(RuleRunMetricsStoreModule, 'RuleRunMetricsStore')
.mockImplementation(() => ruleRunMetricsStore);
mockAlertsService.createAlertsClient.mockImplementation(() => mockAlertsClient);
mockAlertsClient.getAlertsToSerialize.mockResolvedValue({
alertsToReturn: {},
recoveredAlertsToReturn: {},
mockAlertsClient.getRawAlertInstancesForState.mockResolvedValue({
rawActiveAlerts: {},
rawRecoveredAlerts: {},
});
ruleRunMetricsStore.getMetrics.mockReturnValue({
numSearches: 3,
@ -656,9 +656,9 @@ describe('Task Runner', () => {
mockAlertsService.createAlertsClient.mockImplementation(() => {
throw new Error('Could not initialize!');
});
mockLegacyAlertsClient.getAlertsToSerialize.mockResolvedValue({
alertsToReturn: {},
recoveredAlertsToReturn: {},
mockLegacyAlertsClient.getRawAlertInstancesForState.mockResolvedValue({
rawActiveAlerts: {},
rawRecoveredAlerts: {},
});
ruleRunMetricsStore.getMetrics.mockReturnValue({
numSearches: 3,
@ -750,9 +750,9 @@ describe('Task Runner', () => {
const spy2 = jest
.spyOn(RuleRunMetricsStoreModule, 'RuleRunMetricsStore')
.mockImplementation(() => ruleRunMetricsStore);
mockLegacyAlertsClient.getAlertsToSerialize.mockResolvedValue({
alertsToReturn: {},
recoveredAlertsToReturn: {},
mockLegacyAlertsClient.getRawAlertInstancesForState.mockResolvedValue({
rawActiveAlerts: {},
rawRecoveredAlerts: {},
});
ruleRunMetricsStore.getMetrics.mockReturnValue({
numSearches: 3,
@ -836,9 +836,9 @@ describe('Task Runner', () => {
test('should use rule specific flapping settings if global flapping is enabled', async () => {
mockAlertsService.createAlertsClient.mockImplementation(() => mockAlertsClient);
mockAlertsClient.getAlertsToSerialize.mockResolvedValue({
alertsToReturn: {},
recoveredAlertsToReturn: {},
mockAlertsClient.getRawAlertInstancesForState.mockResolvedValue({
rawActiveAlerts: {},
rawRecoveredAlerts: {},
});
const taskRunner = new TaskRunner({
@ -893,9 +893,9 @@ describe('Task Runner', () => {
});
mockAlertsService.createAlertsClient.mockImplementation(() => mockAlertsClient);
mockAlertsClient.getAlertsToSerialize.mockResolvedValue({
alertsToReturn: {},
recoveredAlertsToReturn: {},
mockAlertsClient.getRawAlertInstancesForState.mockResolvedValue({
rawActiveAlerts: {},
rawRecoveredAlerts: {},
});
const taskRunner = new TaskRunner({
@ -983,15 +983,7 @@ describe('Task Runner', () => {
expect(alertsClientToUse.checkLimitUsage).toHaveBeenCalled();
expect(alertsClientNotToUse.checkLimitUsage).not.toHaveBeenCalled();
expect(alertsClientToUse.processAlerts).toHaveBeenCalledWith({
alertDelay: 0,
flappingSettings: {
enabled: true,
lookBackWindow: 20,
statusChangeThreshold: 4,
},
ruleRunMetricsStore,
});
expect(alertsClientToUse.processAlerts).toHaveBeenCalledWith();
expect(alertsClientToUse.logAlerts).toHaveBeenCalledWith({
ruleRunMetricsStore,
@ -1004,12 +996,12 @@ describe('Task Runner', () => {
expect(alertsClientToUse.persistAlerts).toHaveBeenCalled();
expect(alertsClientNotToUse.persistAlerts).not.toHaveBeenCalled();
expect(alertsClientToUse.getProcessedAlerts).toHaveBeenCalledWith('activeCurrent');
expect(alertsClientToUse.getProcessedAlerts).toHaveBeenCalledWith('recoveredCurrent');
expect(alertsClientToUse.getProcessedAlerts).toHaveBeenCalledWith('active');
expect(alertsClientToUse.getProcessedAlerts).toHaveBeenCalledWith('recovered');
expect(alertsClientNotToUse.getProcessedAlerts).not.toHaveBeenCalled();
expect(alertsClientToUse.getAlertsToSerialize).toHaveBeenCalled();
expect(alertsClientNotToUse.getAlertsToSerialize).not.toHaveBeenCalled();
expect(alertsClientToUse.getRawAlertInstancesForState).toHaveBeenCalled();
expect(alertsClientNotToUse.getRawAlertInstancesForState).not.toHaveBeenCalled();
}
}
});

View file

@ -127,11 +127,9 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid
state.alertRecoveredInstances.alertA.meta.flappingHistory
);
// Flapping value for alert doc should be false while flapping value for state should be true
// This is because we write out the alert doc BEFORE calculating the latest flapping state and
// persisting into task state
// Flapping value for alert doc and task state should be false
expect(alertDocs[0]._source![ALERT_FLAPPING]).to.equal(false);
expect(state.alertRecoveredInstances.alertA.meta.flapping).to.equal(true);
expect(state.alertRecoveredInstances.alertA.meta.flapping).to.equal(false);
// Run the rule 6 more times
for (let i = 0; i < 6; i++) {
@ -258,11 +256,9 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid
state.alertRecoveredInstances.alertA.meta.flappingHistory
);
// Flapping value for alert doc should be false while flapping value for state should be true
// This is because we write out the alert doc BEFORE calculating the latest flapping state and
// persisting into task state
// Flapping value for task state should be false
expect(alertDocs[0]._source![ALERT_FLAPPING]).to.equal(false);
expect(state.alertRecoveredInstances.alertA.meta.flapping).to.equal(true);
expect(state.alertRecoveredInstances.alertA.meta.flapping).to.equal(false);
// Run the rule 6 more times
for (let i = 0; i < 6; i++) {
@ -401,7 +397,7 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
// notify_when is not RuleNotifyWhen.CHANGE, so it's not added to activeCurrent
// notify_when is not RuleNotifyWhen.CHANGE, so it's not added to active
getTestRuleData({
rule_type_id: 'test.patternFiringAad',
// set the schedule long so we can use "runSoon" to specify rule runs
@ -539,11 +535,9 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid
state.alertInstances.alertA.meta.flappingHistory
);
// Flapping value for alert doc should be false while flapping value for state should be true
// This is because we write out the alert doc BEFORE calculating the latest flapping state and
// persisting into task state
// Flapping value for alert doc and task state should be false
expect(alertDocs[0]._source![ALERT_FLAPPING]).to.equal(false);
expect(state.alertInstances.alertA.meta.flapping).to.equal(true);
expect(state.alertInstances.alertA.meta.flapping).to.equal(false);
// Run the rule 6 more times
for (let i = 0; i < 6; i++) {
@ -569,11 +563,9 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid
state.alertInstances.alertA.meta.flappingHistory
);
// Flapping value for alert doc should be true while flapping value for state should be false
// This is because we write out the alert doc BEFORE calculating the latest flapping state and
// persisting into task state
// Flapping value for alert doc and task state should be true
expect(alertDocs[0]._source![ALERT_FLAPPING]).to.equal(true);
expect(state.alertInstances.alertA.meta.flapping).to.equal(false);
expect(state.alertInstances.alertA.meta.flapping).to.equal(true);
// Run the rule 3 more times
for (let i = 0; i < 3; i++) {