[ResponseOps][Flapping] Update flapping code once the flapping lookback value is configurable (#149448)

Resolves https://github.com/elastic/kibana/issues/145929

## Summary

Updates previous flapping tests to use the new flapping settings
configs.
Updates flapping logic to use flapping configs instead of hardcoded
values. Calls the flapping api on every rule execution, and then passes
in the flapping settings to the rule executors so they can be used by
the rule registry.


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios


### To verify

I think it's helpful to hide the whitespace when reviewing this pr.

- The flapping logic should remain the same, and all previous tests
should pass. I only updated them to pass in the flapping settings.
- Create rules, and set flapping settings in the ui and see the flapping
behavior change for your rules.
- Verify that the
`x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts`
run with the new flapping configs and output results we would expect

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alexi Doak 2023-02-02 08:32:30 -05:00 committed by GitHub
parent 0f12bbce71
commit cb7cc4a4c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 770 additions and 275 deletions

View file

@ -44,8 +44,13 @@ export const RULES_SETTINGS_SAVED_OBJECT_ID = 'rules-settings';
export const DEFAULT_LOOK_BACK_WINDOW = 20;
export const DEFAULT_STATUS_CHANGE_THRESHOLD = 4;
export const DEFAULT_FLAPPING_SETTINGS = {
export const DEFAULT_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = {
enabled: true,
lookBackWindow: 20,
statusChangeThreshold: 4,
lookBackWindow: DEFAULT_LOOK_BACK_WINDOW,
statusChangeThreshold: DEFAULT_STATUS_CHANGE_THRESHOLD,
};
export const DISABLE_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = {
...DEFAULT_FLAPPING_SETTINGS,
enabled: false,
};

View file

@ -10,6 +10,7 @@ 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,
@ -149,8 +150,8 @@ export function createAlertFactory<
hasReachedAlertLimit,
alertLimit: maxAlerts,
autoRecoverAlerts,
// setFlapping is false, as we only want to use this function to get the recovered alerts
setFlapping: false,
// 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]

View file

@ -14,6 +14,7 @@ import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_e
import { ruleRunMetricsStoreMock } from '../lib/rule_run_metrics_store.mock';
import { getAlertsForNotification, processAlerts, setFlapping } from '../lib';
import { logAlerts } from '../task_runner/log_alerts';
import { DEFAULT_FLAPPING_SETTINGS } from '../../common/rules_settings';
const scheduleActions = jest.fn();
const replaceState = jest.fn(() => ({ scheduleActions }));
@ -229,6 +230,7 @@ describe('Legacy Alerts Client', () => {
ruleLabel: `ruleLogPrefix`,
ruleRunMetricsStore,
shouldLogAndScheduleActionsForAlerts: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(processAlerts).toHaveBeenCalledWith({
@ -244,10 +246,15 @@ describe('Legacy Alerts Client', () => {
hasReachedAlertLimit: false,
alertLimit: 1000,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(setFlapping).toHaveBeenCalledWith(
{
enabled: true,
lookBackWindow: 20,
statusChangeThreshold: 4,
},
{
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
@ -256,6 +263,11 @@ describe('Legacy Alerts Client', () => {
);
expect(getAlertsForNotification).toHaveBeenCalledWith(
{
enabled: true,
lookBackWindow: 20,
statusChangeThreshold: 4,
},
'default',
{},
{

View file

@ -29,6 +29,7 @@ import {
RawAlertInstance,
WithoutReservedActionGroups,
} from '../types';
import { RulesSettingsFlappingProperties } from '../../common/rules_settings';
interface ConstructorOpts {
logger: Logger;
@ -111,11 +112,13 @@ export class LegacyAlertsClient<
ruleLabel,
ruleRunMetricsStore,
shouldLogAndScheduleActionsForAlerts,
flappingSettings,
}: {
eventLogger: AlertingEventLogger;
ruleLabel: string;
shouldLogAndScheduleActionsForAlerts: boolean;
ruleRunMetricsStore: RuleRunMetricsStore;
flappingSettings: RulesSettingsFlappingProperties;
}) {
const {
newAlerts: processedAlertsNew,
@ -132,10 +135,11 @@ export class LegacyAlertsClient<
this.options.ruleType.autoRecoverAlerts !== undefined
? this.options.ruleType.autoRecoverAlerts
: true,
setFlapping: true,
flappingSettings,
});
setFlapping<State, Context, ActionGroupIds, RecoveryActionGroupId>(
flappingSettings,
processedAlertsActive,
processedAlertsRecovered
);
@ -147,6 +151,7 @@ export class LegacyAlertsClient<
);
const alerts = getAlertsForNotification<State, Context, ActionGroupIds, RecoveryActionGroupId>(
flappingSettings,
this.options.ruleType.defaultActionGroupId,
processedAlertsNew,
processedAlertsActive,

View file

@ -5,18 +5,23 @@
* 2.0.
*/
import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings';
import { atCapacity, updateFlappingHistory, isFlapping } from './flapping_utils';
describe('flapping utils', () => {
describe('updateFlappingHistory function', () => {
test('correctly updates flappingHistory', () => {
const flappingHistory = updateFlappingHistory([false, false], true);
const flappingHistory = updateFlappingHistory(
DEFAULT_FLAPPING_SETTINGS,
[false, false],
true
);
expect(flappingHistory).toEqual([false, false, true]);
});
test('correctly updates flappingHistory while maintaining a fixed size', () => {
const flappingHistory = new Array(20).fill(false);
const fh = updateFlappingHistory(flappingHistory, true);
const fh = updateFlappingHistory(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true);
expect(fh.length).toEqual(20);
const result = new Array(19).fill(false);
expect(fh).toEqual(result.concat(true));
@ -24,27 +29,36 @@ describe('flapping utils', () => {
test('correctly updates flappingHistory while maintaining if array is larger than fixed size', () => {
const flappingHistory = new Array(23).fill(false);
const fh = updateFlappingHistory(flappingHistory, true);
const fh = updateFlappingHistory(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true);
expect(fh.length).toEqual(20);
const result = new Array(19).fill(false);
expect(fh).toEqual(result.concat(true));
});
test('does not update flappingHistory if flapping is disabled', () => {
const flappingHistory = updateFlappingHistory(
DISABLE_FLAPPING_SETTINGS,
[false, false],
true
);
expect(flappingHistory).toEqual([false, false]);
});
});
describe('atCapacity and getCapacityDiff functions', () => {
test('returns true if flappingHistory == set capacity', () => {
const flappingHistory = new Array(20).fill(false);
expect(atCapacity(flappingHistory)).toEqual(true);
expect(atCapacity(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true);
});
test('returns true if flappingHistory > set capacity', () => {
const flappingHistory = new Array(25).fill(false);
expect(atCapacity(flappingHistory)).toEqual(true);
expect(atCapacity(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true);
});
test('returns false if flappingHistory < set capacity', () => {
const flappingHistory = new Array(15).fill(false);
expect(atCapacity(flappingHistory)).toEqual(false);
expect(atCapacity(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(false);
});
});
@ -52,39 +66,46 @@ describe('flapping utils', () => {
describe('not currently flapping', () => {
test('returns true if at capacity and flap count exceeds the threshold', () => {
const flappingHistory = [true, true, true, true].concat(new Array(16).fill(false));
expect(isFlapping(flappingHistory)).toEqual(true);
expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true);
});
test("returns false if at capacity and flap count doesn't exceed the threshold", () => {
const flappingHistory = [true, true].concat(new Array(20).fill(false));
expect(isFlapping(flappingHistory)).toEqual(false);
expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(false);
});
test('returns true if not at capacity', () => {
const flappingHistory = new Array(5).fill(true);
expect(isFlapping(flappingHistory)).toEqual(true);
expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true);
});
});
describe('currently flapping', () => {
test('returns true if at capacity and the flap count exceeds the threshold', () => {
const flappingHistory = new Array(16).fill(false).concat([true, true, true, true]);
expect(isFlapping(flappingHistory, true)).toEqual(true);
expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(true);
});
test("returns true if not at capacity and the flap count doesn't exceed the threshold", () => {
const flappingHistory = new Array(16).fill(false);
expect(isFlapping(flappingHistory, true)).toEqual(true);
expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(true);
});
test('returns true if not at capacity and the flap count exceeds the threshold', () => {
const flappingHistory = new Array(10).fill(false).concat([true, true, true, true]);
expect(isFlapping(flappingHistory, true)).toEqual(true);
expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(true);
});
test("returns false if at capacity and the flap count doesn't exceed the threshold", () => {
const flappingHistory = new Array(20).fill(false);
expect(isFlapping(flappingHistory, true)).toEqual(false);
expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(false);
});
});
describe('flapping disabled', () => {
test('returns false if flapping is disabled', () => {
const flappingHistory = new Array(16).fill(false).concat([true, true, true, true]);
expect(isFlapping(DISABLE_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(false);
});
});
});

View file

@ -5,31 +5,46 @@
* 2.0.
*/
const MAX_CAPACITY = 20;
export const MAX_FLAP_COUNT = 4;
import { RulesSettingsFlappingProperties } from '../../common/rules_settings';
export function updateFlappingHistory(flappingHistory: boolean[], state: boolean) {
const updatedFlappingHistory = flappingHistory.concat(state).slice(MAX_CAPACITY * -1);
return updatedFlappingHistory;
export function updateFlappingHistory(
flappingSettings: RulesSettingsFlappingProperties,
flappingHistory: boolean[],
state: boolean
) {
if (flappingSettings.enabled) {
const updatedFlappingHistory = flappingHistory
.concat(state)
.slice(flappingSettings.lookBackWindow * -1);
return updatedFlappingHistory;
}
return flappingHistory;
}
export function isFlapping(
flappingSettings: RulesSettingsFlappingProperties,
flappingHistory: boolean[],
isCurrentlyFlapping: boolean = false
): boolean {
const numStateChanges = flappingHistory.filter((f) => f).length;
if (isCurrentlyFlapping) {
// if an alert is currently flapping,
// it will return false if the flappingHistory array is at capacity and there are 0 state changes
// else it will return true
return !(atCapacity(flappingHistory) && numStateChanges === 0);
} else {
// if an alert is not currently flapping,
// it will return true if the number of state changes in flappingHistory array >= the max flapping count
return numStateChanges >= MAX_FLAP_COUNT;
if (flappingSettings.enabled) {
const numStateChanges = flappingHistory.filter((f) => f).length;
if (isCurrentlyFlapping) {
// if an alert is currently flapping,
// it will return false if the flappingHistory array is at capacity and there are 0 state changes
// else it will return true
return !(atCapacity(flappingSettings, flappingHistory) && numStateChanges === 0);
} else {
// if an alert is not currently flapping,
// it will return true if the number of state changes in flappingHistory array >= the flapping status change threshold
return numStateChanges >= flappingSettings.statusChangeThreshold;
}
}
return false;
}
export function atCapacity(flappingHistory: boolean[] = []): boolean {
return flappingHistory.length >= MAX_CAPACITY;
export function atCapacity(
flappingSettings: RulesSettingsFlappingProperties,
flappingHistory: boolean[] = []
): boolean {
return flappingHistory.length >= flappingSettings.lookBackWindow;
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings';
import { getAlertsForNotification } from '.';
import { Alert } from '../alert';
@ -14,6 +15,7 @@ describe('getAlertsForNotification', () => {
const alert2 = new Alert('2', { meta: { flapping: false } });
const { newAlerts, activeAlerts } = getAlertsForNotification(
DEFAULT_FLAPPING_SETTINGS,
'default',
{
'1': alert1,
@ -66,6 +68,7 @@ describe('getAlertsForNotification', () => {
const { newAlerts, activeAlerts, recoveredAlerts, currentRecoveredAlerts } =
getAlertsForNotification(
DEFAULT_FLAPPING_SETTINGS,
'default',
{},
{},
@ -143,4 +146,117 @@ describe('getAlertsForNotification', () => {
}
`);
});
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',
{},
{},
{
'1': alert1,
'2': alert2,
'3': alert3,
},
{
'1': alert1,
'2': alert2,
'3': alert3,
}
);
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
expect(recoveredAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [
true,
false,
true,
],
"pendingRecoveredCount": 0,
},
"state": Object {},
},
"2": Object {
"meta": Object {
"flapping": false,
"flappingHistory": Array [
true,
false,
true,
],
"pendingRecoveredCount": 0,
},
"state": Object {},
},
"3": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [
true,
false,
true,
],
"pendingRecoveredCount": 0,
},
"state": Object {},
},
}
`);
expect(currentRecoveredAlerts).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [
true,
false,
true,
],
"pendingRecoveredCount": 0,
},
"state": Object {},
},
"2": Object {
"meta": Object {
"flapping": false,
"flappingHistory": Array [
true,
false,
true,
],
"pendingRecoveredCount": 0,
},
"state": Object {},
},
"3": Object {
"meta": Object {
"flapping": true,
"flappingHistory": Array [
true,
false,
true,
],
"pendingRecoveredCount": 0,
},
"state": Object {},
},
}
`);
});
});

View file

@ -6,9 +6,9 @@
*/
import { keys } from 'lodash';
import { RulesSettingsFlappingProperties } from '../../common/rules_settings';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext } from '../types';
import { MAX_FLAP_COUNT } from './flapping_utils';
export function getAlertsForNotification<
State extends AlertInstanceState,
@ -16,6 +16,7 @@ export function getAlertsForNotification<
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>(
flappingSettings: RulesSettingsFlappingProperties,
actionGroupId: string,
newAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
@ -29,34 +30,38 @@ export function getAlertsForNotification<
for (const id of keys(currentRecoveredAlerts)) {
const alert = recoveredAlerts[id];
const flapping = alert.getFlapping();
if (flapping) {
alert.incrementPendingRecoveredCount();
if (flappingSettings.enabled) {
const flapping = alert.getFlapping();
if (flapping) {
alert.incrementPendingRecoveredCount();
if (alert.getPendingRecoveredCount() < MAX_FLAP_COUNT) {
// keep the context and previous actionGroupId if available
const context = alert.getContext();
const lastActionGroupId = alert.getLastScheduledActions()?.group;
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);
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;
// schedule actions for the new active alert
newAlert.scheduleActions(
(lastActionGroupId ? lastActionGroupId : actionGroupId) as ActionGroupIds,
context
);
activeAlerts[id] = newAlert;
// remove from recovered alerts
delete recoveredAlerts[id];
delete currentRecoveredAlerts[id];
} else {
alert.resetPendingRecoveredCount();
// remove from recovered alerts
delete recoveredAlerts[id];
delete currentRecoveredAlerts[id];
} else {
alert.resetPendingRecoveredCount();
}
}
} else {
alert.resetPendingRecoveredCount();
}
}

View file

@ -10,6 +10,7 @@ import { cloneDeep } from 'lodash';
import { processAlerts, updateAlertFlappingHistory } 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;
@ -56,7 +57,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(newAlerts).toEqual({ '1': newAlert });
@ -94,7 +95,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(newAlerts).toEqual({ '1': newAlert1, '2': newAlert2 });
@ -140,7 +141,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -178,7 +179,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -226,7 +227,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -284,7 +285,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -345,7 +346,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(
@ -388,7 +389,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'] });
@ -416,7 +417,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({});
@ -446,7 +447,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] });
@ -485,7 +486,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] });
@ -524,7 +525,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual(updatedAlerts);
@ -554,7 +555,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: false,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({});
@ -600,7 +601,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: true,
alertLimit: 7,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(recoveredAlerts).toEqual({});
@ -636,7 +637,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: true,
alertLimit: 7,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toEqual({
@ -696,7 +697,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: true,
alertLimit: MAX_ALERTS,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(Object.keys(activeAlerts).length).toEqual(MAX_ALERTS);
@ -730,7 +731,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
@ -781,7 +782,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
@ -818,7 +819,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
@ -874,7 +875,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
@ -908,7 +909,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
@ -950,7 +951,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: false,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
@ -1017,7 +1018,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: true,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
@ -1054,7 +1055,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: true,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
@ -1116,7 +1117,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: true,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: true,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
@ -1193,7 +1194,7 @@ describe('processAlerts', () => {
hasReachedAlertLimit: true,
alertLimit: 10,
autoRecoverAlerts: true,
setFlapping: false,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
});
expect(activeAlerts).toMatchInlineSnapshot(`
@ -1240,7 +1241,7 @@ describe('processAlerts', () => {
const alert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory: [false, false] },
});
updateAlertFlappingHistory(alert, true);
updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true);
expect(alert.getFlappingHistory()).toEqual([false, false, true]);
});
@ -1249,7 +1250,7 @@ describe('processAlerts', () => {
const alert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory },
});
updateAlertFlappingHistory(alert, true);
updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true);
const fh = alert.getFlappingHistory() || [];
expect(fh.length).toEqual(20);
const result = new Array(19).fill(false);
@ -1261,7 +1262,7 @@ describe('processAlerts', () => {
const alert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
meta: { flappingHistory },
});
updateAlertFlappingHistory(alert, true);
updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true);
const fh = alert.getFlappingHistory() || [];
expect(fh.length).toEqual(20);
const result = new Array(19).fill(false);

View file

@ -10,6 +10,7 @@ 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,
@ -21,8 +22,7 @@ interface ProcessAlertsOpts<
hasReachedAlertLimit: boolean;
alertLimit: number;
autoRecoverAlerts: boolean;
// flag used to determine whether or not we want to push the flapping state on to the flappingHistory array
setFlapping: boolean;
flappingSettings: RulesSettingsFlappingProperties;
}
interface ProcessAlertsResult<
State extends AlertInstanceState,
@ -49,7 +49,7 @@ export function processAlerts<
hasReachedAlertLimit,
alertLimit,
autoRecoverAlerts,
setFlapping,
flappingSettings,
}: ProcessAlertsOpts<State, Context>): ProcessAlertsResult<
State,
Context,
@ -62,14 +62,14 @@ export function processAlerts<
existingAlerts,
previouslyRecoveredAlerts,
alertLimit,
setFlapping
flappingSettings
)
: processAlertsHelper(
alerts,
existingAlerts,
previouslyRecoveredAlerts,
autoRecoverAlerts,
setFlapping
flappingSettings
);
}
@ -83,7 +83,7 @@ function processAlertsHelper<
existingAlerts: Record<string, Alert<State, Context>>,
previouslyRecoveredAlerts: Record<string, Alert<State, Context>>,
autoRecoverAlerts: boolean,
setFlapping: boolean
flappingSettings: RulesSettingsFlappingProperties
): ProcessAlertsResult<State, Context, ActionGroupIds, RecoveryActionGroupId> {
const existingAlertIds = new Set(Object.keys(existingAlerts));
const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts));
@ -106,13 +106,13 @@ function processAlertsHelper<
const state = newAlerts[id].getState();
newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' });
if (setFlapping) {
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(newAlerts[id], true);
updateAlertFlappingHistory(flappingSettings, newAlerts[id], true);
}
} else {
// this alert did exist in previous run
@ -128,8 +128,8 @@ function processAlertsHelper<
});
// this alert is still active
if (setFlapping) {
updateAlertFlappingHistory(activeAlerts[id], false);
if (flappingSettings.enabled) {
updateAlertFlappingHistory(flappingSettings, activeAlerts[id], false);
}
}
} else if (existingAlertIds.has(id) && autoRecoverAlerts) {
@ -147,8 +147,8 @@ function processAlertsHelper<
...(state.start ? { end: currentTime } : {}),
});
// this alert has flapped from active to recovered
if (setFlapping) {
updateAlertFlappingHistory(recoveredAlerts[id], true);
if (flappingSettings.enabled) {
updateAlertFlappingHistory(flappingSettings, recoveredAlerts[id], true);
}
}
}
@ -157,8 +157,8 @@ function processAlertsHelper<
// alerts are still recovered
for (const id of previouslyRecoveredAlertsIds) {
recoveredAlerts[id] = previouslyRecoveredAlerts[id];
if (setFlapping) {
updateAlertFlappingHistory(recoveredAlerts[id], false);
if (flappingSettings.enabled) {
updateAlertFlappingHistory(flappingSettings, recoveredAlerts[id], false);
}
}
@ -175,7 +175,7 @@ function processAlertsLimitReached<
existingAlerts: Record<string, Alert<State, Context>>,
previouslyRecoveredAlerts: Record<string, Alert<State, Context>>,
alertLimit: number,
setFlapping: boolean
flappingSettings: RulesSettingsFlappingProperties
): ProcessAlertsResult<State, Context, ActionGroupIds, RecoveryActionGroupId> {
const existingAlertIds = new Set(Object.keys(existingAlerts));
const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts));
@ -210,8 +210,8 @@ function processAlertsLimitReached<
});
// this alert is still active
if (setFlapping) {
updateAlertFlappingHistory(activeAlerts[id], false);
if (flappingSettings.enabled) {
updateAlertFlappingHistory(flappingSettings, activeAlerts[id], false);
}
}
}
@ -236,12 +236,12 @@ function processAlertsLimitReached<
const state = newAlerts[id].getState();
newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' });
if (setFlapping) {
if (flappingSettings.enabled) {
if (previouslyRecoveredAlertsIds.has(id)) {
// this alert has flapped from recovered to active
newAlerts[id].setFlappingHistory(previouslyRecoveredAlerts[id].getFlappingHistory());
}
updateAlertFlappingHistory(newAlerts[id], true);
updateAlertFlappingHistory(flappingSettings, newAlerts[id], true);
}
if (!hasCapacityForNewAlerts()) {
@ -258,7 +258,15 @@ export function updateAlertFlappingHistory<
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>(alert: Alert<State, Context, ActionGroupIds | RecoveryActionGroupId>, state: boolean) {
const updatedFlappingHistory = updateFlappingHistory(alert.getFlappingHistory() || [], state);
>(
flappingSettings: RulesSettingsFlappingProperties,
alert: Alert<State, Context, ActionGroupIds | RecoveryActionGroupId>,
state: boolean
) {
const updatedFlappingHistory = updateFlappingHistory(
flappingSettings,
alert.getFlappingHistory() || [],
state
);
alert.setFlappingHistory(updatedFlappingHistory);
}

View file

@ -9,6 +9,7 @@ import { pick } from 'lodash';
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';
describe('setFlapping', () => {
const flapping = new Array(16).fill(false).concat([true, true, true, true]);
@ -29,7 +30,7 @@ describe('setFlapping', () => {
'4': new Alert('4', { meta: { flapping: true, flappingHistory: notFlapping } }),
};
setFlapping(activeAlerts, recoveredAlerts);
setFlapping(DEFAULT_FLAPPING_SETTINGS, activeAlerts, recoveredAlerts);
const fields = ['1.meta.flapping', '2.meta.flapping', '3.meta.flapping', '4.meta.flapping'];
expect(pick(activeAlerts, fields)).toMatchInlineSnapshot(`
Object {
@ -81,6 +82,73 @@ describe('setFlapping', () => {
`);
});
test('should set flapping to false on alerts when flapping is disabled', () => {
const activeAlerts = {
'1': new Alert('1', { meta: { flappingHistory: flapping } }),
'2': new Alert('2', { meta: { flappingHistory: [false, false] } }),
'3': new Alert('3', { meta: { flapping: true, flappingHistory: flapping } }),
'4': new Alert('4', { meta: { flapping: true, flappingHistory: [false, false] } }),
};
const recoveredAlerts = {
'1': new Alert('1', { meta: { flappingHistory: [true, true, true, true] } }),
'2': new Alert('2', { meta: { flappingHistory: notFlapping } }),
'3': new Alert('3', { meta: { flapping: true, flappingHistory: [true, true] } }),
'4': new Alert('4', { meta: { flapping: true, flappingHistory: notFlapping } }),
};
setFlapping(DISABLE_FLAPPING_SETTINGS, activeAlerts, recoveredAlerts);
const fields = ['1.meta.flapping', '2.meta.flapping', '3.meta.flapping', '4.meta.flapping'];
expect(pick(activeAlerts, fields)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flapping": false,
},
},
"2": Object {
"meta": Object {
"flapping": false,
},
},
"3": Object {
"meta": Object {
"flapping": false,
},
},
"4": Object {
"meta": Object {
"flapping": false,
},
},
}
`);
expect(pick(recoveredAlerts, fields)).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"flapping": false,
},
},
"2": Object {
"meta": Object {
"flapping": false,
},
},
"3": Object {
"meta": Object {
"flapping": false,
},
},
"4": Object {
"meta": Object {
"flapping": false,
},
},
}
`);
});
describe('isAlertFlapping', () => {
describe('not currently flapping', () => {
test('returns true if the flap count exceeds the threshold', () => {
@ -91,7 +159,7 @@ describe('setFlapping', () => {
meta: { flappingHistory },
}
);
expect(isAlertFlapping(alert)).toEqual(true);
expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true);
});
test("returns false the flap count doesn't exceed the threshold", () => {
@ -102,7 +170,7 @@ describe('setFlapping', () => {
meta: { flappingHistory },
}
);
expect(isAlertFlapping(alert)).toEqual(false);
expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(false);
});
test('returns true if not at capacity and the flap count exceeds the threshold', () => {
@ -113,7 +181,7 @@ describe('setFlapping', () => {
meta: { flappingHistory },
}
);
expect(isAlertFlapping(alert)).toEqual(true);
expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true);
});
});
@ -126,7 +194,7 @@ describe('setFlapping', () => {
meta: { flappingHistory, flapping: true },
}
);
expect(isAlertFlapping(alert)).toEqual(true);
expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true);
});
test("returns true if not at capacity and the flap count doesn't exceed the threshold", () => {
@ -137,7 +205,7 @@ describe('setFlapping', () => {
meta: { flappingHistory, flapping: true },
}
);
expect(isAlertFlapping(alert)).toEqual(true);
expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true);
});
test('returns true if not at capacity and the flap count exceeds the threshold', () => {
@ -148,7 +216,7 @@ describe('setFlapping', () => {
meta: { flappingHistory, flapping: true },
}
);
expect(isAlertFlapping(alert)).toEqual(true);
expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true);
});
test("returns false if at capacity and the flap count doesn't exceed the threshold", () => {
@ -159,7 +227,7 @@ describe('setFlapping', () => {
meta: { flappingHistory, flapping: true },
}
);
expect(isAlertFlapping(alert)).toEqual(false);
expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(false);
});
});
});

View file

@ -9,6 +9,7 @@ import { keys } from 'lodash';
import { Alert } from '../alert';
import { AlertInstanceState, AlertInstanceContext } from '../types';
import { isFlapping } from './flapping_utils';
import { RulesSettingsFlappingProperties } from '../../common/rules_settings';
export function setFlapping<
State extends AlertInstanceState,
@ -16,18 +17,19 @@ export function setFlapping<
ActionGroupIds extends string,
RecoveryActionGroupIds extends string
>(
flappingSettings: RulesSettingsFlappingProperties,
activeAlerts: Record<string, Alert<State, Context, ActionGroupIds>> = {},
recoveredAlerts: Record<string, Alert<State, Context, RecoveryActionGroupIds>> = {}
) {
for (const id of keys(activeAlerts)) {
const alert = activeAlerts[id];
const flapping = isAlertFlapping(alert);
const flapping = isAlertFlapping(flappingSettings, alert);
alert.setFlapping(flapping);
}
for (const id of keys(recoveredAlerts)) {
const alert = recoveredAlerts[id];
const flapping = isAlertFlapping(alert);
const flapping = isAlertFlapping(flappingSettings, alert);
alert.setFlapping(flapping);
}
}
@ -37,8 +39,13 @@ export function isAlertFlapping<
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
>(alert: Alert<State, Context, ActionGroupIds | RecoveryActionGroupId>): boolean {
>(
flappingSettings: RulesSettingsFlappingProperties,
alert: Alert<State, Context, ActionGroupIds | RecoveryActionGroupId>
): boolean {
const flappingHistory: boolean[] = alert.getFlappingHistory() || [];
const isCurrentlyFlapping = alert.getFlapping();
return isFlapping(flappingHistory, isCurrentlyFlapping);
return flappingSettings.enabled
? isFlapping(flappingSettings, flappingHistory, isCurrentlyFlapping)
: false;
}

View file

@ -464,6 +464,10 @@ export class AlertingPlugin {
return alertingAuthorizationClientFactory!.create(request);
};
const getRulesSettingsClientWithRequest = (request: KibanaRequest) => {
return rulesSettingsClientFactory!.create(request);
};
taskRunnerFactory.initialize({
logger,
data: plugins.data,
@ -488,6 +492,7 @@ export class AlertingPlugin {
maxAlerts: this.config.rules.run.alerts.max,
actionsConfigMap: getActionsConfigMap(this.config.rules.run.actions),
usageCounter: this.usageCounter,
getRulesSettingsClientWithRequest,
});
this.eventLogService!.registerSavedObjectProvider('alert', (request) => {

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { RulesSettingsClientApi, RulesSettingsFlappingClientApi } from './types';
import {
RulesSettingsClientApi,
RulesSettingsFlappingClientApi,
DEFAULT_FLAPPING_SETTINGS,
} from './types';
export type RulesSettingsClientMock = jest.Mocked<RulesSettingsClientApi>;
export type RulesSettingsFlappingClientMock = jest.Mocked<RulesSettingsFlappingClientApi>;
@ -14,7 +18,7 @@ export type RulesSettingsFlappingClientMock = jest.Mocked<RulesSettingsFlappingC
// the mock return value on the flapping
const createRulesSettingsClientMock = () => {
const flappingMocked: RulesSettingsFlappingClientMock = {
get: jest.fn(),
get: jest.fn().mockReturnValue(DEFAULT_FLAPPING_SETTINGS),
update: jest.fn(),
};
const mocked: RulesSettingsClientMock = {

View file

@ -76,6 +76,7 @@ import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_e
import { SharePluginStart } from '@kbn/share-plugin/server';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import { rulesSettingsClientMock } from '../rules_settings_client.mock';
jest.mock('uuid', () => ({
v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
@ -162,6 +163,7 @@ describe('Task Runner', () => {
max: 10000,
},
},
getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()),
};
const ephemeralTestParams: Array<
@ -209,6 +211,9 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) =>
fn()
);
taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue(
rulesSettingsClientMock.create()
);
mockedRuleTypeSavedObject.monitoring!.run.history = [];
mockedRuleTypeSavedObject.monitoring!.run.calculated_metrics.success_ratio = 0;

View file

@ -295,6 +295,8 @@ export class TaskRunner<
...wrappedClientOptions,
searchSourceClient,
});
const rulesSettingsClient = this.context.getRulesSettingsClientWithRequest(fakeRequest);
const flappingSettings = await rulesSettingsClient.flapping().get();
const { updatedRuleTypeState } = await this.timer.runWithTimer(
TaskRunnerTimerSpan.RuleTypeRun,
@ -373,6 +375,7 @@ export class TaskRunner<
notifyWhen,
},
logger: this.logger,
flappingSettings,
})
);
@ -418,6 +421,7 @@ export class TaskRunner<
ruleLabel,
ruleRunMetricsStore,
shouldLogAndScheduleActionsForAlerts: this.shouldLogAndScheduleActionsForAlerts(),
flappingSettings,
});
});

