mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ResponseOps][BE] Alert creation delay based on user definition (#174657)
Related to https://github.com/elastic/kibana/issues/173009
## Summary
This is the first of two PRs and only focuses on the backend
implementation. This PR adds a new `notificationDelay` field to the
`Rule` object. With the delay the rule will run X times and has to match
the threshold X times before triggering actions. It won't affect the
alert recovery, but it can be expanded on easily if we want to include
recovered alerts in the future.
### Checklist
- [ ] [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
- Use [Dev Tools](http://localhost:5601/app/dev_tools#/console) to
create a rule with the `notificationDelay`
```
POST kbn:/api/alerting/rule
{
"params": {
"searchType": "esQuery",
"timeWindowSize": 5,
"timeWindowUnit": "m",
"threshold": [
-1
],
"thresholdComparator": ">",
"size": 100,
"esQuery": """{
"query":{
"match_all" : {}
}
}""",
"aggType": "count",
"groupBy": "all",
"termSize": 5,
"excludeHitsFromPreviousRun": false,
"sourceFields": [],
"index": [
".kibana-event-log*"
],
"timeField": "@timestamp"
},
"consumer": "stackAlerts",
"schedule": {
"interval": "1m"
},
"tags": [],
"name": "test",
"rule_type_id": ".es-query",
"actions": [
{
"group": "query matched",
"id": "${ACTION_ID}",
"params": {
"level": "info",
"message": """Elasticsearch query rule '{{rule.name}}' is active:
- Value: {{context.value}}
- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}
- Timestamp: {{context.date}}
- Link: {{context.link}}"""
},
"frequency": {
"notify_when": "onActionGroupChange",
"throttle": null,
"summary": false
}
}
],
"notification_delay": {
"active": 3
}
}
```
- Verify that the rule will not trigger actions until it has matched the
delay threshold. It might be helpful to look at rule details page and
add the Triggered actions column to easily see the action was triggered
after X consecutive active alerts
<img width="1420" alt="Screen Shot 2024-01-16 at 1 18 52 PM"
src="85d8ceef
-042c-4a52-950e-24492dc0e79f">
- Verify that the delay does not affect recovered alerts
This commit is contained in:
parent
3e9cc8d692
commit
80640cf1e5
33 changed files with 817 additions and 11 deletions
|
@ -17,6 +17,8 @@ const trackedAlertStateRt = t.type({
|
|||
flappingHistory: t.array(t.boolean),
|
||||
// flapping flag that indicates whether the alert is flapping
|
||||
flapping: t.boolean,
|
||||
// count of consecutive recovered alerts for flapping
|
||||
// will reset if the alert is active or if equal to the statusChangeThreshold stored in the rule settings
|
||||
pendingRecoveredCount: t.number,
|
||||
});
|
||||
|
||||
|
|
|
@ -32,8 +32,13 @@ export const metaSchema = schema.object({
|
|||
// flapping flag that indicates whether the alert is flapping
|
||||
flapping: schema.maybe(schema.boolean()),
|
||||
maintenanceWindowIds: schema.maybe(schema.arrayOf(schema.string())),
|
||||
// count of consecutive recovered alerts for flapping
|
||||
// will reset if the alert is active or if equal to the statusChangeThreshold stored in the rule settings
|
||||
pendingRecoveredCount: schema.maybe(schema.number()),
|
||||
uuid: schema.maybe(schema.string()),
|
||||
// count of consecutive active alerts
|
||||
// will reset if the alert is recovered or if equal to notificationDelay.active stored in the rule
|
||||
activeCount: schema.maybe(schema.number()),
|
||||
});
|
||||
|
||||
export const rawAlertInstanceSchema = schema.object({
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { validateDurationV1, validateHoursV1, validateTimezoneV1 } from '../../../validation';
|
||||
import { notifyWhenSchemaV1 } from '../../../response';
|
||||
import { notifyWhenSchemaV1, notificationDelaySchemaV1 } from '../../../response';
|
||||
import { alertsFilterQuerySchemaV1 } from '../../../../alerts_filter_query';
|
||||
|
||||
export const actionFrequencySchema = schema.object({
|
||||
|
@ -68,6 +68,7 @@ export const createBodySchema = schema.object({
|
|||
}),
|
||||
actions: schema.arrayOf(actionSchema, { defaultValue: [] }),
|
||||
notify_when: schema.maybe(schema.nullable(notifyWhenSchemaV1)),
|
||||
notification_delay: schema.maybe(notificationDelaySchemaV1),
|
||||
});
|
||||
|
||||
export const createParamsSchema = schema.object({
|
||||
|
|
|
@ -30,6 +30,7 @@ export interface CreateRuleRequestBody<Params extends RuleParamsV1 = never> {
|
|||
schedule: CreateBodySchema['schedule'];
|
||||
actions: CreateBodySchema['actions'];
|
||||
notify_when?: CreateBodySchema['notify_when'];
|
||||
notification_delay?: CreateBodySchema['notification_delay'];
|
||||
}
|
||||
|
||||
export interface CreateRuleResponse<Params extends RuleParamsV1 = never> {
|
||||
|
|
|
@ -37,6 +37,7 @@ export {
|
|||
ruleSnoozeScheduleSchema as ruleSnoozeScheduleSchemaV1,
|
||||
notifyWhenSchema as notifyWhenSchemaV1,
|
||||
scheduleIdsSchema as scheduleIdsSchemaV1,
|
||||
notificationDelaySchema as notificationDelaySchemaV1,
|
||||
} from './schemas/v1';
|
||||
|
||||
export type {
|
||||
|
|
|
@ -182,6 +182,10 @@ export const ruleSnoozeScheduleSchema = schema.object({
|
|||
skipRecurrences: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
||||
export const notificationDelaySchema = schema.object({
|
||||
active: schema.number(),
|
||||
});
|
||||
|
||||
export const ruleResponseSchema = schema.object({
|
||||
id: schema.string(),
|
||||
enabled: schema.boolean(),
|
||||
|
@ -214,6 +218,7 @@ export const ruleResponseSchema = schema.object({
|
|||
revision: schema.number(),
|
||||
running: schema.maybe(schema.nullable(schema.boolean())),
|
||||
view_in_app_relative_url: schema.maybe(schema.nullable(schema.string())),
|
||||
notification_delay: schema.maybe(notificationDelaySchema),
|
||||
});
|
||||
|
||||
export const scheduleIdsSchema = schema.maybe(schema.arrayOf(schema.string()));
|
||||
|
|
|
@ -53,4 +53,5 @@ export interface RuleResponse<Params extends RuleParams = never> {
|
|||
revision: RuleResponseSchemaType['revision'];
|
||||
running?: RuleResponseSchemaType['running'];
|
||||
view_in_app_relative_url?: RuleResponseSchemaType['view_in_app_relative_url'];
|
||||
notification_delay?: RuleResponseSchemaType['notification_delay'];
|
||||
}
|
||||
|
|
|
@ -141,6 +141,10 @@ export interface MappedParamsProperties {
|
|||
|
||||
export type MappedParams = SavedObjectAttributes & MappedParamsProperties;
|
||||
|
||||
export interface NotificationDelay {
|
||||
active: number;
|
||||
}
|
||||
|
||||
export interface Rule<Params extends RuleTypeParams = never> {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
|
@ -174,6 +178,7 @@ export interface Rule<Params extends RuleTypeParams = never> {
|
|||
revision: number;
|
||||
running?: boolean | null;
|
||||
viewInAppRelativeUrl?: string;
|
||||
notificationDelay?: NotificationDelay;
|
||||
}
|
||||
|
||||
export interface SanitizedAlertsFilter extends AlertsFilter {
|
||||
|
|
|
@ -543,6 +543,7 @@ describe('toRaw', () => {
|
|||
},
|
||||
flappingHistory: [false, true, true],
|
||||
pendingRecoveredCount: 2,
|
||||
activeCount: 1,
|
||||
},
|
||||
};
|
||||
const alertInstance = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>(
|
||||
|
@ -562,6 +563,7 @@ describe('toRaw', () => {
|
|||
},
|
||||
flappingHistory: [false, true, true],
|
||||
flapping: false,
|
||||
activeCount: 1,
|
||||
},
|
||||
};
|
||||
const alertInstance = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>(
|
||||
|
@ -574,6 +576,7 @@ describe('toRaw', () => {
|
|||
flapping: false,
|
||||
maintenanceWindowIds: [],
|
||||
uuid: expect.any(String),
|
||||
activeCount: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -746,3 +749,43 @@ describe('isFilteredOut', () => {
|
|||
expect(alert.isFilteredOut(summarizedAlerts)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('incrementActiveCount', () => {
|
||||
test('correctly increments activeCount', () => {
|
||||
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1', {
|
||||
meta: { activeCount: 3 },
|
||||
});
|
||||
alert.incrementActiveCount();
|
||||
expect(alert.getActiveCount()).toEqual(4);
|
||||
});
|
||||
|
||||
test('correctly increments activeCount when it is not already defined', () => {
|
||||
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1');
|
||||
alert.incrementActiveCount();
|
||||
expect(alert.getActiveCount()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveCount', () => {
|
||||
test('returns ActiveCount', () => {
|
||||
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1', {
|
||||
meta: { activeCount: 3 },
|
||||
});
|
||||
expect(alert.getActiveCount()).toEqual(3);
|
||||
});
|
||||
|
||||
test('defines and returns activeCount when it is not already defined', () => {
|
||||
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1');
|
||||
expect(alert.getActiveCount()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetActiveCount', () => {
|
||||
test('resets activeCount to 0', () => {
|
||||
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1', {
|
||||
meta: { activeCount: 3 },
|
||||
});
|
||||
alert.resetActiveCount();
|
||||
expect(alert.getActiveCount()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -255,6 +255,7 @@ export class Alert<
|
|||
flappingHistory: this.meta.flappingHistory,
|
||||
flapping: this.meta.flapping,
|
||||
uuid: this.meta.uuid,
|
||||
activeCount: this.meta.activeCount,
|
||||
},
|
||||
}
|
||||
: {
|
||||
|
@ -327,4 +328,19 @@ export class Alert<
|
|||
getMaintenanceWindowIds() {
|
||||
return this.meta.maintenanceWindowIds ?? [];
|
||||
}
|
||||
|
||||
incrementActiveCount() {
|
||||
if (!this.meta.activeCount) {
|
||||
this.meta.activeCount = 0;
|
||||
}
|
||||
this.meta.activeCount++;
|
||||
}
|
||||
|
||||
getActiveCount() {
|
||||
return this.meta.activeCount || 0;
|
||||
}
|
||||
|
||||
resetActiveCount() {
|
||||
this.meta.activeCount = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { validateDuration } from '../../../validation';
|
||||
import { notifyWhenSchema, actionAlertsFilterSchema } from '../../../schemas';
|
||||
import {
|
||||
notifyWhenSchema,
|
||||
actionAlertsFilterSchema,
|
||||
notificationDelaySchema,
|
||||
} from '../../../schemas';
|
||||
|
||||
export const createRuleDataSchema = schema.object({
|
||||
name: schema.string(),
|
||||
|
@ -40,4 +44,5 @@ export const createRuleDataSchema = schema.object({
|
|||
{ defaultValue: [] }
|
||||
),
|
||||
notifyWhen: schema.maybe(schema.nullable(notifyWhenSchema)),
|
||||
notificationDelay: schema.maybe(notificationDelaySchema),
|
||||
});
|
||||
|
|
|
@ -22,4 +22,5 @@ export interface CreateRuleData<Params extends RuleParams = never> {
|
|||
schedule: CreateRuleDataType['schedule'];
|
||||
actions: CreateRuleDataType['actions'];
|
||||
notifyWhen?: CreateRuleDataType['notifyWhen'];
|
||||
notificationDelay?: CreateRuleDataType['notificationDelay'];
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export {
|
|||
monitoringSchema,
|
||||
ruleSchema,
|
||||
ruleDomainSchema,
|
||||
notificationDelaySchema,
|
||||
} from './rule_schemas';
|
||||
|
||||
export {
|
||||
|
|
|
@ -132,6 +132,10 @@ export const snoozeScheduleSchema = schema.object({
|
|||
skipRecurrences: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
||||
export const notificationDelaySchema = schema.object({
|
||||
active: schema.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Unsanitized (domain) rule schema, used by internal rules clients
|
||||
*/
|
||||
|
@ -168,6 +172,7 @@ export const ruleDomainSchema = schema.object({
|
|||
revision: schema.number(),
|
||||
running: schema.maybe(schema.nullable(schema.boolean())),
|
||||
viewInAppRelativeUrl: schema.maybe(schema.nullable(schema.string())),
|
||||
notificationDelay: schema.maybe(notificationDelaySchema),
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -205,4 +210,5 @@ export const ruleSchema = schema.object({
|
|||
revision: schema.number(),
|
||||
running: schema.maybe(schema.nullable(schema.boolean())),
|
||||
viewInAppRelativeUrl: schema.maybe(schema.nullable(schema.string())),
|
||||
notificationDelay: schema.maybe(notificationDelaySchema),
|
||||
});
|
||||
|
|
|
@ -216,6 +216,7 @@ export const transformRuleAttributesToRuleDomain = <Params extends RuleParams =
|
|||
...(esRule.nextRun ? { nextRun: new Date(esRule.nextRun) } : {}),
|
||||
revision: esRule.revision,
|
||||
running: esRule.running,
|
||||
...(esRule.notificationDelay ? { notificationDelay: esRule.notificationDelay } : {}),
|
||||
};
|
||||
|
||||
// Bad casts, but will fix once we fix all rule types
|
||||
|
|
|
@ -49,6 +49,7 @@ export const transformRuleDomainToRule = <Params extends RuleParams = never>(
|
|||
revision: ruleDomain.revision,
|
||||
running: ruleDomain.running,
|
||||
viewInAppRelativeUrl: ruleDomain.viewInAppRelativeUrl,
|
||||
notificationDelay: ruleDomain.notificationDelay,
|
||||
};
|
||||
|
||||
if (isPublic) {
|
||||
|
|
|
@ -68,5 +68,6 @@ export const transformRuleDomainToRuleAttributes = (
|
|||
...(rule.nextRun !== undefined ? { nextRun: rule.nextRun?.toISOString() || null } : {}),
|
||||
revision: rule.revision,
|
||||
...(rule.running !== undefined ? { running: rule.running } : {}),
|
||||
...(rule.notificationDelay !== undefined ? { notificationDelay: rule.notificationDelay } : {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -85,6 +85,7 @@ export interface Rule<Params extends RuleParams = never> {
|
|||
revision: RuleSchemaType['revision'];
|
||||
running?: RuleSchemaType['running'];
|
||||
viewInAppRelativeUrl?: RuleSchemaType['viewInAppRelativeUrl'];
|
||||
notificationDelay?: RuleSchemaType['notificationDelay'];
|
||||
}
|
||||
|
||||
export interface RuleDomain<Params extends RuleParams = never> {
|
||||
|
@ -120,4 +121,5 @@ export interface RuleDomain<Params extends RuleParams = never> {
|
|||
revision: RuleDomainSchemaType['revision'];
|
||||
running?: RuleDomainSchemaType['running'];
|
||||
viewInAppRelativeUrl?: RuleDomainSchemaType['viewInAppRelativeUrl'];
|
||||
notificationDelay?: RuleSchemaType['notificationDelay'];
|
||||
}
|
||||
|
|
|
@ -142,6 +142,10 @@ interface RuleMetaAttributes {
|
|||
versionApiKeyLastmodified?: string;
|
||||
}
|
||||
|
||||
interface NotificationDelayAttributes {
|
||||
active: number;
|
||||
}
|
||||
|
||||
export interface RuleAttributes {
|
||||
name: string;
|
||||
tags: string[];
|
||||
|
@ -174,4 +178,5 @@ export interface RuleAttributes {
|
|||
nextRun?: string | null;
|
||||
revision: number;
|
||||
running?: boolean | null;
|
||||
notificationDelay?: NotificationDelayAttributes;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 1,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -49,6 +50,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 1,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -59,6 +61,7 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
"2": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 1,
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -105,6 +108,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"3": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -129,6 +133,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"3": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -141,18 +146,19 @@ describe('getAlertsForNotification', () => {
|
|||
`);
|
||||
expect(Object.values(currentActiveAlerts).map((a) => a.getScheduledActionOptions()))
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actionGroup": "default",
|
||||
"context": Object {},
|
||||
"state": Object {},
|
||||
},
|
||||
]
|
||||
`);
|
||||
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 [],
|
||||
|
@ -163,6 +169,7 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
"2": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -176,6 +183,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -186,6 +194,7 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
"2": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -233,6 +242,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [
|
||||
true,
|
||||
|
@ -247,6 +257,7 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
"2": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [
|
||||
true,
|
||||
|
@ -261,6 +272,7 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
"3": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [
|
||||
true,
|
||||
|
@ -279,6 +291,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [
|
||||
true,
|
||||
|
@ -293,6 +306,7 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
"2": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [
|
||||
true,
|
||||
|
@ -307,6 +321,7 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
"3": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [
|
||||
true,
|
||||
|
@ -357,6 +372,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"3": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -385,6 +401,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -395,6 +412,7 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
"2": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -408,6 +426,7 @@ describe('getAlertsForNotification', () => {
|
|||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -418,6 +437,7 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
"2": Object {
|
||||
"meta": Object {
|
||||
"activeCount": 0,
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"maintenanceWindowIds": Array [],
|
||||
|
@ -428,4 +448,130 @@ describe('getAlertsForNotification', () => {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
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 } = getAlertsForNotification(
|
||||
DEFAULT_FLAPPING_SETTINGS,
|
||||
true,
|
||||
'default',
|
||||
{
|
||||
'1': alert1,
|
||||
},
|
||||
{
|
||||
'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 {},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('should reset activeCount for all recovered alerts', () => {
|
||||
const alert1 = new Alert('1', { meta: { activeCount: 3 } });
|
||||
const alert3 = new Alert('3');
|
||||
|
||||
const { recoveredAlerts, currentRecoveredAlerts } = getAlertsForNotification(
|
||||
DEFAULT_FLAPPING_SETTINGS,
|
||||
true,
|
||||
'default',
|
||||
{},
|
||||
{},
|
||||
{
|
||||
'1': alert1,
|
||||
'3': alert3,
|
||||
},
|
||||
{
|
||||
'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 {},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,12 +28,14 @@ export function getAlertsForNotification<
|
|||
|
||||
for (const id of keys(activeAlerts)) {
|
||||
const alert = activeAlerts[id];
|
||||
alert.incrementActiveCount();
|
||||
alert.resetPendingRecoveredCount();
|
||||
currentActiveAlerts[id] = alert;
|
||||
}
|
||||
|
||||
for (const id of keys(currentRecoveredAlerts)) {
|
||||
const alert = recoveredAlerts[id];
|
||||
alert.resetActiveCount();
|
||||
if (flappingSettings.enabled) {
|
||||
const flapping = alert.getFlapping();
|
||||
if (flapping) {
|
||||
|
|
|
@ -58,5 +58,6 @@ export const transformCreateBody = <Params extends RuleParams = never>(
|
|||
schedule: createBody.schedule,
|
||||
actions: transformCreateBodyActions(createBody.actions),
|
||||
...(createBody.notify_when ? { notifyWhen: createBody.notify_when } : {}),
|
||||
...(createBody.notification_delay ? { notificationDelay: createBody.notification_delay } : {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -119,4 +119,5 @@ export const transformRuleToRuleResponse = <Params extends RuleParams = never>(
|
|||
...(rule.viewInAppRelativeUrl !== undefined
|
||||
? { view_in_app_relative_url: rule.viewInAppRelativeUrl }
|
||||
: {}),
|
||||
...(rule.notificationDelay !== undefined ? { notification_delay: rule.notificationDelay } : {}),
|
||||
});
|
||||
|
|
|
@ -213,6 +213,10 @@ const rawRuleActionSchema = schema.object({
|
|||
useAlertDataForTemplate: schema.maybe(schema.boolean()),
|
||||
});
|
||||
|
||||
export const notificationDelaySchema = schema.object({
|
||||
active: schema.number(),
|
||||
});
|
||||
|
||||
export const rawRuleSchema = schema.object({
|
||||
name: schema.string(),
|
||||
enabled: schema.boolean(),
|
||||
|
@ -270,4 +274,5 @@ export const rawRuleSchema = schema.object({
|
|||
),
|
||||
params: schema.recordOf(schema.string(), schema.maybe(schema.any())),
|
||||
typeVersion: schema.maybe(schema.number()),
|
||||
notificationDelay: schema.maybe(notificationDelaySchema),
|
||||
});
|
||||
|
|
|
@ -161,6 +161,7 @@ const generateAlert = ({
|
|||
lastScheduledActionsGroup = 'default',
|
||||
maintenanceWindowIds,
|
||||
pendingRecoveredCount,
|
||||
activeCount,
|
||||
}: {
|
||||
id: number;
|
||||
group?: ActiveActionGroup | 'recovered';
|
||||
|
@ -171,6 +172,7 @@ const generateAlert = ({
|
|||
lastScheduledActionsGroup?: string;
|
||||
maintenanceWindowIds?: string[];
|
||||
pendingRecoveredCount?: number;
|
||||
activeCount?: number;
|
||||
}) => {
|
||||
const alert = new Alert<AlertInstanceState, AlertInstanceContext, 'default' | 'other-group'>(
|
||||
String(id),
|
||||
|
@ -184,6 +186,7 @@ const generateAlert = ({
|
|||
actions: throttledActions,
|
||||
},
|
||||
pendingRecoveredCount,
|
||||
activeCount,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -2049,6 +2052,165 @@ describe('Execution Handler', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('does not schedule actions for alerts with activeCount less than the notificationDelay.active threshold', async () => {
|
||||
const executionHandler = new ExecutionHandler(
|
||||
generateExecutionParams({
|
||||
...defaultExecutionParams,
|
||||
rule: {
|
||||
...defaultExecutionParams.rule,
|
||||
notificationDelay: {
|
||||
active: 3,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await executionHandler.run({
|
||||
...generateAlert({ id: 1 }),
|
||||
...generateAlert({ id: 2, activeCount: 2 }),
|
||||
});
|
||||
|
||||
expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled();
|
||||
expect(defaultExecutionParams.logger.debug).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(defaultExecutionParams.logger.debug).toHaveBeenCalledWith(
|
||||
'no scheduling of action "1" for rule "1": the alert activeCount: 0 is less than the rule notificationDelay.active: 3 threshold.'
|
||||
);
|
||||
expect(defaultExecutionParams.logger.debug).toHaveBeenCalledWith(
|
||||
'no scheduling of action "1" for rule "1": the alert activeCount: 2 is less than the rule notificationDelay.active: 3 threshold.'
|
||||
);
|
||||
});
|
||||
|
||||
test('schedules actions for alerts with activeCount greater than or equal the notificationDelay.active threshold', async () => {
|
||||
const executionHandler = new ExecutionHandler(
|
||||
generateExecutionParams({
|
||||
...defaultExecutionParams,
|
||||
rule: {
|
||||
...defaultExecutionParams.rule,
|
||||
notificationDelay: {
|
||||
active: 3,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await executionHandler.run({
|
||||
...generateAlert({ id: 1, activeCount: 3 }),
|
||||
...generateAlert({ id: 2, activeCount: 4 }),
|
||||
});
|
||||
|
||||
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.bulkEnqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 1 goes here",
|
||||
"contextVal": "My goes here",
|
||||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"namespace": "test1",
|
||||
"type": "alert",
|
||||
"typeId": "test",
|
||||
},
|
||||
],
|
||||
"source": Object {
|
||||
"source": Object {
|
||||
"id": "1",
|
||||
"type": "alert",
|
||||
},
|
||||
"type": "SAVED_OBJECT",
|
||||
},
|
||||
"spaceId": "test1",
|
||||
},
|
||||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 2 goes here",
|
||||
"contextVal": "My goes here",
|
||||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"namespace": "test1",
|
||||
"type": "alert",
|
||||
"typeId": "test",
|
||||
},
|
||||
],
|
||||
"source": Object {
|
||||
"source": Object {
|
||||
"id": "1",
|
||||
"type": "alert",
|
||||
},
|
||||
"type": "SAVED_OBJECT",
|
||||
},
|
||||
"spaceId": "test1",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('schedules actions if notificationDelay.active threshold is not defined', async () => {
|
||||
const executionHandler = new ExecutionHandler(generateExecutionParams());
|
||||
|
||||
await executionHandler.run({
|
||||
...generateAlert({ id: 1, activeCount: 1 }),
|
||||
});
|
||||
|
||||
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.bulkEnqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"actionTypeId": "test",
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"consumer": "rule-consumer",
|
||||
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 1 goes here",
|
||||
"contextVal": "My goes here",
|
||||
"foo": true,
|
||||
"stateVal": "My goes here",
|
||||
},
|
||||
"relatedSavedObjects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"namespace": "test1",
|
||||
"type": "alert",
|
||||
"typeId": "test",
|
||||
},
|
||||
],
|
||||
"source": Object {
|
||||
"source": Object {
|
||||
"id": "1",
|
||||
"type": "alert",
|
||||
},
|
||||
"type": "SAVED_OBJECT",
|
||||
},
|
||||
"spaceId": "test1",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('rule url', () => {
|
||||
const ruleWithUrl = {
|
||||
...rule,
|
||||
|
|
|
@ -628,6 +628,22 @@ export class ExecutionHandler<
|
|||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
this.rule.notificationDelay &&
|
||||
alert.getActiveCount() < this.rule.notificationDelay.active
|
||||
) {
|
||||
this.logger.debug(
|
||||
`no scheduling of action "${action.id}" for rule "${
|
||||
this.taskInstance.params.alertId
|
||||
}": the alert activeCount: ${alert.getActiveCount()} is less than the rule notificationDelay.active: ${
|
||||
this.rule.notificationDelay.active
|
||||
} threshold.`
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
alert.resetActiveCount();
|
||||
}
|
||||
|
||||
const actionGroup = this.getActionGroup(alert);
|
||||
|
||||
if (!this.ruleTypeActionGroups!.has(actionGroup)) {
|
||||
|
|
|
@ -454,6 +454,7 @@ export const generateAlertInstance = (
|
|||
flapping: false,
|
||||
maintenanceWindowIds: maintenanceWindowIds || [],
|
||||
pendingRecoveredCount: 0,
|
||||
activeCount: 0,
|
||||
},
|
||||
state: {
|
||||
bar: false,
|
||||
|
|
|
@ -2954,6 +2954,7 @@ describe('Task Runner', () => {
|
|||
maintenanceWindowIds: [],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 0,
|
||||
activeCount: 0,
|
||||
},
|
||||
state: {
|
||||
duration: '0',
|
||||
|
@ -3124,6 +3125,7 @@ describe('Task Runner', () => {
|
|||
maintenanceWindowIds: [],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 0,
|
||||
activeCount: 0,
|
||||
},
|
||||
state: {
|
||||
duration: '0',
|
||||
|
@ -3141,6 +3143,7 @@ describe('Task Runner', () => {
|
|||
maintenanceWindowIds: [],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 0,
|
||||
activeCount: 0,
|
||||
},
|
||||
state: {
|
||||
duration: '0',
|
||||
|
|
|
@ -60,6 +60,7 @@ import {
|
|||
AlertsFilter,
|
||||
AlertsFilterTimeframe,
|
||||
RuleAlertData,
|
||||
NotificationDelay,
|
||||
} from '../common';
|
||||
import { PublicAlertFactory } from './alert/create_alert_factory';
|
||||
import { RulesSettingsFlappingProperties } from '../common/rules_settings';
|
||||
|
@ -409,6 +410,7 @@ export type PublicRuleResultService = PublicLastRunSetters;
|
|||
|
||||
export interface RawRuleLastRun extends SavedObjectAttributes, RuleLastRun {}
|
||||
export interface RawRuleMonitoring extends SavedObjectAttributes, RuleMonitoring {}
|
||||
export interface RawNotificationDelay extends SavedObjectAttributes, NotificationDelay {}
|
||||
|
||||
export interface RawRuleAlertsFilter extends AlertsFilter {
|
||||
query?: {
|
||||
|
@ -485,6 +487,7 @@ export interface RawRule extends SavedObjectAttributes {
|
|||
nextRun?: string | null;
|
||||
revision: number;
|
||||
running?: boolean | null;
|
||||
notificationDelay?: RawNotificationDelay;
|
||||
}
|
||||
|
||||
export type { DataStreamAdapter } from './alerts_service/lib/data_stream_adapter';
|
||||
|
|
|
@ -1848,6 +1848,158 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(hasActions).eql(false);
|
||||
});
|
||||
|
||||
it('should generate expected events with a notificationDelay', 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 pattern = {
|
||||
instance: [true, true, true, false, true],
|
||||
};
|
||||
|
||||
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: {},
|
||||
},
|
||||
],
|
||||
notification_delay: {
|
||||
active: 3,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
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: 5 }],
|
||||
['execute', { gte: 5 }],
|
||||
['new-instance', { equal: 2 }],
|
||||
['active-instance', { gte: 1 }],
|
||||
['recovered-instance', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const actualTriggeredActions = events
|
||||
.filter((event) => event?.event?.action === 'execute')
|
||||
.reduce(
|
||||
(acc, event) =>
|
||||
acc +
|
||||
(event?.kibana?.alert?.rule?.execution?.metrics
|
||||
?.number_of_triggered_actions as number),
|
||||
0
|
||||
);
|
||||
expect(actualTriggeredActions).to.eql(1);
|
||||
});
|
||||
|
||||
it('should generate expected events with a notificationDelay with AAD', 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 pattern = {
|
||||
instance: [true, true, true, false, true],
|
||||
};
|
||||
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.patternFiringAad',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
params: {
|
||||
pattern,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
notification_delay: {
|
||||
active: 3,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
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: 5 }],
|
||||
['execute', { gte: 5 }],
|
||||
['new-instance', { equal: 2 }],
|
||||
['active-instance', { gte: 1 }],
|
||||
['recovered-instance', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const actualTriggeredActions = events
|
||||
.filter((event) => event?.event?.action === 'execute')
|
||||
.reduce(
|
||||
(acc, event) =>
|
||||
acc +
|
||||
(event?.kibana?.alert?.rule?.execution?.metrics
|
||||
?.number_of_triggered_actions as number),
|
||||
0
|
||||
);
|
||||
expect(actualTriggeredActions).to.eql(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from '../../../../../common/lib';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createAlertsAsDataInstallResourcesTest({ getService }: FtrProviderContext) {
|
||||
export default function createAlertsAsDataFlappingTest({ getService }: FtrProviderContext) {
|
||||
const es = getService('es');
|
||||
const retry = getService('retry');
|
||||
const supertest = getService('supertest');
|
||||
|
|
|
@ -28,6 +28,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
|
|||
loadTestFile(require.resolve('./run_soon'));
|
||||
loadTestFile(require.resolve('./flapping_history'));
|
||||
loadTestFile(require.resolve('./check_registered_rule_types'));
|
||||
loadTestFile(require.resolve('./notification_delay'));
|
||||
loadTestFile(require.resolve('./generate_alert_schemas'));
|
||||
// Do not place test files here, due to https://github.com/elastic/kibana/issues/123059
|
||||
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { get } from 'lodash';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib';
|
||||
import { Spaces } from '../../../scenarios';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createNotificationDelayTests({ getService }: FtrProviderContext) {
|
||||
const es = getService('es');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const retry = getService('retry');
|
||||
const supertest = getService('supertest');
|
||||
const space = Spaces.default;
|
||||
|
||||
const ACTIVE_PATH = 'alertInstances.instance.meta.activeCount';
|
||||
const RECOVERED_PATH = 'alertRecoveredInstances.instance.meta.activeCount';
|
||||
|
||||
describe('Notification Delay', () => {
|
||||
let actionId: string;
|
||||
const objectRemover = new ObjectRemover(supertestWithoutAuth);
|
||||
|
||||
before(async () => {
|
||||
actionId = await createAction();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
objectRemover.add(space.id, actionId, 'connector', 'actions');
|
||||
await objectRemover.removeAll();
|
||||
});
|
||||
|
||||
afterEach(() => objectRemover.removeAll());
|
||||
|
||||
it('should clear the activeCount if the notificationDelay is not configured for the rule', async () => {
|
||||
const start = new Date().toISOString();
|
||||
const pattern = {
|
||||
instance: [true],
|
||||
};
|
||||
|
||||
const ruleId = await createRule(actionId, pattern);
|
||||
objectRemover.add(space.id, ruleId, 'rule', 'alerting');
|
||||
|
||||
const state = await getAlertState(start, ruleId, 0);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(0);
|
||||
});
|
||||
|
||||
it('should update the activeCount when alert is active and clear when recovered if the notificationDelay is configured for the rule', async () => {
|
||||
let start = new Date().toISOString();
|
||||
const pattern = {
|
||||
instance: [true, true, true, false, true],
|
||||
};
|
||||
|
||||
const ruleId = await createRule(actionId, pattern, 20);
|
||||
objectRemover.add(space.id, ruleId, 'rule', 'alerting');
|
||||
|
||||
let state = await getAlertState(start, ruleId);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(1);
|
||||
|
||||
start = new Date().toISOString();
|
||||
state = await getAlertState(start, ruleId, 2, true);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(2);
|
||||
|
||||
start = new Date().toISOString();
|
||||
state = await getAlertState(start, ruleId, 3, true);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(3);
|
||||
|
||||
start = new Date().toISOString();
|
||||
state = await getAlertState(start, ruleId, 0, true, true);
|
||||
expect(get(state, RECOVERED_PATH)).to.eql(0);
|
||||
|
||||
start = new Date().toISOString();
|
||||
state = await getAlertState(start, ruleId, 1, true);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(1);
|
||||
});
|
||||
|
||||
it('should reset the activeCount when count of consecutive active alerts exceeds the notificationDelay count', async () => {
|
||||
let start = new Date().toISOString();
|
||||
const pattern = {
|
||||
instance: [true, true, true, true, true],
|
||||
};
|
||||
|
||||
const ruleId = await createRule(actionId, pattern, 3);
|
||||
objectRemover.add(space.id, ruleId, 'rule', 'alerting');
|
||||
|
||||
let state = await getAlertState(start, ruleId);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(1);
|
||||
|
||||
start = new Date().toISOString();
|
||||
state = await getAlertState(start, ruleId, 2, true);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(2);
|
||||
|
||||
start = new Date().toISOString();
|
||||
state = await getAlertState(start, ruleId, 0, true);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(0);
|
||||
|
||||
start = new Date().toISOString();
|
||||
state = await getAlertState(start, ruleId, 1, true);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(1);
|
||||
|
||||
start = new Date().toISOString();
|
||||
state = await getAlertState(start, ruleId, 2, true);
|
||||
expect(get(state, ACTIVE_PATH)).to.eql(2);
|
||||
});
|
||||
});
|
||||
|
||||
async function getState(start: string, count: number, recovered: boolean) {
|
||||
const result: any = await retry.try(async () => {
|
||||
const searchResult = await es.search({
|
||||
index: '.kibana_task_manager',
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'task.taskType': 'alerting:test.patternFiring',
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'task.scheduledAt': {
|
||||
gte: start,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const taskDoc: any = searchResult.hits.hits[0];
|
||||
const state = JSON.parse(taskDoc._source.task.state);
|
||||
const activeCount = recovered ? get(state, RECOVERED_PATH) : get(state, ACTIVE_PATH);
|
||||
if (activeCount !== count) {
|
||||
throw new Error(`Expected ${count} rule executions but received ${activeCount}.`);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getAlertState(
|
||||
start: string,
|
||||
ruleId: string,
|
||||
count: number = 1,
|
||||
runRule: boolean = false,
|
||||
recovered: boolean = false
|
||||
) {
|
||||
if (runRule) {
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/internal/alerting/rule/${ruleId}/_run_soon`)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
expect(response.status).to.eql(204);
|
||||
}
|
||||
return await getState(start, count, recovered);
|
||||
}
|
||||
|
||||
async function createRule(
|
||||
actionId: string,
|
||||
pattern: { instance: boolean[] },
|
||||
activeCount?: number
|
||||
) {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.patternFiring',
|
||||
schedule: { interval: '24h' },
|
||||
throttle: null,
|
||||
params: {
|
||||
pattern,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: actionId,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
...(activeCount ? { notification_delay: { active: activeCount } } : {}),
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
return createdRule.id;
|
||||
}
|
||||
|
||||
async function createAction() {
|
||||
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);
|
||||
return createdAction.id;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue