[Response Ops][Flapping] Rule Specific Flapping - Create/Update API changes (#190019)

## Summary
Issue: https://github.com/elastic/kibana/issues/190018

Implement rule specific flapping support for create and update Rule API.
The new property on the rule is named `flapping`;

```
flapping: {
  look_back_window: number;
  status_change_threshold: number;
}
```

Also make changes in the task runner to use the rule's flapping settings
if it exists. Otherwise use the global flapping setting.

# To test
1. Go to
`x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts`
and turn `IS_RULE_SPECIFIC_FLAPPING_ENABLED` to `true`
2. Create a rule with a rule specific flapping setting, generate the
alert and let it flap
3. Assert that the flapping is now using the rule specific flapping
4. Turn space flapping off
5. Assert that it no longer flaps despite having a rule specific
flapping
6. Try deleting/adding back the rule specific flapping via the UI and
verify everything works.

### 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

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2024-10-08 18:01:45 -07:00 committed by GitHub
parent e9aff7dccd
commit edd61f63db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 2108 additions and 74 deletions

View file

@ -1851,6 +1851,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"id": {
"description": "The identifier for the rule.",
"type": "string"
@ -2667,6 +2688,27 @@
"description": "Indicates whether you want to run the rule on an interval basis after it is created.",
"type": "boolean"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"name": {
"description": "The name of the rule. While this name does not have to be unique, a distinctive name can help you identify a rule.",
"type": "string"
@ -3047,6 +3089,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"id": {
"description": "The identifier for the rule.",
"type": "string"
@ -3853,6 +3916,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"name": {
"description": "The name of the rule. While this name does not have to be unique, a distinctive name can help you identify a rule.",
"type": "string"
@ -4226,6 +4310,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"id": {
"description": "The identifier for the rule.",
"type": "string"
@ -5692,6 +5797,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"id": {
"description": "The identifier for the rule.",
"type": "string"

View file

@ -1851,6 +1851,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"id": {
"description": "The identifier for the rule.",
"type": "string"
@ -2667,6 +2688,27 @@
"description": "Indicates whether you want to run the rule on an interval basis after it is created.",
"type": "boolean"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"name": {
"description": "The name of the rule. While this name does not have to be unique, a distinctive name can help you identify a rule.",
"type": "string"
@ -3047,6 +3089,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"id": {
"description": "The identifier for the rule.",
"type": "string"
@ -3853,6 +3916,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"name": {
"description": "The name of the rule. While this name does not have to be unique, a distinctive name can help you identify a rule.",
"type": "string"
@ -4226,6 +4310,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"id": {
"description": "The identifier for the rule.",
"type": "string"
@ -5692,6 +5797,27 @@
],
"type": "object"
},
"flapping": {
"additionalProperties": false,
"nullable": true,
"properties": {
"look_back_window": {
"maximum": 20,
"minimum": 2,
"type": "number"
},
"status_change_threshold": {
"maximum": 20,
"minimum": 2,
"type": "number"
}
},
"required": [
"look_back_window",
"status_change_threshold"
],
"type": "object"
},
"id": {
"description": "The identifier for the rule.",
"type": "string"

View file

@ -1269,6 +1269,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -2026,6 +2042,22 @@ paths:
Indicates whether you want to run the rule on an interval
basis after it is created.
type: boolean
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
name:
description: >-
The name of the rule. While this name does not have to be
@ -2410,6 +2442,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -3146,6 +3194,22 @@ paths:
type: number
required:
- active
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
name:
description: >-
The name of the rule. While this name does not have to be
@ -3522,6 +3586,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -4732,6 +4812,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string

View file

@ -1269,6 +1269,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -2026,6 +2042,22 @@ paths:
Indicates whether you want to run the rule on an interval
basis after it is created.
type: boolean
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
name:
description: >-
The name of the rule. While this name does not have to be
@ -2410,6 +2442,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -3146,6 +3194,22 @@ paths:
type: number
required:
- active
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
name:
description: >-
The name of the rule. While this name does not have to be
@ -3522,6 +3586,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -4732,6 +4812,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string

View file

@ -1650,6 +1650,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -2407,6 +2423,22 @@ paths:
Indicates whether you want to run the rule on an interval
basis after it is created.
type: boolean
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
name:
description: >-
The name of the rule. While this name does not have to be
@ -2791,6 +2823,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -3527,6 +3575,22 @@ paths:
type: number
required:
- active
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
name:
description: >-
The name of the rule. While this name does not have to be
@ -3903,6 +3967,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -5113,6 +5193,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string

View file

@ -1650,6 +1650,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -2407,6 +2423,22 @@ paths:
Indicates whether you want to run the rule on an interval
basis after it is created.
type: boolean
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
name:
description: >-
The name of the rule. While this name does not have to be
@ -2791,6 +2823,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -3527,6 +3575,22 @@ paths:
type: number
required:
- active
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
name:
description: >-
The name of the rule. While this name does not have to be
@ -3903,6 +3967,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string
@ -5113,6 +5193,22 @@ paths:
required:
- status
- last_execution_date
flapping:
additionalProperties: false
nullable: true
type: object
properties:
look_back_window:
maximum: 20
minimum: 2
type: number
status_change_threshold:
maximum: 20
minimum: 2
type: number
required:
- look_back_window
- status_change_threshold
id:
description: The identifier for the rule.
type: string

View file

@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './v1';

View file

@ -18,5 +18,4 @@ export * from './r_rule_types';
export * from './rule_notify_when_type';
export * from './rule_type_types';
export * from './rule_types';
export * from './rule_flapping';
export * from './search_strategy_types';

View file

@ -205,6 +205,11 @@ export type SanitizedRuleAction = Omit<RuleAction, 'alertsFilter'> & {
alertsFilter?: SanitizedAlertsFilter;
};
export interface Flapping extends SavedObjectAttributes {
lookBackWindow: number;
statusChangeThreshold: number;
}
export interface Rule<Params extends RuleTypeParams = never> {
id: string;
enabled: boolean;
@ -240,10 +245,7 @@ export interface Rule<Params extends RuleTypeParams = never> {
running?: boolean | null;
viewInAppRelativeUrl?: string;
alertDelay?: AlertDelay | null;
flapping?: {
lookBackWindow: number;
statusChangeThreshold: number;
};
flapping?: Flapping | null;
}
export type SanitizedRule<Params extends RuleTypeParams = never> = Omit<

View file

@ -17,10 +17,8 @@ const transformCreateRuleFlapping = (flapping: Rule['flapping']) => {
}
return {
flapping: {
look_back_window: flapping.lookBackWindow,
status_change_threshold: flapping.statusChangeThreshold,
},
look_back_window: flapping.lookBackWindow,
status_change_threshold: flapping.statusChangeThreshold,
};
};
@ -60,5 +58,5 @@ export const transformCreateRuleBody: RewriteResponseCase<CreateRuleBody> = ({
};
}),
...(alertDelay ? { alert_delay: alertDelay } : {}),
...(flapping !== undefined ? transformCreateRuleFlapping(flapping) : {}),
...(flapping !== undefined ? { flapping: transformCreateRuleFlapping(flapping) } : {}),
});

View file

@ -17,10 +17,8 @@ const transformUpdateRuleFlapping = (flapping: Rule['flapping']) => {
}
return {
flapping: {
look_back_window: flapping.lookBackWindow,
status_change_threshold: flapping.statusChangeThreshold,
},
look_back_window: flapping.lookBackWindow,
status_change_threshold: flapping.statusChangeThreshold,
};
};
@ -57,5 +55,5 @@ export const transformUpdateRuleBody: RewriteResponseCase<UpdateRuleBody> = ({
};
}),
...(alertDelay ? { alert_delay: alertDelay } : {}),
...(flapping !== undefined ? transformUpdateRuleFlapping(flapping) : {}),
...(flapping !== undefined ? { flapping: transformUpdateRuleFlapping(flapping) } : {}),
});

View file

@ -40,10 +40,8 @@ const transformFlapping = (flapping: AsApiContract<Rule['flapping']>) => {
}
return {
flapping: {
lookBackWindow: flapping.look_back_window,
statusChangeThreshold: flapping.status_change_threshold,
},
lookBackWindow: flapping.look_back_window,
statusChangeThreshold: flapping.status_change_threshold,
};
};
@ -91,7 +89,7 @@ export const transformRule: RewriteRequestCase<Rule> = ({
...(nextRun ? { nextRun } : {}),
...(apiKeyCreatedByUser !== undefined ? { apiKeyCreatedByUser } : {}),
...(alertDelay ? { alertDelay } : {}),
...(flapping !== undefined ? transformFlapping(flapping) : {}),
...(flapping !== undefined ? { flapping: transformFlapping(flapping) } : {}),
...rest,
});

View file

@ -27,6 +27,8 @@ import { TypeRegistry } from '../type_registry';
export type { SanitizedRuleAction as RuleAction } from '@kbn/alerting-types';
export type { Flapping } from '@kbn/alerting-types';
export type RuleTypeWithDescription = RuleType<string, string> & { description?: string };
export type RuleTypeIndexWithDescriptions = Map<string, RuleTypeWithDescription>;

View file

@ -14,7 +14,7 @@ import {
MAX_LOOK_BACK_WINDOW,
MIN_STATUS_CHANGE_THRESHOLD,
MAX_STATUS_CHANGE_THRESHOLD,
} from '@kbn/alerting-types';
} from '@kbn/alerting-types/flapping/latest';
import { i18n } from '@kbn/i18n';
import { RuleSettingsRangeInput } from './rule_settings_range_input';

View file

@ -59,7 +59,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"action": "0e6fc0b74c7312a8c11ff6b14437b93a997358b8",
"action_task_params": "b50cb5c8a493881474918e8d4985e61374ca4c30",
"ad_hoc_run_params": "d4e3c5c794151d0a4f5c71e886b2aa638da73ad2",
"alert": "e920b0583b1a32338d5dbbbfabb6ff2a511f5f6c",
"alert": "05b07040b12ff45ab642f47464e8a6c903cf7b86",
"api_key_pending_invalidation": "1399e87ca37b3d3a65d269c924eda70726cfe886",
"apm-custom-dashboards": "b67128f78160c288bd7efe25b2da6e2afd5e82fc",
"apm-indices": "8a2d68d415a4b542b26b0d292034a28ffac6fed4",

View file

@ -23,7 +23,7 @@ export type {
RuleTaskState,
RuleTaskParams,
} from '@kbn/alerting-state-types';
export type { AlertingFrameworkHealth } from '@kbn/alerting-types';
export type { AlertingFrameworkHealth, Flapping } from '@kbn/alerting-types';
export * from './alert_summary';
export * from './builtin_action_groups';
export * from './bulk_edit';

View file

@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema';
import { validateDurationV1, validateHoursV1, validateTimezoneV1 } from '../../../validation';
import { notifyWhenSchemaV1, alertDelaySchemaV1 } from '../../../response';
import { alertsFilterQuerySchemaV1 } from '../../../../alerts_filter_query';
import { flappingSchemaV1 } from '../../../common';
export const actionFrequencySchema = schema.object({
summary: schema.boolean({
@ -186,6 +187,7 @@ export const createBodySchema = schema.object({
actions: schema.arrayOf(actionSchema, { defaultValue: [] }),
notify_when: schema.maybe(schema.nullable(notifyWhenSchemaV1)),
alert_delay: schema.maybe(alertDelaySchemaV1),
flapping: schema.maybe(schema.nullable(flappingSchemaV1)),
});
export const createParamsSchema = schema.object({

View file

@ -31,6 +31,7 @@ export interface CreateRuleRequestBody<Params extends RuleParamsV1 = never> {
actions: CreateBodySchema['actions'];
notify_when?: CreateBodySchema['notify_when'];
alert_delay?: CreateBodySchema['alert_delay'];
flapping?: CreateBodySchema['flapping'];
}
export interface CreateRuleResponse<Params extends RuleParamsV1 = never> {

View file

@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema';
import { validateDurationV1, validateHoursV1, validateTimezoneV1 } from '../../../validation';
import { notifyWhenSchemaV1, alertDelaySchemaV1 } from '../../../response';
import { alertsFilterQuerySchemaV1 } from '../../../../alerts_filter_query';
import { flappingSchemaV1 } from '../../../common';
export const actionFrequencySchema = schema.object({
summary: schema.boolean({
@ -158,6 +159,7 @@ export const updateBodySchema = schema.object({
actions: schema.arrayOf(actionSchema, { defaultValue: [] }),
notify_when: schema.maybe(schema.nullable(notifyWhenSchemaV1)),
alert_delay: schema.maybe(alertDelaySchemaV1),
flapping: schema.maybe(schema.nullable(flappingSchemaV1)),
});
export const updateParamsSchema = schema.object({

View file

@ -30,6 +30,7 @@ export interface UpdateRuleRequestBody<Params extends RuleParamsV1 = never> {
actions: UpdateBodySchema['actions'];
notify_when?: UpdateBodySchema['notify_when'];
alert_delay?: UpdateBodySchema['alert_delay'];
flapping?: UpdateBodySchema['flapping'];
}
export interface UpdateRuleResponse<Params extends RuleParamsV1 = never> {

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,29 @@
/*
* 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 { schema } from '@kbn/config-schema';
import {
MIN_LOOK_BACK_WINDOW as MIN_LOOK_BACK_WINDOW_V1,
MAX_LOOK_BACK_WINDOW as MAX_LOOK_BACK_WINDOW_V1,
MIN_STATUS_CHANGE_THRESHOLD as MIN_STATUS_CHANGE_THRESHOLD_V1,
MAX_STATUS_CHANGE_THRESHOLD as MAX_STATUS_CHANGE_THRESHOLD_V1,
} from '@kbn/alerting-types/flapping/v1';
import { validateFlapping as validateFlappingV1 } from '../../../validation/validate_flapping/v1';
export const flappingSchema = schema.object(
{
look_back_window: schema.number({
min: MIN_LOOK_BACK_WINDOW_V1,
max: MAX_LOOK_BACK_WINDOW_V1,
}),
status_change_threshold: schema.number({
min: MIN_STATUS_CHANGE_THRESHOLD_V1,
max: MAX_STATUS_CHANGE_THRESHOLD_V1,
}),
},
{ validate: validateFlappingV1 }
);

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,11 @@
/*
* 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 type { TypeOf } from '@kbn/config-schema';
import { flappingSchemaV1 } from '../..';
export type Flapping = TypeOf<typeof flappingSchemaV1>;

View file

@ -13,6 +13,8 @@ export {
ruleExecutionStatusWarningReason,
} from './constants/latest';
export { flappingSchema } from './flapping/schemas/latest';
export type {
RuleNotifyWhen,
RuleLastRunOutcomeValues,
@ -21,6 +23,8 @@ export type {
RuleExecutionStatusWarningReason,
} from './constants/latest';
export type { Flapping } from './flapping/types/latest';
export {
ruleNotifyWhen as ruleNotifyWhenV1,
ruleLastRunOutcomeValues as ruleLastRunOutcomeValuesV1,
@ -29,6 +33,8 @@ export {
ruleExecutionStatusWarningReason as ruleExecutionStatusWarningReasonV1,
} from './constants/v1';
export { flappingSchema as flappingSchemaV1 } from './flapping/schemas/v1';
export type {
RuleNotifyWhen as RuleNotifyWhenV1,
RuleLastRunOutcomeValues as RuleLastRunOutcomeValuesV1,
@ -36,3 +42,5 @@ export type {
RuleExecutionStatusErrorReason as RuleExecutionStatusErrorReasonV1,
RuleExecutionStatusWarningReason as RuleExecutionStatusWarningReasonV1,
} from './constants/v1';
export type { Flapping as FlappingV1 } from './flapping/types/v1';

View file

@ -16,6 +16,7 @@ import {
ruleLastRunOutcomeValues as ruleLastRunOutcomeValuesV1,
} from '../../common/constants/v1';
import { validateNotifyWhenV1 } from '../../validation';
import { flappingSchemaV1 } from '../../common';
export const ruleParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()), {
meta: { description: 'The parameters for the rule.' },
@ -626,6 +627,7 @@ export const ruleResponseSchema = schema.object({
)
),
alert_delay: schema.maybe(alertDelaySchema),
flapping: schema.maybe(schema.nullable(flappingSchemaV1)),
});
export const scheduleIdsSchema = schema.maybe(schema.arrayOf(schema.string()));

View file

@ -54,4 +54,5 @@ export interface RuleResponse<Params extends RuleParams = never> {
running?: RuleResponseSchemaType['running'];
view_in_app_relative_url?: RuleResponseSchemaType['view_in_app_relative_url'];
alert_delay?: RuleResponseSchemaType['alert_delay'];
flapping?: RuleResponseSchemaType['flapping'];
}

View file

@ -9,9 +9,11 @@ export { validateDuration } from './validate_duration/latest';
export { validateHours } from './validate_hours/latest';
export { validateTimezone } from './validate_timezone/latest';
export { validateSnoozeSchedule } from './validate_snooze_schedule/latest';
export { validateFlapping } from './validate_flapping/latest';
export { validateDuration as validateDurationV1 } from './validate_duration/v1';
export { validateHours as validateHoursV1 } from './validate_hours/v1';
export { validateNotifyWhen as validateNotifyWhenV1 } from './validate_notify_when/v1';
export { validateTimezone as validateTimezoneV1 } from './validate_timezone/v1';
export { validateSnoozeSchedule as validateSnoozeScheduleV1 } from './validate_snooze_schedule/v1';
export { validateFlapping as validateFlappingV1 } from './validate_flapping/v1';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,57 @@
/*
* 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 { validateFlapping } from './v1';
describe('validateFlapping', () => {
test('should error if look back window exceeds the lower bound', () => {
const result = validateFlapping({
look_back_window: 0,
status_change_threshold: 10,
});
expect(result).toEqual('look back window must be between 2 and 20');
});
test('should error if look back window exceeds the upper bound', () => {
const result = validateFlapping({
look_back_window: 50,
status_change_threshold: 10,
});
expect(result).toEqual('look back window must be between 2 and 20');
});
test('should error if status change threshold exceeds the lower bound', () => {
const result = validateFlapping({
look_back_window: 10,
status_change_threshold: 1,
});
expect(result).toEqual('status change threshold must be between 2 and 20');
});
test('should error if status change threshold exceeds the upper bound', () => {
const result = validateFlapping({
look_back_window: 10,
status_change_threshold: 50,
});
expect(result).toEqual('status change threshold must be between 2 and 20');
});
test('should error if status change threshold is greater than the look back window', () => {
const result = validateFlapping({
look_back_window: 10,
status_change_threshold: 15,
});
expect(result).toEqual(
'lookBackWindow (10) must be equal to or greater than statusChangeThreshold (15)'
);
});
});

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
MIN_LOOK_BACK_WINDOW as MIN_LOOK_BACK_WINDOW_V1,
MAX_LOOK_BACK_WINDOW as MAX_LOOK_BACK_WINDOW_V1,
MIN_STATUS_CHANGE_THRESHOLD as MIN_STATUS_CHANGE_THRESHOLD_V1,
MAX_STATUS_CHANGE_THRESHOLD as MAX_STATUS_CHANGE_THRESHOLD_V1,
} from '@kbn/alerting-types/flapping/v1';
export const validateFlapping = (flapping: {
look_back_window: number;
status_change_threshold: number;
}) => {
const { look_back_window: lookBackWindow, status_change_threshold: statusChangeThreshold } =
flapping;
if (lookBackWindow < MIN_LOOK_BACK_WINDOW_V1 || lookBackWindow > MAX_LOOK_BACK_WINDOW_V1) {
return `look back window must be between ${MIN_LOOK_BACK_WINDOW_V1} and ${MAX_LOOK_BACK_WINDOW_V1}`;
}
if (
statusChangeThreshold < MIN_STATUS_CHANGE_THRESHOLD_V1 ||
statusChangeThreshold > MAX_STATUS_CHANGE_THRESHOLD_V1
) {
return `status change threshold must be between ${MIN_STATUS_CHANGE_THRESHOLD_V1} and ${MAX_STATUS_CHANGE_THRESHOLD_V1}`;
}
if (lookBackWindow < statusChangeThreshold) {
return `lookBackWindow (${lookBackWindow}) must be equal to or greater than statusChangeThreshold (${statusChangeThreshold})`;
}
};

View file

@ -18,11 +18,6 @@ export interface RulesSettingsFlappingProperties {
statusChangeThreshold: number;
}
export interface RuleSpecificFlappingProperties {
lookBackWindow: number;
statusChangeThreshold: number;
}
export type RulesSettingsFlapping = RulesSettingsFlappingProperties &
RulesSettingsModificationMetadata;
@ -43,13 +38,6 @@ export interface RulesSettings {
queryDelay?: RulesSettingsQueryDelay;
}
export {
MIN_LOOK_BACK_WINDOW,
MAX_LOOK_BACK_WINDOW,
MIN_STATUS_CHANGE_THRESHOLD,
MAX_STATUS_CHANGE_THRESHOLD,
} from '@kbn/alerting-types';
export const MIN_QUERY_DELAY = 0;
export const MAX_QUERY_DELAY = 60;

View file

@ -225,6 +225,18 @@ export const alertMappings: SavedObjectsTypeMappingDefinition = {
},
},
// NO NEED TO BE INDEXED
// flapping: {
// index: false,
// properties: {
// lookBackWindow: {
// type: 'long',
// },
// statusChangeThreshold: {
// type: 'long',
// },
// },
// },
// NO NEED TO BE INDEXED
// nextRun: {
// type: 'date',
// },

View file

@ -3182,6 +3182,81 @@ describe('create()', () => {
expect(taskManager.schedule).toHaveBeenCalled();
});
test('should create rule with flapping', async () => {
const flapping = {
lookBackWindow: 10,
statusChangeThreshold: 10,
};
const data = getMockData({
name: 'my rule name',
flapping,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: RULE_SAVED_OBJECT_TYPE,
attributes: {
enabled: false,
name: ' my rule name ',
alertTypeId: '123',
schedule: { interval: 10000 },
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending(now),
running: false,
createdAt: now,
updatedAt: now,
actions: [],
flapping,
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
});
const result = await rulesClient.create({ data, isFlappingEnabled: true });
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
expect.objectContaining({
flapping,
}),
{
id: 'mock-saved-object-id',
references: [
{
id: '1',
name: 'action_0',
type: 'action',
},
],
}
);
expect(result.flapping).toEqual(flapping);
});
test('throws error when creating a rule with flapping if global flapping is disabled', async () => {
const flapping = {
lookBackWindow: 10,
statusChangeThreshold: 10,
};
const data = getMockData({
name: 'my rule name',
flapping,
});
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Error creating rule: can not create rule with flapping if global flapping is disabled"`
);
});
test('throws error when creating with an interval less than the minimum configured one when enforce = true', async () => {
rulesClient = new RulesClient({
...rulesClientParams,
@ -3770,7 +3845,7 @@ describe('create()', () => {
],
});
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to validate actions due to the following error: Action's alertsFilter must have either \\"query\\" or \\"timeframe\\" : 152"`
`"Failed to validate actions due to the following error: Action's alertsFilter must have either \\"query\\" or \\"timeframe\\" : 154"`
);
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
@ -3826,7 +3901,7 @@ describe('create()', () => {
],
});
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to validate actions due to the following error: This ruleType (Test) can't have an action with Alerts Filter. Actions: [153]"`
`"Failed to validate actions due to the following error: This ruleType (Test) can't have an action with Alerts Filter. Actions: [155]"`
);
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
@ -3898,7 +3973,7 @@ describe('create()', () => {
group: 'default',
actionTypeId: 'test',
params: { foo: true },
uuid: '154',
uuid: '156',
},
],
alertTypeId: '123',
@ -4127,13 +4202,13 @@ describe('create()', () => {
params: {
foo: true,
},
uuid: '156',
uuid: '158',
},
{
actionRef: 'system_action:system_action-id',
actionTypeId: '.test',
params: { foo: 'test' },
uuid: '157',
uuid: '159',
},
],
alertTypeId: '123',
@ -4205,13 +4280,13 @@ describe('create()', () => {
params: {
foo: true,
},
uuid: '158',
uuid: '160',
},
{
actionRef: 'system_action:system_action-id',
actionTypeId: '.test',
params: { foo: 'test' },
uuid: '159',
uuid: '161',
},
]);
});

View file

@ -48,6 +48,7 @@ export interface CreateRuleParams<Params extends RuleParams = never> {
data: CreateRuleData<Params>;
options?: CreateRuleOptions;
allowMissingConnectorSecrets?: boolean;
isFlappingEnabled?: boolean;
}
export async function createRule<Params extends RuleParams = never>(
@ -55,7 +56,13 @@ export async function createRule<Params extends RuleParams = never>(
createParams: CreateRuleParams<Params>
// TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed
): Promise<SanitizedRule<Params>> {
const { data: initialData, options, allowMissingConnectorSecrets } = createParams;
const {
data: initialData,
options,
allowMissingConnectorSecrets,
isFlappingEnabled = false,
} = createParams;
const actionsClient = await context.getActionsClient();
const { actions: genAction, systemActions: genSystemActions } = await addGeneratedActionValues(
@ -170,6 +177,12 @@ export async function createRule<Params extends RuleParams = never>(
);
}
if (initialData.flapping !== undefined && !isFlappingEnabled) {
throw Boom.badRequest(
'Error creating rule: can not create rule with flapping if global flapping is disabled'
);
}
const allActions = [...data.actions, ...(data.systemActions ?? [])];
// Extract saved object references for this rule
const {

View file

@ -12,6 +12,7 @@ import {
alertDelaySchema,
actionRequestSchema,
systemActionRequestSchema,
flappingSchema,
} from '../../../schemas';
export const createRuleDataSchema = schema.object(
@ -36,6 +37,7 @@ export const createRuleDataSchema = schema.object(
),
notifyWhen: schema.maybe(schema.nullable(notifyWhenSchema)),
alertDelay: schema.maybe(alertDelaySchema),
flapping: schema.maybe(schema.nullable(flappingSchema)),
},
{ unknowns: 'allow' }
);

View file

@ -24,4 +24,5 @@ export interface CreateRuleData<Params extends RuleParams = never> {
systemActions?: CreateRuleDataType['systemActions'];
notifyWhen?: CreateRuleDataType['notifyWhen'];
alertDelay?: CreateRuleDataType['alertDelay'];
flapping?: CreateRuleDataType['flapping'];
}

View file

@ -12,6 +12,7 @@ import {
alertDelaySchema,
actionRequestSchema,
systemActionRequestSchema,
flappingSchema,
} from '../../../schemas';
export const updateRuleDataSchema = schema.object(
@ -27,6 +28,7 @@ export const updateRuleDataSchema = schema.object(
systemActions: schema.maybe(schema.arrayOf(systemActionRequestSchema, { defaultValue: [] })),
notifyWhen: schema.maybe(schema.nullable(notifyWhenSchema)),
alertDelay: schema.maybe(alertDelaySchema),
flapping: schema.maybe(schema.nullable(flappingSchema)),
},
{ unknowns: 'allow' }
);

View file

@ -21,4 +21,5 @@ export interface UpdateRuleData<Params extends RuleParams = never> {
systemActions?: UpdateRuleDataType['systemActions'];
notifyWhen?: UpdateRuleDataType['notifyWhen'];
alertDelay?: UpdateRuleDataType['alertDelay'];
flapping?: UpdateRuleDataType['flapping'];
}

View file

@ -1769,6 +1769,100 @@ describe('update()', () => {
expect(rulesClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/my alert name');
});
it('should update rule flapping', async () => {
const flapping = {
lookBackWindow: 10,
statusChangeThreshold: 10,
};
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: RULE_SAVED_OBJECT_TYPE,
attributes: {
enabled: true,
schedule: { interval: '1m' },
params: {
bar: true,
},
actions: [],
notifyWhen: 'onActiveAlert',
revision: 1,
scheduledTaskId: 'task-123',
executionStatus: {
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
flapping,
},
references: [],
});
const result = await rulesClient.update({
id: '1',
data: {
schedule: { interval: '1m' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
throttle: null,
notifyWhen: 'onActiveAlert',
actions: [],
systemActions: [],
flapping,
},
isFlappingEnabled: true,
});
expect(unsecuredSavedObjectsClient.create).toHaveBeenNthCalledWith(
1,
RULE_SAVED_OBJECT_TYPE,
expect.objectContaining({
flapping,
}),
{
id: '1',
overwrite: true,
references: [],
version: '123',
}
);
expect(result.flapping).toEqual(flapping);
});
it('should throw error when updating a rule with flapping if global flapping is disabled', async () => {
const flapping = {
lookBackWindow: 10,
statusChangeThreshold: 10,
};
await expect(
rulesClient.update({
id: '1',
data: {
schedule: { interval: '1m' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
throttle: null,
notifyWhen: 'onActiveAlert',
actions: [],
systemActions: [],
flapping,
},
isFlappingEnabled: false,
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Error updating rule: can not update rule flapping if global flapping is disabled"`
);
});
it('swallows error when invalidate API key throws', async () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',

View file

@ -41,6 +41,36 @@ import { RuleAttributes } from '../../../../data/rule/types';
import { transformRuleAttributesToRuleDomain, transformRuleDomainToRule } from '../../transforms';
import { ruleDomainSchema } from '../../schemas';
const validateCanUpdateFlapping = (
isFlappingEnabled: boolean,
originalFlapping: RuleAttributes['flapping'],
updateFlapping: UpdateRuleParams['data']['flapping']
) => {
// If flapping is enabled, allow rule flapping to be updated and do nothing
if (isFlappingEnabled) {
return;
}
// If updated flapping is undefined then don't do anything, it's not being updated
if (updateFlapping === undefined) {
return;
}
// If both versions are falsy, allow it even if its changing between undefined and null
if (!originalFlapping && !updateFlapping) {
return;
}
// If both values are equal, allow it because it's essentially not changing anything
if (isEqual(originalFlapping, updateFlapping)) {
return;
}
throw Boom.badRequest(
`Error updating rule: can not update rule flapping if global flapping is disabled`
);
};
type ShouldIncrementRevision = (params?: RuleParams) => boolean;
export interface UpdateRuleParams<Params extends RuleParams = never> {
@ -48,6 +78,7 @@ export interface UpdateRuleParams<Params extends RuleParams = never> {
data: UpdateRuleData<Params>;
allowMissingConnectorSecrets?: boolean;
shouldIncrementRevision?: ShouldIncrementRevision;
isFlappingEnabled?: boolean;
}
export async function updateRule<Params extends RuleParams = never>(
@ -70,6 +101,7 @@ async function updateWithOCC<Params extends RuleParams = never>(
data: initialData,
allowMissingConnectorSecrets,
id,
isFlappingEnabled = false,
shouldIncrementRevision = () => true,
} = updateParams;
@ -114,8 +146,18 @@ async function updateWithOCC<Params extends RuleParams = never>(
systemActions: genSystemActions,
};
const { alertTypeId, consumer, enabled, schedule, name, apiKey, apiKeyCreatedByUser } =
originalRuleSavedObject.attributes;
const {
alertTypeId,
consumer,
enabled,
schedule,
name,
apiKey,
apiKeyCreatedByUser,
flapping: originalFlapping,
} = originalRuleSavedObject.attributes;
validateCanUpdateFlapping(isFlappingEnabled, originalFlapping, initialData.flapping);
let validationPayload: ValidateScheduleLimitResult = null;
if (enabled && schedule.interval !== data.schedule.interval) {

View file

@ -0,0 +1,13 @@
/*
* 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 { schema } from '@kbn/config-schema';
export const flappingSchema = schema.object({
lookBackWindow: schema.number(),
statusChangeThreshold: schema.number(),
});

View file

@ -8,3 +8,4 @@
export * from './rule_schemas';
export * from './action_schemas';
export * from './notify_when_schema';
export * from './flapping_schema';

View file

@ -16,6 +16,7 @@ import { rRuleSchema } from '../../r_rule/schemas';
import { dateSchema } from './date_schema';
import { notifyWhenSchema } from './notify_when_schema';
import { actionSchema, systemActionSchema } from './action_schemas';
import { flappingSchema } from './flapping_schema';
export const ruleParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
export const mappedParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
@ -177,6 +178,7 @@ export const ruleDomainSchema = schema.object({
viewInAppRelativeUrl: schema.maybe(schema.nullable(schema.string())),
alertDelay: schema.maybe(alertDelaySchema),
legacyId: schema.maybe(schema.nullable(schema.string())),
flapping: schema.maybe(schema.nullable(flappingSchema)),
});
/**
@ -217,4 +219,5 @@ export const ruleSchema = schema.object({
viewInAppRelativeUrl: schema.maybe(schema.nullable(schema.string())),
alertDelay: schema.maybe(alertDelaySchema),
legacyId: schema.maybe(schema.nullable(schema.string())),
flapping: schema.maybe(schema.nullable(flappingSchema)),
});

View file

@ -92,6 +92,10 @@ describe('transformRuleAttributesToRuleDomain', () => {
updatedBy: 'user',
apiKey: MOCK_API_KEY,
apiKeyOwner: 'user',
flapping: {
lookBackWindow: 20,
statusChangeThreshold: 20,
},
};
it('transforms the actions correctly', () => {
@ -106,6 +110,13 @@ describe('transformRuleAttributesToRuleDomain', () => {
isSystemAction
);
expect(res.flapping).toMatchInlineSnapshot(`
Object {
"lookBackWindow": 20,
"statusChangeThreshold": 20,
}
`);
expect(res.actions).toMatchInlineSnapshot(`
Array [
Object {

View file

@ -232,6 +232,7 @@ export const transformRuleAttributesToRuleDomain = <Params extends RuleParams =
running: esRule.running,
...(esRule.alertDelay ? { alertDelay: esRule.alertDelay } : {}),
...(esRule.legacyId !== undefined ? { legacyId: esRule.legacyId } : {}),
...(esRule.flapping !== undefined ? { flapping: esRule.flapping } : {}),
};
// Bad casts, but will fix once we fix all rule types

View file

@ -67,6 +67,10 @@ describe('transformRuleDomainToRule', () => {
updatedBy: 'user',
apiKey: MOCK_API_KEY,
apiKeyOwner: 'user',
flapping: {
lookBackWindow: 20,
statusChangeThreshold: 20,
},
};
it('should transform rule domain to rule', () => {
@ -100,6 +104,10 @@ describe('transformRuleDomainToRule', () => {
revision: 0,
updatedBy: 'user',
apiKeyOwner: 'user',
flapping: {
lookBackWindow: 20,
statusChangeThreshold: 20,
},
});
});
@ -135,6 +143,10 @@ describe('transformRuleDomainToRule', () => {
revision: 0,
updatedBy: 'user',
apiKeyOwner: 'user',
flapping: {
lookBackWindow: 20,
statusChangeThreshold: 20,
},
});
});
@ -172,6 +184,10 @@ describe('transformRuleDomainToRule', () => {
revision: 0,
updatedBy: 'user',
apiKeyOwner: 'user',
flapping: {
lookBackWindow: 20,
statusChangeThreshold: 20,
},
});
});
});

View file

@ -53,6 +53,7 @@ export const transformRuleDomainToRule = <Params extends RuleParams = never>(
viewInAppRelativeUrl: ruleDomain.viewInAppRelativeUrl,
alertDelay: ruleDomain.alertDelay,
legacyId: ruleDomain.legacyId,
flapping: ruleDomain.flapping,
};
if (isPublic) {

View file

@ -0,0 +1,122 @@
/*
* 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 { RuleDomain } from '../types';
import { transformRuleDomainToRuleAttributes } from './transform_rule_domain_to_rule_attributes';
describe('transformRuleDomainToRuleAttributes', () => {
const MOCK_API_KEY = Buffer.from('123:abc').toString('base64');
const defaultAction = {
id: '1',
uuid: 'test-uuid',
group: 'default',
actionTypeId: 'test',
params: {},
};
const rule: RuleDomain<{}> = {
id: 'test',
enabled: false,
name: 'my rule name',
tags: ['foo'],
alertTypeId: 'myType',
consumer: 'myApp',
schedule: { interval: '1m' },
actions: [defaultAction],
params: {},
mapped_params: {},
createdBy: 'user',
createdAt: new Date('2019-02-12T21:01:22.479Z'),
updatedAt: new Date('2019-02-12T21:01:22.479Z'),
legacyId: 'legacyId',
muteAll: false,
mutedInstanceIds: [],
snoozeSchedule: [],
scheduledTaskId: 'task-123',
executionStatus: {
lastExecutionDate: new Date('2019-02-12T21:01:22.479Z'),
status: 'pending' as const,
},
throttle: null,
notifyWhen: null,
revision: 0,
updatedBy: 'user',
apiKey: MOCK_API_KEY,
apiKeyOwner: 'user',
flapping: {
lookBackWindow: 20,
statusChangeThreshold: 20,
},
};
test('should transform rule domain to rule attribute', () => {
const result = transformRuleDomainToRuleAttributes({
rule,
actionsWithRefs: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
uuid: 'test-uuid',
params: {},
},
],
params: {
legacyId: 'test',
paramsWithRefs: {},
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"actionRef": "action_0",
"actionTypeId": "test",
"group": "default",
"params": Object {},
"uuid": "test-uuid",
},
],
"alertTypeId": "myType",
"apiKey": "MTIzOmFiYw==",
"apiKeyOwner": "user",
"consumer": "myApp",
"createdAt": "2019-02-12T21:01:22.479Z",
"createdBy": "user",
"enabled": false,
"executionStatus": Object {
"lastExecutionDate": "2019-02-12T21:01:22.479Z",
"status": "pending",
},
"flapping": Object {
"lookBackWindow": 20,
"statusChangeThreshold": 20,
},
"legacyId": "test",
"muteAll": false,
"mutedInstanceIds": Array [],
"name": "my rule name",
"notifyWhen": null,
"params": Object {},
"revision": 0,
"schedule": Object {
"interval": "1m",
},
"scheduledTaskId": "task-123",
"snoozeSchedule": Array [],
"tags": Array [
"foo",
],
"throttle": null,
"updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "user",
}
`);
});
});

View file

@ -80,5 +80,6 @@ export const transformRuleDomainToRuleAttributes = ({
revision: rule.revision,
...(rule.running !== undefined ? { running: rule.running } : {}),
...(rule.alertDelay !== undefined ? { alertDelay: rule.alertDelay } : {}),
...(rule.flapping !== undefined ? { flapping: rule.flapping } : {}),
};
};

View file

@ -84,6 +84,7 @@ export interface Rule<Params extends RuleParams = never> {
viewInAppRelativeUrl?: RuleSchemaType['viewInAppRelativeUrl'];
alertDelay?: RuleSchemaType['alertDelay'];
legacyId?: RuleSchemaType['legacyId'];
flapping?: RuleSchemaType['flapping'];
}
export interface RuleDomain<Params extends RuleParams = never> {
@ -122,4 +123,5 @@ export interface RuleDomain<Params extends RuleParams = never> {
viewInAppRelativeUrl?: RuleDomainSchemaType['viewInAppRelativeUrl'];
alertDelay?: RuleSchemaType['alertDelay'];
legacyId?: RuleSchemaType['legacyId'];
flapping?: RuleSchemaType['flapping'];
}

View file

@ -147,6 +147,11 @@ interface AlertDelayAttributes {
active: number;
}
interface FlappingAttributes {
lookBackWindow: number;
statusChangeThreshold: number;
}
export interface RuleAttributes {
name: string;
tags: string[];
@ -180,4 +185,5 @@ export interface RuleAttributes {
revision: number;
running?: boolean | null;
alertDelay?: AlertDelayAttributes;
flapping?: FlappingAttributes | null;
}

View file

@ -676,7 +676,10 @@ export class AlertingPlugin {
getRulesClient: () => {
return rulesClientFactory!.create(request, savedObjects);
},
getRulesSettingsClient: () => {
getRulesSettingsClient: (withoutAuth?: boolean) => {
if (withoutAuth) {
return rulesSettingsClientFactory.create(request);
}
return rulesSettingsClientFactory.createWithAuthorization(request);
},
getMaintenanceWindowClient: () => {

View file

@ -227,6 +227,7 @@ describe('createRuleRoute', () => {
],
"throttle": "30s",
},
"isFlappingEnabled": true,
"options": Object {
"id": undefined,
},
@ -343,6 +344,7 @@ describe('createRuleRoute', () => {
],
"throttle": "30s",
},
"isFlappingEnabled": true,
"options": Object {
"id": "custom-id",
},
@ -460,6 +462,7 @@ describe('createRuleRoute', () => {
],
"throttle": "30s",
},
"isFlappingEnabled": true,
"options": Object {
"id": "custom-id",
},
@ -577,6 +580,7 @@ describe('createRuleRoute', () => {
],
"throttle": "30s",
},
"isFlappingEnabled": true,
"options": Object {
"id": "custom-id",
},
@ -732,6 +736,7 @@ describe('createRuleRoute', () => {
],
"throttle": "30s",
},
"isFlappingEnabled": true,
"options": Object {
"id": undefined,
},

View file

@ -64,6 +64,7 @@ export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOpt
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
const actionsClient = (await context.actions).getActionsClient();
const rulesSettingsClient = (await context.alerting).getRulesSettingsClient(true);
// Assert versioned inputs
const createRuleData: CreateRuleRequestBodyV1<RuleParamsV1> = req.body;
@ -90,6 +91,8 @@ export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOpt
actionsClient.isSystemAction(action.id)
);
const flappingSettings = await rulesSettingsClient.flapping().get();
// TODO (http-versioning): Remove this cast, this enables us to move forward
// without fixing all of other solution types
const createdRule: Rule<RuleParamsV1> = (await rulesClient.create<RuleParamsV1>({
@ -98,6 +101,7 @@ export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOpt
actions,
systemActions,
}),
isFlappingEnabled: flappingSettings.enabled,
options: { id: params?.id },
})) as Rule<RuleParamsV1>;

View file

@ -66,6 +66,18 @@ const transformCreateBodySystemActions = (actions: CreateRuleActionV1[]): System
});
};
const transformCreateBodyFlapping = <Params extends RuleParams = never>(
flapping: CreateRuleRequestBodyV1<Params>['flapping']
) => {
if (!flapping) {
return flapping;
}
return {
lookBackWindow: flapping.look_back_window,
statusChangeThreshold: flapping.status_change_threshold,
};
};
export const transformCreateBody = <Params extends RuleParams = never>({
createBody,
actions,
@ -88,5 +100,8 @@ export const transformCreateBody = <Params extends RuleParams = never>({
systemActions: transformCreateBodySystemActions(systemActions),
...(createBody.notify_when ? { notifyWhen: createBody.notify_when } : {}),
...(createBody.alert_delay ? { alertDelay: createBody.alert_delay } : {}),
...(createBody.flapping !== undefined
? { flapping: transformCreateBodyFlapping(createBody.flapping) }
: {}),
};
};

View file

@ -16,6 +16,7 @@ import {
transformRuleActionsV1,
transformMonitoringV1,
transformRuleLastRunV1,
transformFlappingV1,
} from '../../../../transforms';
export const transformPartialRule = <Params extends RuleParams = never>(
@ -78,6 +79,7 @@ export const transformPartialRule = <Params extends RuleParams = never>(
? { view_in_app_relative_url: rule.viewInAppRelativeUrl }
: {}),
...(rule.alertDelay !== undefined ? { alert_delay: rule.alertDelay } : {}),
...(rule.flapping !== undefined ? { flapping: transformFlappingV1(rule.flapping) } : {}),
};
type RuleKeys = keyof RuleResponseV1<RuleParamsV1>;

View file

@ -70,6 +70,18 @@ export const transformUpdateBodySystemActions = (
});
};
const transformUpdateBodyFlapping = <Params extends RuleParams = never>(
flapping: UpdateRuleRequestBodyV1<Params>['flapping']
) => {
if (!flapping) {
return flapping;
}
return {
lookBackWindow: flapping.look_back_window,
statusChangeThreshold: flapping.status_change_threshold,
};
};
export const transformUpdateBody = <Params extends RuleParams = never>({
updateBody,
actions,
@ -89,5 +101,8 @@ export const transformUpdateBody = <Params extends RuleParams = never>({
systemActions: transformUpdateBodySystemActions(systemActions),
...(updateBody.notify_when ? { notifyWhen: updateBody.notify_when } : {}),
...(updateBody.alert_delay ? { alertDelay: updateBody.alert_delay } : {}),
...(updateBody.flapping !== undefined
? { flapping: transformUpdateBodyFlapping(updateBody.flapping) }
: {}),
};
};

View file

@ -215,6 +215,7 @@ describe('updateRuleRoute', () => {
"throttle": "10m",
},
"id": "1",
"isFlappingEnabled": true,
},
]
`);

View file

@ -66,6 +66,7 @@ export const updateRuleRoute = (
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
const actionsClient = (await context.actions).getActionsClient();
const rulesSettingsClient = (await context.alerting).getRulesSettingsClient(true);
// Assert versioned inputs
const updateRuleData: UpdateRuleRequestBodyV1<RuleParamsV1> = req.body;
@ -86,6 +87,8 @@ export const updateRuleRoute = (
actionsClient.isSystemAction(action.id)
);
const flappingSettings = await rulesSettingsClient.flapping().get();
// TODO (http-versioning): Remove this cast, this enables us to move forward
// without fixing all of other solution types
const updatedRule: Rule<RuleParamsV1> = (await rulesClient.update<RuleParamsV1>({
@ -95,6 +98,7 @@ export const updateRuleRoute = (
actions,
systemActions,
}),
isFlappingEnabled: flappingSettings.enabled,
})) as Rule<RuleParamsV1>;
// Assert versioned response type

View file

@ -10,10 +10,12 @@ export {
transformRuleActions,
transformRuleLastRun,
transformMonitoring,
transformFlapping,
} from './transform_rule_to_rule_response/latest';
export {
transformRuleToRuleResponse as transformRuleToRuleResponseV1,
transformRuleActions as transformRuleActionsV1,
transformRuleLastRun as transformRuleLastRunV1,
transformMonitoring as transformMonitoringV1,
transformFlapping as transformFlappingV1,
} from './transform_rule_to_rule_response/v1';

View file

@ -88,6 +88,17 @@ export const transformRuleActions = (
];
};
export const transformFlapping = (flapping: Rule['flapping']) => {
if (!flapping) {
return flapping;
}
return {
look_back_window: flapping.lookBackWindow,
status_change_threshold: flapping.statusChangeThreshold,
};
};
export const transformRuleToRuleResponse = <Params extends RuleParams = never>(
rule: Rule<Params>
): RuleResponseV1<RuleParamsV1> => ({
@ -144,4 +155,5 @@ export const transformRuleToRuleResponse = <Params extends RuleParams = never>(
? { view_in_app_relative_url: rule.viewInAppRelativeUrl }
: {}),
...(rule.alertDelay !== undefined ? { alert_delay: rule.alertDelay } : {}),
...(rule.flapping !== undefined ? { flapping: transformFlapping(rule.flapping) } : {}),
});

View file

@ -122,7 +122,7 @@ function getPartialRuleFromRaw<Params extends RuleTypeParams>(
actions,
snoozeSchedule,
lastRun,
isSnoozedUntil: DoNotUseIsSNoozedUntil,
isSnoozedUntil: DoNotUseIsSnoozedUntil,
...partialRawRule
} = rawRule;

View file

@ -12,15 +12,17 @@ import {
SavedObject,
SavedObjectsErrorHelpers,
} from '@kbn/core/server';
import {
MAX_LOOK_BACK_WINDOW,
MAX_STATUS_CHANGE_THRESHOLD,
MIN_LOOK_BACK_WINDOW,
MIN_STATUS_CHANGE_THRESHOLD,
} from '@kbn/alerting-types/flapping/latest';
import {
RulesSettings,
RulesSettingsFlapping,
RulesSettingsFlappingProperties,
RulesSettingsModificationMetadata,
MIN_LOOK_BACK_WINDOW,
MAX_LOOK_BACK_WINDOW,
MIN_STATUS_CHANGE_THRESHOLD,
MAX_STATUS_CHANGE_THRESHOLD,
RULES_SETTINGS_SAVED_OBJECT_TYPE,
RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
DEFAULT_FLAPPING_SETTINGS,

View file

@ -11,6 +11,7 @@ import {
RulesSettingsQueryDelayClientApi,
DEFAULT_FLAPPING_SETTINGS,
DEFAULT_QUERY_DELAY_SETTINGS,
RulesSettingsFlappingProperties,
} from '../types';
export type RulesSettingsClientMock = jest.Mocked<RulesSettingsClientApi>;
@ -19,9 +20,9 @@ export type RulesSettingsQueryDelayClientMock = jest.Mocked<RulesSettingsQueryDe
// Warning: Becareful when resetting all mocks in tests as it would clear
// the mock return value on the flapping
const createRulesSettingsClientMock = () => {
const createRulesSettingsClientMock = (flappingOverride?: RulesSettingsFlappingProperties) => {
const flappingMocked: RulesSettingsFlappingClientMock = {
get: jest.fn().mockReturnValue(DEFAULT_FLAPPING_SETTINGS),
get: jest.fn().mockReturnValue(flappingOverride || DEFAULT_FLAPPING_SETTINGS),
update: jest.fn(),
};
const queryDelayMocked: RulesSettingsQueryDelayClientMock = {
@ -36,7 +37,8 @@ const createRulesSettingsClientMock = () => {
};
export const rulesSettingsClientMock: {
create: () => RulesSettingsClientMock;
create: (flappingOverride?: RulesSettingsFlappingProperties) => RulesSettingsClientMock;
} = {
create: createRulesSettingsClientMock,
create: (flappingOverride?: RulesSettingsFlappingProperties) =>
createRulesSettingsClientMock(flappingOverride),
};

View file

@ -6,7 +6,7 @@
*/
import { SavedObjectsModelVersionMap } from '@kbn/core-saved-objects-server';
import { rawRuleSchemaV1 } from '../schemas/raw_rule';
import { rawRuleSchemaV1, rawRuleSchemaV2 } from '../schemas/raw_rule';
export const ruleModelVersions: SavedObjectsModelVersionMap = {
'1': {
@ -16,4 +16,11 @@ export const ruleModelVersions: SavedObjectsModelVersionMap = {
create: rawRuleSchemaV1,
},
},
'2': {
changes: [],
schemas: {
forwardCompatibility: rawRuleSchemaV2.extends({}, { unknowns: 'ignore' }),
create: rawRuleSchemaV2,
},
},
};

View file

@ -6,3 +6,4 @@
*/
export { rawRuleSchema as rawRuleSchemaV1 } from './v1';
export { rawRuleSchema as rawRuleSchemaV2 } from './v2';

View file

@ -0,0 +1,18 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { rawRuleSchema as rawRuleSchemaV1 } from './v1';
export const flappingSchema = schema.object({
lookBackWindow: schema.number(),
statusChangeThreshold: schema.number(),
});
export const rawRuleSchema = rawRuleSchemaV1.extends({
flapping: schema.maybe(schema.nullable(flappingSchema)),
});

View file

@ -290,11 +290,22 @@ export class TaskRunner<
state: { previousStartedAt },
} = this.taskInstance;
const { queryDelaySettings, flappingSettings } =
const { queryDelaySettings, flappingSettings: spaceFlappingSettings } =
await this.context.rulesSettingsService.getSettings(fakeRequest, spaceId);
const ruleRunMetricsStore = new RuleRunMetricsStore();
const ruleLabel = `${this.ruleType.id}:${ruleId}: '${rule.name}'`;
const ruleFlappingSettings = rule.flapping
? {
enabled: true,
...rule.flapping,
}
: null;
const flappingSettings = spaceFlappingSettings.enabled
? ruleFlappingSettings || spaceFlappingSettings
: spaceFlappingSettings;
const ruleTypeRunnerContext = {
alertingEventLogger: this.alertingEventLogger,
flappingSettings,

View file

@ -15,6 +15,7 @@ import {
AlertInstanceContext,
Rule,
RuleAlertData,
RawRule,
MaintenanceWindowStatus,
DEFAULT_FLAPPING_SETTINGS,
DEFAULT_QUERY_DELAY_SETTINGS,
@ -105,6 +106,7 @@ import {
import { backfillClientMock } from '../backfill_client/backfill_client.mock';
import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry';
import { createTaskRunnerLogger } from './lib';
import { SavedObject } from '@kbn/core/server';
import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock';
import { getMockMaintenanceWindow } from '../data/maintenance_window/test_helpers';
import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock';
@ -824,6 +826,112 @@ describe('Task Runner', () => {
spy1.mockRestore();
spy2.mockRestore();
});
test('should use rule specific flapping settings if global flapping is enabled', async () => {
mockAlertsService.createAlertsClient.mockImplementation(() => mockAlertsClient);
mockAlertsClient.getAlertsToSerialize.mockResolvedValue({
alertsToReturn: {},
recoveredAlertsToReturn: {},
});
const taskRunner = new TaskRunner({
ruleType: ruleTypeWithAlerts,
internalSavedObjectsRepository,
taskInstance: {
...mockedTaskInstance,
state: {
...mockedTaskInstance.state,
previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
},
},
context: taskRunnerFactoryInitializerParams,
inMemoryMetrics,
});
const ruleSpecificFlapping = {
lookBackWindow: 10,
statusChangeThreshold: 10,
};
mockGetAlertFromRaw.mockReturnValue({
...mockedRuleTypeSavedObject,
flapping: ruleSpecificFlapping,
} as Rule);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({
...mockedRawRuleSO,
flapping: ruleSpecificFlapping,
} as SavedObject<RawRule>);
await taskRunner.run();
expect(mockAlertsClient.initializeExecution).toHaveBeenCalledWith(
expect.objectContaining({
flappingSettings: {
enabled: true,
...ruleSpecificFlapping,
},
})
);
});
test('should not use rule specific flapping settings if global flapping is disabled', async () => {
rulesSettingsService.getSettings.mockResolvedValue({
flappingSettings: {
enabled: false,
lookBackWindow: 20,
statusChangeThreshold: 20,
},
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
});
mockAlertsService.createAlertsClient.mockImplementation(() => mockAlertsClient);
mockAlertsClient.getAlertsToSerialize.mockResolvedValue({
alertsToReturn: {},
recoveredAlertsToReturn: {},
});
const taskRunner = new TaskRunner({
ruleType: ruleTypeWithAlerts,
internalSavedObjectsRepository,
taskInstance: {
...mockedTaskInstance,
state: {
...mockedTaskInstance.state,
previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
},
},
context: taskRunnerFactoryInitializerParams,
inMemoryMetrics,
});
const ruleSpecificFlapping = {
lookBackWindow: 10,
statusChangeThreshold: 10,
};
mockGetAlertFromRaw.mockReturnValue({
...mockedRuleTypeSavedObject,
flapping: ruleSpecificFlapping,
} as Rule);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({
...mockedRawRuleSO,
flapping: ruleSpecificFlapping,
} as SavedObject<RawRule>);
await taskRunner.run();
expect(mockAlertsClient.initializeExecution).toHaveBeenCalledWith(
expect.objectContaining({
flappingSettings: {
enabled: false,
lookBackWindow: 20,
statusChangeThreshold: 20,
},
})
);
});
});
function testCorrectAlertsClientUsed<

View file

@ -11,7 +11,7 @@ import type {
SavedObjectReference,
IUiSettingsClient,
} from '@kbn/core/server';
import z from '@kbn/zod';
import { z } from '@kbn/zod';
import { DataViewsContract } from '@kbn/data-views-plugin/common';
import { ISearchStartSearchSource } from '@kbn/data-plugin/common';
import { LicenseType } from '@kbn/licensing-plugin/server';
@ -64,6 +64,7 @@ import {
AlertsFilterTimeframe,
RuleAlertData,
AlertDelay,
Flapping,
} from '../common';
import { PublicAlertFactory } from './alert/create_alert_factory';
import { RulesSettingsFlappingProperties } from '../common/rules_settings';
@ -77,7 +78,7 @@ export type { RuleTypeParams };
*/
export interface AlertingApiRequestHandlerContext {
getRulesClient: () => RulesClient;
getRulesSettingsClient: () => RulesSettingsClient;
getRulesSettingsClient: (withoutAuth?: boolean) => RulesSettingsClient;
getMaintenanceWindowClient: () => MaintenanceWindowClient;
listTypes: RuleTypeRegistry['list'];
getFrameworkHealth: () => Promise<AlertsHealth>;
@ -505,6 +506,7 @@ export interface RawRule extends SavedObjectAttributes {
revision: number;
running?: boolean | null;
alertDelay?: AlertDelay;
flapping?: Flapping | null;
}
export type { DataStreamAdapter } from './alerts_service/lib/data_stream_adapter';

View file

@ -57,6 +57,16 @@ const transformLastRun: RewriteRequestCase<RuleLastRun> = ({
...rest,
});
const transformFlapping = (flapping: AsApiContract<Rule['flapping']>) => {
if (!flapping) {
return flapping;
}
return {
lookBackWindow: flapping.look_back_window,
statusChangeThreshold: flapping.status_change_threshold,
};
};
export const transformRule: RewriteRequestCase<Rule> = ({
rule_type_id: ruleTypeId,
created_by: createdBy,
@ -77,6 +87,7 @@ export const transformRule: RewriteRequestCase<Rule> = ({
last_run: lastRun,
next_run: nextRun,
alert_delay: alertDelay,
flapping,
...rest
}: any) => ({
ruleTypeId,
@ -100,6 +111,7 @@ export const transformRule: RewriteRequestCase<Rule> = ({
...(nextRun ? { nextRun } : {}),
...(apiKeyCreatedByUser !== undefined ? { apiKeyCreatedByUser } : {}),
...(alertDelay ? { alertDelay } : {}),
...(flapping !== undefined ? { flapping: transformFlapping(flapping) } : {}),
...rest,
});

View file

@ -62,7 +62,6 @@ import {
isActionGroupDisabledForActionTypeId,
RuleActionAlertsFilterProperty,
RuleActionKey,
RuleSpecificFlappingProperties,
} from '@kbn/alerting-plugin/common';
import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common';
import { AlertConsumers } from '@kbn/rule-data-utils';
@ -415,10 +414,6 @@ export const RuleForm = ({
dispatch({ command: { type: 'setAlertDelayProperty' }, payload: { key, value } });
};
const setFlapping = (flapping: RuleSpecificFlappingProperties | null) => {
dispatch({ command: { type: 'setProperty' }, payload: { key: 'flapping', value: flapping } });
};
const onAlertDelayChange = (value: string) => {
const parsedValue = value === '' ? '' : parseInt(value, 10);
setAlertDelayProperty('active', parsedValue || 1);
@ -887,7 +882,7 @@ export const RuleForm = ({
alertDelay={alertDelay}
flappingSettings={rule.flapping}
onAlertDelayChange={onAlertDelayChange}
onFlappingChange={setFlapping}
onFlappingChange={(flapping) => setRuleProperty('flapping', flapping)}
enabledFlapping={IS_RULE_SPECIFIC_FLAPPING_ENABLED}
/>
</EuiAccordion>

View file

@ -31,8 +31,9 @@ import {
} from '@elastic/eui';
import { RuleSettingsFlappingInputs } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_inputs';
import { RuleSettingsFlappingMessage } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_message';
import { RuleSpecificFlappingProperties } from '@kbn/alerting-plugin/common';
import { Rule } from '@kbn/alerts-ui-shared';
import { FormattedMessage } from '@kbn/i18n-react';
import { Flapping } from '@kbn/alerting-plugin/common';
import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings';
import { useKibana } from '../../../common/lib/kibana';
@ -146,7 +147,10 @@ const flappingTitlePopoverLookBack = i18n.translate(
}
);
const clampFlappingValues = (flapping: RuleSpecificFlappingProperties) => {
const clampFlappingValues = (flapping: Rule['flapping']) => {
if (!flapping) {
return;
}
return {
...flapping,
statusChangeThreshold: Math.min(flapping.lookBackWindow, flapping.statusChangeThreshold),
@ -157,9 +161,9 @@ const INTEGER_REGEX = /^[1-9][0-9]*$/;
export interface RuleFormAdvancedOptionsProps {
alertDelay?: number;
flappingSettings?: RuleSpecificFlappingProperties;
flappingSettings?: Flapping | null;
onAlertDelayChange: (value: string) => void;
onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void;
onFlappingChange: (value: Flapping | null) => void;
enabledFlapping?: boolean;
}
@ -183,7 +187,7 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) =>
const [isFlappingOffPopoverOpen, setIsFlappingOffPopoverOpen] = useState<boolean>(false);
const [isFlappingTitlePopoverOpen, setIsFlappingTitlePopoverOpen] = useState<boolean>(false);
const cachedFlappingSettings = useRef<RuleSpecificFlappingProperties>();
const cachedFlappingSettings = useRef<Flapping>();
const isDesktop = useIsWithinMinBreakpoint('xl');
@ -204,8 +208,11 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) =>
);
const internalOnFlappingChange = useCallback(
(flapping: RuleSpecificFlappingProperties) => {
(flapping: Flapping) => {
const clampedValue = clampFlappingValues(flapping);
if (!clampedValue) {
return;
}
onFlappingChange(clampedValue);
cachedFlappingSettings.current = clampedValue;
},
@ -407,7 +414,7 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) =>
{flappingOffTooltip}
</EuiFlexItem>
</EuiFlexGroup>
{flappingSettings && (
{flappingSettings && enabled && (
<>
<EuiSpacer size="m" />
<EuiHorizontalRule margin="none" />
@ -425,6 +432,9 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) =>
]);
const flappingFormBody = useMemo(() => {
if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) {
return null;
}
if (!flappingSettings) {
return null;
}
@ -438,7 +448,12 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) =>
/>
</EuiFlexItem>
);
}, [flappingSettings, onLookBackWindowChange, onStatusChangeThresholdChange]);
}, [
flappingSettings,
spaceFlappingSettings,
onLookBackWindowChange,
onStatusChangeThresholdChange,
]);
const flappingFormMessage = useMemo(() => {
if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) {

View file

@ -66,6 +66,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
params: {},
},
],
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
})
);
@ -126,6 +130,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
muted_alert_ids: [],
execution_status: response.body.execution_status,
revision: 0,
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
...(response.body.next_run ? { next_run: response.body.next_run } : {}),
...(response.body.last_run ? { last_run: response.body.last_run } : {}),
});

View file

@ -80,6 +80,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
};
const response = await supertestWithoutAuth
.put(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}`)
@ -138,6 +142,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
revision: 1,
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
...(response.body.next_run ? { next_run: response.body.next_run } : {}),
...(response.body.last_run ? { last_run: response.body.last_run } : {}),
});
@ -185,6 +193,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
};
const response = await supertestWithoutAuth
.put(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}`)
@ -231,6 +243,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
revision: 1,
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
...(response.body.next_run ? { next_run: response.body.next_run } : {}),
...(response.body.last_run ? { last_run: response.body.last_run } : {}),
});
@ -278,6 +294,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
};
const response = await supertestWithoutAuth
.put(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}`)
@ -324,6 +344,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
revision: 1,
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
...(response.body.next_run ? { next_run: response.body.next_run } : {}),
...(response.body.last_run ? { last_run: response.body.last_run } : {}),
});
@ -371,6 +395,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
};
const response = await supertestWithoutAuth
.put(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}`)
@ -424,6 +452,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
revision: 1,
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
...(response.body.next_run ? { next_run: response.body.next_run } : {}),
...(response.body.last_run ? { last_run: response.body.last_run } : {}),
});
@ -480,6 +512,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
};
const response = await supertestWithoutAuth
.put(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}`)
@ -522,6 +558,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
revision: 1,
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
...(response.body.next_run ? { next_run: response.body.next_run } : {}),
...(response.body.last_run ? { last_run: response.body.last_run } : {}),
});
@ -564,6 +604,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
actions: [],
throttle: '1m',
notify_when: 'onActiveAlert',
flapping: {
look_back_window: 10,
status_change_threshold: 10,
},
};
const response = await supertestWithoutAuth
.put(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}`)

View file

@ -19,6 +19,7 @@ import {
ObjectRemover,
getUnauthorizedErrorMessage,
TaskManagerDoc,
resetRulesSettings,
} from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
@ -635,6 +636,90 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
)
.expect(400);
});
describe('create rule flapping', () => {
afterEach(async () => {
await resetRulesSettings(supertest, 'space1');
});
it('should allow flapping to be created', async () => {
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
flapping: {
look_back_window: 5,
status_change_threshold: 5,
},
})
);
expect(response.status).to.eql(200);
objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting');
expect(response.body.flapping).to.eql({
look_back_window: 5,
status_change_threshold: 5,
});
});
it('should throw if flapping is created when global flapping is off', async () => {
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.send({
enabled: false,
look_back_window: 5,
status_change_threshold: 5,
});
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
flapping: {
look_back_window: 5,
status_change_threshold: 5,
},
})
);
expect(response.statusCode).eql(400);
expect(response.body.message).eql(
'Error creating rule: can not create rule with flapping if global flapping is disabled'
);
});
it('should throw if flapping is invalid', async () => {
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
flapping: {
look_back_window: 5,
status_change_threshold: 10,
},
})
)
.expect(400);
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
flapping: {
look_back_window: -5,
status_change_threshold: -5,
},
})
)
.expect(400);
});
});
});
describe('legacy', function () {

View file

@ -8,7 +8,13 @@
import expect from '@kbn/expect';
import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server';
import { Spaces } from '../../../scenarios';
import { checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib';
import {
checkAAD,
getUrlPrefix,
getTestRuleData,
ObjectRemover,
resetRulesSettings,
} from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
@ -177,6 +183,233 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
});
});
describe('update rule flapping', () => {
afterEach(async () => {
await resetRulesSettings(supertest, 'space1');
});
it('should allow flapping to be updated', async () => {
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData());
expect(response.body.flapping).eql(undefined);
objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting');
const { body: updatedRule } = await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${response.body.id}`)
.set('kbn-xsrf', 'foo')
.send({
name: 'bcd',
tags: ['foo'],
params: {
foo: true,
},
schedule: { interval: '12s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: 5,
status_change_threshold: 5,
},
});
expect(updatedRule.flapping).eql({
look_back_window: 5,
status_change_threshold: 5,
});
});
it('should allow flapping to be removed via update', async () => {
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
flapping: {
look_back_window: 5,
status_change_threshold: 5,
},
})
);
expect(response.body.flapping).eql({
look_back_window: 5,
status_change_threshold: 5,
});
objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting');
const { body: updatedRule } = await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${response.body.id}`)
.set('kbn-xsrf', 'foo')
.send({
name: 'bcd',
tags: ['foo'],
params: {
foo: true,
},
schedule: { interval: '12s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: null,
});
expect(updatedRule.flapping).eql(null);
});
it('should throw if flapping is updated when global flapping is off', async () => {
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData());
objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting');
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.send({
enabled: false,
look_back_window: 5,
status_change_threshold: 5,
});
await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${response.body.id}`)
.set('kbn-xsrf', 'foo')
.send({
name: 'bcd',
tags: ['foo'],
params: {
foo: true,
},
schedule: { interval: '12s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: 5,
status_change_threshold: 5,
},
})
.expect(400);
});
it('should allow rule to be updated when global flapping is off if not updating flapping', async () => {
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
flapping: {
look_back_window: 5,
status_change_threshold: 5,
},
})
);
objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting');
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.send({
enabled: false,
look_back_window: 5,
status_change_threshold: 5,
});
await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${response.body.id}`)
.set('kbn-xsrf', 'foo')
.send({
name: 'updated name 1',
tags: ['foo'],
params: {
foo: true,
},
schedule: { interval: '12s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
})
.expect(200);
await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${response.body.id}`)
.set('kbn-xsrf', 'foo')
.send({
name: 'updated name 2',
tags: ['foo'],
params: {
foo: true,
},
schedule: { interval: '12s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: 5,
status_change_threshold: 5,
},
})
.expect(200);
});
it('should throw if flapping is invalid', async () => {
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData());
objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting');
await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${response.body.id}`)
.set('kbn-xsrf', 'foo')
.send({
name: 'bcd',
tags: ['foo'],
params: {
foo: true,
},
schedule: { interval: '12s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: 5,
status_change_threshold: 10,
},
})
.expect(400);
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send({
name: 'bcd',
tags: ['foo'],
params: {
foo: true,
},
schedule: { interval: '12s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
flapping: {
look_back_window: -5,
status_change_threshold: -5,
},
})
.expect(400);
});
});
describe('legacy', function () {
this.tags('skipFIPS');
it('should handle update alert request appropriately', async () => {

View file

@ -18,6 +18,7 @@ import {
getTestRuleData,
getUrlPrefix,
ObjectRemover,
resetRulesSettings,
TaskManagerDoc,
} from '../../../../../common/lib';
import { TEST_CACHE_EXPIRATION_TIME } from '../../create_test_data';
@ -45,6 +46,10 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid
await objectRemover.removeAll();
});
after(async () => {
await resetRulesSettings(supertestWithoutAuth, 'space1');
});
// These are the same tests from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts
// but testing that flapping status & flapping history is updated as expected for AAD docs
@ -535,6 +540,203 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid
expect(alertDocs[0]._source![ALERT_FLAPPING]).to.equal(false);
expect(state.alertInstances.alertA.meta.flapping).to.equal(false);
});
it('should allow rule specific flapping to override space flapping', async () => {
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.auth('superuser', 'superuser')
.send({
enabled: true,
look_back_window: 10,
status_change_threshold: 2,
})
.expect(200);
const pattern = {
alertA: [true, false, true, false, true, false, true, false],
};
const ruleParameters = { pattern };
const createdRule1 = await supertestWithoutAuth
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.patternFiringAad',
// set the schedule long so we can use "runSoon" to specify rule runs
schedule: { interval: '1d' },
throttle: null,
params: ruleParameters,
actions: [],
notify_when: RuleNotifyWhen.CHANGE,
})
)
.expect(200);
const rule1Id = createdRule1.body.id;
objectRemover.add(Spaces.space1.id, rule1Id, 'rule', 'alerting');
// Wait for the rule to run once
let run = 1;
let runWhichItFlapped = 0;
await waitForEventLogDocs(rule1Id, new Map([['execute', { equal: 1 }]]));
// Run them all
for (let i = 0; i < 7; i++) {
await retry.try(async () => {
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${rule1Id}/_run_soon`)
.set('kbn-xsrf', 'foo');
expect(response.status).to.eql(204);
});
await waitForEventLogDocs(rule1Id, new Map([['execute', { equal: ++run }]]));
const alertDocs = await queryForAlertDocs<PatternFiringAlert>(rule1Id);
const isFlapping = alertDocs[0]._source![ALERT_FLAPPING];
if (!runWhichItFlapped && isFlapping) {
runWhichItFlapped = run;
}
}
// Flapped on the 4th run
expect(runWhichItFlapped).eql(4);
// Create a rule with flapping
const createdRule2 = await supertestWithoutAuth
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.patternFiringAad',
// set the schedule long so we can use "runSoon" to specify rule runs
schedule: { interval: '1d' },
throttle: null,
params: ruleParameters,
actions: [],
notify_when: RuleNotifyWhen.CHANGE,
flapping: {
look_back_window: 10,
status_change_threshold: 4,
},
})
)
.expect(200);
const rule2Id = createdRule2.body.id;
objectRemover.add(Spaces.space1.id, rule2Id, 'rule', 'alerting');
// Wait for the rule to run once
run = 1;
runWhichItFlapped = 0;
await waitForEventLogDocs(rule2Id, new Map([['execute', { equal: 1 }]]));
// Run them all
for (let i = 0; i < 7; i++) {
await retry.try(async () => {
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${rule2Id}/_run_soon`)
.set('kbn-xsrf', 'foo');
expect(response.status).to.eql(204);
});
await waitForEventLogDocs(rule2Id, new Map([['execute', { equal: ++run }]]));
const alertDocs = await queryForAlertDocs<PatternFiringAlert>(rule2Id);
const isFlapping = alertDocs[0]._source![ALERT_FLAPPING];
if (!runWhichItFlapped && isFlapping) {
runWhichItFlapped = run;
}
}
// Flapped on the 6th run, which is more than the space status change threshold
expect(runWhichItFlapped).eql(6);
});
it('should ignore rule flapping if the space flapping is disabled', async () => {
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.auth('superuser', 'superuser')
.send({
enabled: true,
look_back_window: 10,
status_change_threshold: 2,
})
.expect(200);
const pattern = {
alertA: [true, false, true, false, true, false, true, false],
};
const ruleParameters = { pattern };
// Create a rule with flapping
const createdRule = await supertestWithoutAuth
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.patternFiringAad',
// set the schedule long so we can use "runSoon" to specify rule runs
schedule: { interval: '1d' },
throttle: null,
params: ruleParameters,
actions: [],
notify_when: RuleNotifyWhen.CHANGE,
flapping: {
look_back_window: 10,
status_change_threshold: 4,
},
})
)
.expect(200);
const ruleId = createdRule.body.id;
objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting');
// Turn global flapping off, need to do this after the rule is created because
// we do not allow rules to be created with flapping if global flapping is off.
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.auth('superuser', 'superuser')
.send({
enabled: false,
look_back_window: 10,
status_change_threshold: 2,
})
.expect(200);
// Wait for the rule to run once
let run = 1;
let runWhichItFlapped = 0;
await waitForEventLogDocs(ruleId, new Map([['execute', { equal: 1 }]]));
// Run them all
for (let i = 0; i < 7; i++) {
await retry.try(async () => {
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${ruleId}/_run_soon`)
.set('kbn-xsrf', 'foo');
expect(response.status).to.eql(204);
});
await waitForEventLogDocs(ruleId, new Map([['execute', { equal: ++run }]]));
const alertDocs = await queryForAlertDocs<PatternFiringAlert>(ruleId);
const isFlapping = alertDocs[0]._source![ALERT_FLAPPING];
if (!runWhichItFlapped && isFlapping) {
runWhichItFlapped = run;
}
}
// Never flapped, since globl flapping is off
expect(runWhichItFlapped).eql(0);
});
});
async function getRuleState(ruleId: string) {