View file

@ -53,6 +53,7 @@ import { EVENT_LOG_ACTIONS } from '../plugin';
import { SharePluginStart } from '@kbn/share-plugin/server';
import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { rulesSettingsClientMock } from '../rules_settings_client.mock';
jest.mock('uuid', () => ({
v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
@ -138,6 +139,7 @@ describe('Task Runner Cancel', () => {
max: 1000,
},
},
getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()),
};
beforeEach(() => {
@ -165,6 +167,9 @@ describe('Task Runner Cancel', () => {
taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) =>
fn()
);
taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue(
rulesSettingsClientMock.create()
);
rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({

View file

@ -29,6 +29,7 @@ import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
import { SharePluginStart } from '@kbn/share-plugin/server';
import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { rulesSettingsClientMock } from '../rules_settings_client.mock';
const inMemoryMetrics = inMemoryMetricsMock.create();
const executionContext = executionContextServiceMock.createSetupContract();
@ -115,6 +116,7 @@ describe('Task Runner Factory', () => {
max: 1000,
},
},
getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()),
};
beforeEach(() => {

View file

@ -31,6 +31,7 @@ import {
AlertInstanceState,
AlertInstanceContext,
RulesClientApi,
RulesSettingsClientApi,
} from '../types';
import { TaskRunner } from './task_runner';
import { NormalizedRuleType } from '../rule_type_registry';
@ -61,6 +62,7 @@ export interface TaskRunnerContext {
actionsConfigMap: ActionsConfigMap;
cancelAlertsOnRuleTimeout: boolean;
usageCounter?: UsageCounter;
getRulesSettingsClientWithRequest(request: KibanaRequest): RulesSettingsClientApi;
}
export class TaskRunnerFactory {

View file

@ -51,6 +51,7 @@ import {
} from '../common';
import { PublicAlertFactory } from './alert/create_alert_factory';
import { FieldMap } from '../common/alert_schema/field_maps/types';
import { RulesSettingsFlappingProperties } from '../common/rules_settings';
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
export type { RuleTypeParams };
@ -111,6 +112,7 @@ export interface RuleExecutorOptions<
startedAt: Date;
state: State;
namespace?: string;
flappingSettings: RulesSettingsFlappingProperties;
}
export interface RuleParamsAndRefs<Params extends RuleTypeParams> {

View file

@ -12,6 +12,7 @@ import { IRuleDataClient } from '@kbn/rule-registry-plugin/server';
import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks';
import { PluginSetupContract as AlertingPluginSetupContract } from '@kbn/alerting-plugin/server';
import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common';
import { APMConfig, APM_SERVER_FEATURE_ID } from '../../..';
export const createRuleTypeMocks = () => {
@ -80,6 +81,7 @@ export const createRuleTypeMocks = () => {
ruleTypeName: 'ruleTypeName',
},
startedAt: new Date(),
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
},
};

View file

@ -33,6 +33,7 @@ import {
} from './metric_threshold_executor';
import { Evaluation } from './lib/evaluate_rule';
import type { LogMeta, Logger } from '@kbn/logging';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common';
jest.mock('./lib/evaluate_rule', () => ({ evaluateRule: jest.fn() }));
@ -116,6 +117,7 @@ const mockOptions = {
ruleTypeName: '',
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
};
const setEvaluationResults = (response: Array<Record<string, Evaluation>>) => {

View file

@ -19,6 +19,7 @@ import { ISearchStartSearchSource } from '@kbn/data-plugin/public';
import { MockedLogger } from '@kbn/logging-mocks';
import { SanitizedRuleConfig } from '@kbn/alerting-plugin/common';
import { Alert, RuleExecutorServices } from '@kbn/alerting-plugin/server';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings';
import {
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
@ -118,6 +119,7 @@ describe('BurnRateRuleExecutor', () => {
rule: {} as SanitizedRuleConfig,
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(alertWithLifecycleMock).not.toBeCalled();
@ -142,6 +144,7 @@ describe('BurnRateRuleExecutor', () => {
rule: {} as SanitizedRuleConfig,
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(alertWithLifecycleMock).not.toBeCalled();
@ -166,6 +169,7 @@ describe('BurnRateRuleExecutor', () => {
rule: {} as SanitizedRuleConfig,
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(alertWithLifecycleMock).not.toBeCalled();
@ -195,6 +199,7 @@ describe('BurnRateRuleExecutor', () => {
rule: {} as SanitizedRuleConfig,
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(alertWithLifecycleMock).toBeCalledWith({
@ -242,6 +247,7 @@ describe('BurnRateRuleExecutor', () => {
rule: {} as SanitizedRuleConfig,
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(alertWithLifecycleMock).not.toBeCalled();

View file

@ -162,6 +162,7 @@ export const createLifecycleExecutor =
const {
services: { alertFactory, shouldWriteAlerts },
state: previousState,
flappingSettings,
} = options;
const ruleDataClientWriter = await ruleDataClient.getWriter();
@ -266,6 +267,7 @@ export const createLifecycleExecutor =
const isActive = !isRecovered;
const flappingHistory = getUpdatedFlappingHistory<State>(
flappingSettings,
alertId,
state,
isNew,
@ -290,7 +292,7 @@ export const createLifecycleExecutor =
pendingRecoveredCount: 0,
};
const flapping = isFlapping(flappingHistory, isCurrentlyFlapping);
const flapping = isFlapping(flappingSettings, flappingHistory, isCurrentlyFlapping);
const event: ParsedTechnicalFields & ParsedExperimentalFields = {
...alertData?.fields,
@ -329,7 +331,7 @@ export const createLifecycleExecutor =
const newEventsToIndex = makeEventsDataMapFor(newAlertIds);
const trackedRecoveredEventsToIndex = makeEventsDataMapFor(trackedAlertRecoveredIds);
const allEventsToIndex = [
...getAlertsForNotification(trackedEventsToIndex),
...getAlertsForNotification(flappingSettings, trackedEventsToIndex),
...newEventsToIndex,
];

View file

@ -22,6 +22,7 @@ import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_fac
import { ISearchStartSearchSource } from '@kbn/data-plugin/common';
import { SharePluginStart } from '@kbn/share-plugin/server';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings';
type RuleTestHelpers = ReturnType<typeof createRule>;
@ -138,6 +139,7 @@ function createRule(shouldWriteAlerts: boolean = true) {
spaceId: 'spaceId',
startedAt,
state,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
})) ?? {}) as Record<string, any>);
previousStartedAt = startedAt;

View file

@ -5,7 +5,12 @@
* 2.0.
*/
import {
DEFAULT_FLAPPING_SETTINGS,
DISABLE_FLAPPING_SETTINGS,
} from '@kbn/alerting-plugin/common/rules_settings';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import { cloneDeep } from 'lodash';
import { getAlertsForNotification } from './get_alerts_for_notification';
describe('getAlertsForNotification', () => {
@ -38,7 +43,8 @@ describe('getAlertsForNotification', () => {
test('should set pendingRecoveredCount to zero for all active alerts', () => {
const trackedEvents = [alert4];
expect(getAlertsForNotification(trackedEvents)).toMatchInlineSnapshot(`
expect(getAlertsForNotification(DEFAULT_FLAPPING_SETTINGS, trackedEvents))
.toMatchInlineSnapshot(`
Array [
Object {
"event": Object {
@ -55,8 +61,9 @@ describe('getAlertsForNotification', () => {
});
test('should not remove alerts if the num of recovered alerts is not at the limit', () => {
const trackedEvents = [alert1, alert2, alert3];
expect(getAlertsForNotification(trackedEvents)).toMatchInlineSnapshot(`
const trackedEvents = cloneDeep([alert1, alert2, alert3]);
expect(getAlertsForNotification(DEFAULT_FLAPPING_SETTINGS, trackedEvents))
.toMatchInlineSnapshot(`
Array [
Object {
"event": Object {
@ -82,4 +89,34 @@ describe('getAlertsForNotification', () => {
]
`);
});
test('should reset counts and not modify alerts if flapping is disabled', () => {
const trackedEvents = cloneDeep([alert1, alert2, alert3]);
expect(getAlertsForNotification(DISABLE_FLAPPING_SETTINGS, trackedEvents))
.toMatchInlineSnapshot(`
Array [
Object {
"event": Object {
"kibana.alert.status": "recovered",
},
"flapping": true,
"pendingRecoveredCount": 0,
},
Object {
"event": Object {
"kibana.alert.status": "recovered",
},
"flapping": false,
"pendingRecoveredCount": 0,
},
Object {
"event": Object {
"kibana.alert.status": "recovered",
},
"flapping": true,
"pendingRecoveredCount": 0,
},
]
`);
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { MAX_FLAP_COUNT } from '@kbn/alerting-plugin/server/lib/flapping_utils';
import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common/rules_settings';
import {
ALERT_END,
ALERT_STATUS,
@ -14,15 +14,21 @@ import {
EVENT_ACTION,
} from '@kbn/rule-data-utils';
export function getAlertsForNotification(trackedEventsToIndex: any[]) {
export function getAlertsForNotification(
flappingSettings: RulesSettingsFlappingProperties,
trackedEventsToIndex: any[]
) {
return trackedEventsToIndex.map((trackedEvent) => {
if (trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_ACTIVE) {
if (!flappingSettings.enabled || trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_ACTIVE) {
trackedEvent.pendingRecoveredCount = 0;
} else if (trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_RECOVERED) {
} else if (
flappingSettings.enabled &&
trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_RECOVERED
) {
if (trackedEvent.flapping) {
const count = trackedEvent.pendingRecoveredCount || 0;
trackedEvent.pendingRecoveredCount = count + 1;
if (trackedEvent.pendingRecoveredCount < MAX_FLAP_COUNT) {
if (trackedEvent.pendingRecoveredCount < flappingSettings.statusChangeThreshold) {
trackedEvent.event[ALERT_STATUS] = ALERT_STATUS_ACTIVE;
trackedEvent.event[EVENT_ACTION] = 'active';
delete trackedEvent.event[ALERT_END];

View file

@ -5,6 +5,10 @@
* 2.0.
*/
import {
DEFAULT_FLAPPING_SETTINGS,
DISABLE_FLAPPING_SETTINGS,
} from '@kbn/alerting-plugin/common/rules_settings';
import { getUpdatedFlappingHistory } from './get_updated_flapping_history';
describe('getUpdatedFlappingHistory', () => {
@ -17,8 +21,17 @@ describe('getUpdatedFlappingHistory', () => {
test('sets flapping state to true if the alert is new', () => {
const state = { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} };
expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, true, false, false, []))
.toMatchInlineSnapshot(`
expect(
getUpdatedFlappingHistory(
DEFAULT_FLAPPING_SETTINGS,
'TEST_ALERT_0',
state,
true,
false,
false,
[]
)
).toMatchInlineSnapshot(`
Array [
true,
]
@ -40,8 +53,17 @@ describe('getUpdatedFlappingHistory', () => {
},
trackedAlertsRecovered: {},
};
expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, false, false, true, []))
.toMatchInlineSnapshot(`
expect(
getUpdatedFlappingHistory(
DEFAULT_FLAPPING_SETTINGS,
'TEST_ALERT_0',
state,
false,
false,
true,
[]
)
).toMatchInlineSnapshot(`
Array [
false,
]
@ -64,8 +86,17 @@ describe('getUpdatedFlappingHistory', () => {
trackedAlerts: {},
};
const recoveredIds = ['TEST_ALERT_0'];
expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, true, false, true, recoveredIds))
.toMatchInlineSnapshot(`
expect(
getUpdatedFlappingHistory(
DEFAULT_FLAPPING_SETTINGS,
'TEST_ALERT_0',
state,
true,
false,
true,
recoveredIds
)
).toMatchInlineSnapshot(`
Array [
true,
]
@ -89,8 +120,17 @@ describe('getUpdatedFlappingHistory', () => {
trackedAlertsRecovered: {},
};
const recoveredIds = ['TEST_ALERT_0'];
expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, false, true, false, recoveredIds))
.toMatchInlineSnapshot(`
expect(
getUpdatedFlappingHistory(
DEFAULT_FLAPPING_SETTINGS,
'TEST_ALERT_0',
state,
false,
true,
false,
recoveredIds
)
).toMatchInlineSnapshot(`
Array [
true,
]
@ -98,7 +138,7 @@ describe('getUpdatedFlappingHistory', () => {
expect(recoveredIds).toEqual(['TEST_ALERT_0']);
});
test('sets flapping state to true on an alert that is still recovered', () => {
test('sets flapping state to false on an alert that is still recovered', () => {
const state = {
wrapped: initialRuleState,
trackedAlerts: {},
@ -114,12 +154,49 @@ describe('getUpdatedFlappingHistory', () => {
},
};
const recoveredIds = ['TEST_ALERT_0'];
expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, false, true, false, recoveredIds))
.toMatchInlineSnapshot(`
expect(
getUpdatedFlappingHistory(
DEFAULT_FLAPPING_SETTINGS,
'TEST_ALERT_0',
state,
false,
true,
false,
recoveredIds
)
).toMatchInlineSnapshot(`
Array [
false,
]
`);
expect(recoveredIds).toEqual(['TEST_ALERT_0']);
});
test('does not set flapping state if flapping is not enabled', () => {
const state = {
wrapped: initialRuleState,
trackedAlerts: {},
trackedAlertsRecovered: {
TEST_ALERT_0: {
alertId: 'TEST_ALERT_0',
alertUuid: 'TEST_ALERT_0_UUID',
started: '2020-01-01T12:00:00.000Z',
flappingHistory: [],
flapping: false,
pendingRecoveredCount: 0,
},
},
};
expect(
getUpdatedFlappingHistory(
DISABLE_FLAPPING_SETTINGS,
'TEST_ALERT_0',
state,
false,
true,
false,
['TEST_ALERT_0']
)
).toMatchInlineSnapshot(`Array []`);
});
});

View file

@ -6,11 +6,13 @@
*/
import { RuleTypeState } from '@kbn/alerting-plugin/common';
import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common/rules_settings';
import { updateFlappingHistory } from '@kbn/alerting-plugin/server/lib';
import { remove } from 'lodash';
import { WrappedLifecycleRuleState } from './create_lifecycle_executor';
export function getUpdatedFlappingHistory<State extends RuleTypeState = never>(
flappingSettings: RulesSettingsFlappingProperties,
alertId: string,
state: WrappedLifecycleRuleState<State>,
isNew: boolean,
@ -20,31 +22,43 @@ export function getUpdatedFlappingHistory<State extends RuleTypeState = never>(
) {
// duplicating this logic to determine flapping at this level
let flappingHistory: boolean[] = [];
if (isRecovered) {
if (state.trackedAlerts[alertId]) {
// this alert has flapped from active to recovered
flappingHistory = updateFlappingHistory(state.trackedAlerts[alertId].flappingHistory, true);
} else if (state.trackedAlertsRecovered[alertId]) {
// this alert is still recovered
if (flappingSettings.enabled) {
if (isRecovered) {
if (state.trackedAlerts[alertId]) {
// this alert has flapped from active to recovered
flappingHistory = updateFlappingHistory(
flappingSettings,
state.trackedAlerts[alertId].flappingHistory,
true
);
} else if (state.trackedAlertsRecovered[alertId]) {
// this alert is still recovered
flappingHistory = updateFlappingHistory(
flappingSettings,
state.trackedAlertsRecovered[alertId].flappingHistory,
false
);
}
} else if (isNew) {
if (state.trackedAlertsRecovered[alertId]) {
// this alert has flapped from recovered to active
flappingHistory = updateFlappingHistory(
flappingSettings,
state.trackedAlertsRecovered[alertId].flappingHistory,
true
);
remove(recoveredIds, (id) => id === alertId);
} else {
flappingHistory = updateFlappingHistory(flappingSettings, [], true);
}
} else if (isActive) {
// this alert is still active
flappingHistory = updateFlappingHistory(
state.trackedAlertsRecovered[alertId].flappingHistory,
flappingSettings,
state.trackedAlerts[alertId].flappingHistory,
false
);
}
} else if (isNew) {
if (state.trackedAlertsRecovered[alertId]) {
// this alert has flapped from recovered to active
flappingHistory = updateFlappingHistory(
state.trackedAlertsRecovered[alertId].flappingHistory,
true
);
remove(recoveredIds, (id) => id === alertId);
} else {
flappingHistory = updateFlappingHistory([], true);
}
} else if (isActive) {
// this alert is still active
flappingHistory = updateFlappingHistory(state.trackedAlerts[alertId].flappingHistory, false);
}
return flappingHistory;
}

View file

@ -21,6 +21,7 @@ import { searchSourceCommonMock } from '@kbn/data-plugin/common/search/search_so
import { Logger } from '@kbn/logging';
import { SharePluginStart } from '@kbn/share-plugin/server';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings';
export const createDefaultAlertExecutorOptions = <
Params extends RuleTypeParams = never,
@ -87,4 +88,5 @@ export const createDefaultAlertExecutorOptions = <
namespace: undefined,
executionId: 'b33f65d7-6e8b-4aae-8d20-c93613deb33f',
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});

View file

@ -8,6 +8,7 @@
import { loggingSystemMock } from '@kbn/core/server/mocks';
import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks';
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common';
import { getRuleMock } from '../../../routes/__mocks__/request_responses';
// eslint-disable-next-line no-restricted-imports
@ -67,6 +68,7 @@ describe('legacyRules_notification_alert_type', () => {
notifyWhen: null,
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
};
alert = legacyRulesNotificationAlertType({

View file

@ -15,7 +15,7 @@ import type {
AlertInstanceState,
RuleTypeState,
} from '@kbn/alerting-plugin/common';
import { parseDuration } from '@kbn/alerting-plugin/common';
import { parseDuration, DISABLE_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common';
import type { ExecutorType } from '@kbn/alerting-plugin/server/types';
import type { Alert } from '@kbn/alerting-plugin/server';
@ -263,6 +263,7 @@ export const previewRulesRoute = async (
startedAt: startedAt.toDate(),
state: statePreview,
logger,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
})) as { state: TState });
const errors = loggedStatusChanges

View file

@ -24,6 +24,7 @@ import { ActionGroupId, ConditionMetAlertInstanceId } from './constants';
import { OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types';
import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
import { Comparator } from '../../../common/comparator_types';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings';
const logger = loggingSystemMock.create().get();
const coreSetup = coreMock.createSetup();
@ -726,5 +727,6 @@ async function invokeExecutor({
notifyWhen: null,
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
}

View file

@ -16,6 +16,7 @@ import { Params } from './rule_type_params';
import { TIME_SERIES_BUCKET_SELECTOR_FIELD } from '@kbn/triggers-actions-ui-plugin/server';
import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks';
import { Comparator } from '../../../common/comparator_types';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings';
let fakeTimer: sinon.SinonFakeTimers;
@ -217,6 +218,7 @@ describe('ruleType', () => {
notifyWhen: null,
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(alertServices.alertFactory.create).toHaveBeenCalledWith('all documents');
@ -280,6 +282,7 @@ describe('ruleType', () => {
notifyWhen: null,
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled();
@ -343,6 +346,7 @@ describe('ruleType', () => {
notifyWhen: null,
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled();
@ -405,6 +409,7 @@ describe('ruleType', () => {
notifyWhen: null,
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
});
expect(data.timeSeriesQuery).toHaveBeenCalledWith(

View file

@ -19,3 +19,4 @@ export * from './test_assertions';
export { checkAAD } from './check_aad';
export { getEventLog } from './get_event_log';
export { createWaitForExecutionCount } from './wait_for_execution_count';
export { resetRulesSettings } from './reset_rules_settings';

View file

@ -0,0 +1,19 @@
/*
* 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 } from '@kbn/alerting-plugin/common';
import { Superuser } from '../../security_and_spaces/scenarios';
import { getUrlPrefix } from './space_test_utils';
export const resetRulesSettings = (supertest: any, space: string) => {
return supertest
.post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.auth(Superuser.username, Superuser.password)
.send(DEFAULT_FLAPPING_SETTINGS)
.expect(200);
};

View file

@ -8,7 +8,7 @@
import expect from '@kbn/expect';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common';
import { UserAtSpaceScenarios } from '../../../scenarios';
import { getUrlPrefix } from '../../../../common/lib';
import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
@ -16,6 +16,16 @@ export default function getFlappingSettingsTests({ getService }: FtrProviderCont
const supertestWithoutAuth = getService('supertestWithoutAuth');
describe('getFlappingSettings', () => {
beforeEach(async () => {
await resetRulesSettings(supertestWithoutAuth, 'space1');
await resetRulesSettings(supertestWithoutAuth, 'space2');
});
after(async () => {
await resetRulesSettings(supertestWithoutAuth, 'space1');
await resetRulesSettings(supertestWithoutAuth, 'space2');
});
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {

View file

@ -6,19 +6,10 @@
*/
import expect from '@kbn/expect';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common';
import { UserAtSpaceScenarios, Superuser } from '../../../scenarios';
import { getUrlPrefix } from '../../../../common/lib';
import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
const resetRulesSettings = (supertestWithoutAuth: any, space: string) => {
return supertestWithoutAuth
.post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.auth(Superuser.username, Superuser.password)
.send(DEFAULT_FLAPPING_SETTINGS);
};
// eslint-disable-next-line import/no-default-export
export default function updateFlappingSettingsTest({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');

View file

@ -9,7 +9,13 @@ import expect from '@kbn/expect';
import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server';
import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers';
import { Spaces } from '../../../scenarios';
import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib';
import {
getUrlPrefix,
getTestRuleData,
ObjectRemover,
getEventLog,
resetRulesSettings,
} from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
@ -23,10 +29,17 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
const objectRemover = new ObjectRemover(supertest);
beforeEach(async () => {
await resetRulesSettings(supertest, Spaces.default.id);
await resetRulesSettings(supertest, Spaces.space1.id);
await esTestIndexTool.destroy();
await esTestIndexTool.setup();
});
after(async () => {
await resetRulesSettings(supertest, Spaces.default.id);
await resetRulesSettings(supertest, Spaces.space1.id);
});
afterEach(async () => {
await objectRemover.removeAll();
});
@ -527,6 +540,16 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
});
it('should generate expected events for flapping alerts that are mainly active', async () => {
await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.auth('superuser', 'superuser')
.send({
enabled: true,
lookBackWindow: 3,
statusChangeThreshold: 2,
})
.expect(200);
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
@ -539,7 +562,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
.expect(200);
// pattern of when the alert should fire
const instance = [true, false, true, false].concat(new Array(22).fill(true));
const instance = [true, false, true, true, true, true, true];
const pattern = {
instance,
};
@ -579,11 +602,95 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
provider: 'alerting',
actions: new Map([
// make sure the counts of the # of events per type are as expected
['execute-start', { gte: 25 }],
['execute', { gte: 25 }],
['execute-action', { equal: 25 }],
['execute-start', { gte: 6 }],
['execute', { gte: 6 }],
['execute-action', { equal: 7 }],
['new-instance', { equal: 1 }],
['active-instance', { gte: 6 }],
['recovered-instance', { equal: 1 }],
]),
});
});
const flapping = events
.filter(
(event) =>
event?.event?.action === 'active-instance' ||
event?.event?.action === 'recovered-instance'
)
.map((event) => event?.kibana?.alert?.flapping);
const result = [false, true, true, true, false, false, false, false];
expect(flapping).to.eql(result);
});
it('should generate expected events for flapping alerts that are mainly recovered', async () => {
await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.auth('superuser', 'superuser')
.send({
enabled: true,
lookBackWindow: 3,
statusChangeThreshold: 2,
})
.expect(200);
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'MY action',
connector_type_id: 'test.noop',
config: {},
secrets: {},
})
.expect(200);
// pattern of when the alert should fire
const instance = [true, false, true, false, false, false, true];
const pattern = {
instance,
};
const response = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.patternFiring',
schedule: { interval: '1s' },
throttle: null,
params: {
pattern,
},
actions: [
{
id: createdAction.id,
group: 'default',
params: {},
},
],
})
);
expect(response.status).to.eql(200);
const alertId = response.body.id;
objectRemover.add(space.id, alertId, 'rule', 'alerting');
// get the events we're expecting
const events = await retry.try(async () => {
return await getEventLog({
getService,
spaceId: space.id,
type: 'alert',
id: alertId,
provider: 'alerting',
actions: new Map([
// make sure the counts of the # of events per type are as expected
['execute-start', { gte: 6 }],
['execute', { gte: 6 }],
['execute-action', { equal: 6 }],
['new-instance', { equal: 2 }],
['active-instance', { gte: 25 }],
['active-instance', { gte: 6 }],
['recovered-instance', { equal: 2 }],
]),
});
@ -596,96 +703,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
event?.event?.action === 'recovered-instance'
)
.map((event) => event?.kibana?.alert?.flapping);
const result = [false, false, false]
.concat(new Array(20).fill(true))
.concat([false, false, false, false]);
expect(flapping).to.eql(result);
});
it('should generate expected events for flapping alerts that are mainly recovered', async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'MY action',
connector_type_id: 'test.noop',
config: {},
secrets: {},
})
.expect(200);
// pattern of when the alert should fire
const instance = [true, false, true].concat(new Array(18).fill(false)).concat(true);
const pattern = {
instance,
};
const response = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.patternFiring',
schedule: { interval: '1s' },
throttle: null,
params: {
pattern,
},
actions: [
{
id: createdAction.id,
group: 'default',
params: {},
},
],
})
);
expect(response.status).to.eql(200);
const alertId = response.body.id;
objectRemover.add(space.id, alertId, 'rule', 'alerting');
// get the events we're expecting
const events = await retry.try(async () => {
return await getEventLog({
getService,
spaceId: space.id,
type: 'alert',
id: alertId,
provider: 'alerting',
actions: new Map([
// make sure the counts of the # of events per type are as expected
['execute-start', { gte: 20 }],
['execute', { gte: 20 }],
['execute-action', { equal: 9 }],
['new-instance', { equal: 3 }],
['active-instance', { gte: 9 }],
['recovered-instance', { equal: 3 }],
]),
});
});
const flapping = events
.filter(
(event) =>
event?.event?.action === 'active-instance' ||
event?.event?.action === 'recovered-instance'
)
.map((event) => event?.kibana?.alert?.flapping);
expect(flapping).to.eql([
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
]);
expect(flapping).to.eql([false, true, true, true, true, true, true, true]);
});
});
}

View file

@ -28,6 +28,7 @@ import {
RuleDataService,
} from '@kbn/rule-registry-plugin/server';
import { RuleExecutorOptions } from '@kbn/alerting-plugin/server';
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings';
import { get } from 'lodash';
import type { FtrProviderContext } from '../../../common/ftr_provider_context';
import {
@ -171,6 +172,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide
alertFactory: { create: sinon.stub() },
shouldWriteAlerts: sinon.stub().returns(true),
},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
} as unknown as RuleExecutorOptions<
MockRuleParams,
WrappedLifecycleRuleState<{ shouldTriggerAlert: boolean }>,
@ -329,6 +331,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide
alertFactory: { create: sinon.stub() },
shouldWriteAlerts: sinon.stub().returns(true),
},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
} as unknown as RuleExecutorOptions<
MockRuleParams,
WrappedLifecycleRuleState<{ shouldTriggerAlert: boolean }>,