diff --git a/packages/kbn-alerts-as-data-utils/src/field_maps/alert_field_map.ts b/packages/kbn-alerts-as-data-utils/src/field_maps/alert_field_map.ts index 07ada8b7c06b5..48320fd29e474 100644 --- a/packages/kbn-alerts-as-data-utils/src/field_maps/alert_field_map.ts +++ b/packages/kbn-alerts-as-data-utils/src/field_maps/alert_field_map.ts @@ -14,6 +14,7 @@ import { ALERT_FLAPPING, ALERT_FLAPPING_HISTORY, ALERT_MAINTENANCE_WINDOW_IDS, + ALERT_CONSECUTIVE_MATCHES, ALERT_INSTANCE_ID, ALERT_LAST_DETECTED, ALERT_REASON, @@ -80,6 +81,11 @@ export const alertFieldMap = { array: true, required: false, }, + [ALERT_CONSECUTIVE_MATCHES]: { + type: 'long', + array: false, + required: false, + }, [ALERT_INSTANCE_ID]: { type: 'keyword', array: false, diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/alert_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/alert_schema.ts index b183ca5c792f8..7d1f9304eaa34 100644 --- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/alert_schema.ts +++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/alert_schema.ts @@ -86,6 +86,7 @@ const AlertOptional = rt.partial({ 'event.kind': schemaString, 'kibana.alert.action_group': schemaString, 'kibana.alert.case_ids': schemaStringArray, + 'kibana.alert.consecutive_matches': schemaStringOrNumber, 'kibana.alert.duration.us': schemaStringOrNumber, 'kibana.alert.end': schemaDate, 'kibana.alert.flapping': schemaBoolean, diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts index bc8150356e039..c57f9862f4327 100644 --- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts +++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts @@ -127,6 +127,7 @@ const SecurityAlertOptional = rt.partial({ 'kibana.alert.ancestors.rule': schemaString, 'kibana.alert.building_block_type': schemaString, 'kibana.alert.case_ids': schemaStringArray, + 'kibana.alert.consecutive_matches': schemaStringOrNumber, 'kibana.alert.duration.us': schemaStringOrNumber, 'kibana.alert.end': schemaDate, 'kibana.alert.flapping': schemaBoolean, diff --git a/packages/kbn-rule-data-utils/src/default_alerts_as_data.ts b/packages/kbn-rule-data-utils/src/default_alerts_as_data.ts index 7c08271478131..dfd51bf737583 100644 --- a/packages/kbn-rule-data-utils/src/default_alerts_as_data.ts +++ b/packages/kbn-rule-data-utils/src/default_alerts_as_data.ts @@ -43,6 +43,9 @@ const ALERT_FLAPPING_HISTORY = `${ALERT_NAMESPACE}.flapping_history` as const; // kibana.alert.maintenance_window_ids - IDs of maintenance windows that are affecting this alert const ALERT_MAINTENANCE_WINDOW_IDS = `${ALERT_NAMESPACE}.maintenance_window_ids` as const; +// kibana.alert.consecutive_matches - count of consecutive times the alert has been active +const ALERT_CONSECUTIVE_MATCHES = `${ALERT_NAMESPACE}.consecutive_matches` as const; + // kibana.alert.instance.id - alert ID, also known as alert instance ID const ALERT_INSTANCE_ID = `${ALERT_NAMESPACE}.instance.id` as const; @@ -120,6 +123,7 @@ const fields = { ALERT_FLAPPING, ALERT_FLAPPING_HISTORY, ALERT_MAINTENANCE_WINDOW_IDS, + ALERT_CONSECUTIVE_MATCHES, ALERT_INSTANCE_ID, ALERT_LAST_DETECTED, ALERT_REASON, @@ -160,6 +164,7 @@ export { ALERT_FLAPPING, ALERT_FLAPPING_HISTORY, ALERT_MAINTENANCE_WINDOW_IDS, + ALERT_CONSECUTIVE_MATCHES, ALERT_INSTANCE_ID, ALERT_LAST_DETECTED, ALERT_REASON, diff --git a/x-pack/plugins/alerting/common/alert_schema/field_maps/mapping_from_field_map.test.ts b/x-pack/plugins/alerting/common/alert_schema/field_maps/mapping_from_field_map.test.ts index ce48922159537..8718fcb2db59f 100644 --- a/x-pack/plugins/alerting/common/alert_schema/field_maps/mapping_from_field_map.test.ts +++ b/x-pack/plugins/alerting/common/alert_schema/field_maps/mapping_from_field_map.test.ts @@ -211,6 +211,9 @@ describe('mappingFromFieldMap', () => { case_ids: { type: 'keyword', }, + consecutive_matches: { + type: 'long', + }, duration: { properties: { us: { diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts index 81848c3e65686..dcebb07009af5 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts @@ -15,6 +15,7 @@ import { } from '../types'; import { ALERT_ACTION_GROUP, + ALERT_CONSECUTIVE_MATCHES, ALERT_DURATION, ALERT_END, ALERT_FLAPPING, @@ -154,6 +155,7 @@ const fetchedAlert1 = { [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_DURATION]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true], @@ -184,6 +186,7 @@ const fetchedAlert2 = { [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_DURATION]: 36000000000, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true, false], @@ -214,6 +217,7 @@ const getNewIndexedAlertDoc = (overrides = {}) => ({ [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 1, [ALERT_DURATION]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true], @@ -260,6 +264,7 @@ const getRecoveredIndexedAlertDoc = (overrides = {}) => ({ [ALERT_END]: date, [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: date }, [ALERT_STATUS]: 'recovered', + [ALERT_CONSECUTIVE_MATCHES]: 0, ...overrides, }); @@ -670,6 +675,7 @@ describe('Alerts Client', () => { [TIMESTAMP]: date, [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 1, [ALERT_DURATION]: 36000000000, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true, false], @@ -777,7 +783,7 @@ describe('Alerts Client', () => { }, }, // ongoing alert doc - getOngoingIndexedAlertDoc({ [ALERT_UUID]: 'abc' }), + getOngoingIndexedAlertDoc({ [ALERT_UUID]: 'abc', [ALERT_CONSECUTIVE_MATCHES]: 0 }), ], }); }); @@ -950,6 +956,7 @@ describe('Alerts Client', () => { [TIMESTAMP]: date, [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 1, [ALERT_DURATION]: 72000000000, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true, false, false, false], @@ -997,6 +1004,7 @@ describe('Alerts Client', () => { [TIMESTAMP]: date, [EVENT_ACTION]: 'close', [ALERT_ACTION_GROUP]: 'recovered', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_DURATION]: 36000000000, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true, true], diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/build_new_alert.test.ts b/x-pack/plugins/alerting/server/alerts_client/lib/build_new_alert.test.ts index 2da67f584e93d..47c4e9e5f4f59 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/build_new_alert.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/build_new_alert.test.ts @@ -25,6 +25,7 @@ import { TIMESTAMP, VERSION, ALERT_TIME_RANGE, + ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; import { alertRule } from './test_fixtures'; @@ -46,6 +47,7 @@ describe('buildNewAlert', () => { [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_INSTANCE_ID]: 'alert-A', @@ -77,6 +79,7 @@ describe('buildNewAlert', () => { [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_INSTANCE_ID]: 'alert-A', @@ -112,6 +115,7 @@ describe('buildNewAlert', () => { [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true, false, false, false, true, true], [ALERT_INSTANCE_ID]: 'alert-A', @@ -152,6 +156,7 @@ describe('buildNewAlert', () => { [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_INSTANCE_ID]: 'alert-A', @@ -197,6 +202,7 @@ describe('buildNewAlert', () => { [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_INSTANCE_ID]: 'alert-A', @@ -247,6 +253,7 @@ describe('buildNewAlert', () => { [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_INSTANCE_ID]: 'alert-A', @@ -299,6 +306,7 @@ describe('buildNewAlert', () => { [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_INSTANCE_ID]: 'alert-A', diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/build_new_alert.ts b/x-pack/plugins/alerting/server/alerts_client/lib/build_new_alert.ts index 4af3e3f93817b..911c0cc8c6c9b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/build_new_alert.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/build_new_alert.ts @@ -14,6 +14,7 @@ import { ALERT_FLAPPING_HISTORY, ALERT_INSTANCE_ID, ALERT_MAINTENANCE_WINDOW_IDS, + ALERT_CONSECUTIVE_MATCHES, ALERT_RULE_TAGS, ALERT_START, ALERT_STATUS, @@ -86,6 +87,7 @@ export const buildNewAlert = < [ALERT_FLAPPING_HISTORY]: legacyAlert.getFlappingHistory(), [ALERT_INSTANCE_ID]: legacyAlert.getId(), [ALERT_MAINTENANCE_WINDOW_IDS]: legacyAlert.getMaintenanceWindowIds(), + [ALERT_CONSECUTIVE_MATCHES]: legacyAlert.getActiveCount(), [ALERT_STATUS]: 'active', [ALERT_UUID]: legacyAlert.getUuid(), [ALERT_WORKFLOW_STATUS]: get(cleanedPayload, ALERT_WORKFLOW_STATUS, 'open'), diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/build_ongoing_alert.test.ts b/x-pack/plugins/alerting/server/alerts_client/lib/build_ongoing_alert.test.ts index 2c4fc087a4745..7e76a829d0d35 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/build_ongoing_alert.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/build_ongoing_alert.test.ts @@ -26,6 +26,7 @@ import { TIMESTAMP, VERSION, ALERT_TIME_RANGE, + ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; import { alertRule, existingFlattenedNewAlert, existingExpandedNewAlert } from './test_fixtures'; @@ -55,6 +56,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'warning', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: [], @@ -114,6 +116,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'warning', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: [], @@ -190,6 +193,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'error', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [false, false, true, true], [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-xyz'], @@ -279,6 +283,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'warning', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: [], @@ -375,6 +380,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'warning', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: [], @@ -475,6 +481,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'warning', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: [], @@ -555,6 +562,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'warning', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: [], @@ -653,6 +661,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'active', [ALERT_ACTION_GROUP]: 'warning', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: [], diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/build_ongoing_alert.ts b/x-pack/plugins/alerting/server/alerts_client/lib/build_ongoing_alert.ts index ec4eae47d6e82..8d1be2e75ecb0 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/build_ongoing_alert.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/build_ongoing_alert.ts @@ -9,6 +9,7 @@ import deepmerge from 'deepmerge'; import type { Alert } from '@kbn/alerts-as-data-utils'; import { ALERT_ACTION_GROUP, + ALERT_CONSECUTIVE_MATCHES, ALERT_DURATION, ALERT_FLAPPING, ALERT_FLAPPING_HISTORY, @@ -93,6 +94,8 @@ export const buildOngoingAlert = < [ALERT_FLAPPING_HISTORY]: legacyAlert.getFlappingHistory(), // Set latest maintenance window IDs [ALERT_MAINTENANCE_WINDOW_IDS]: legacyAlert.getMaintenanceWindowIds(), + // Set latest match count + [ALERT_CONSECUTIVE_MATCHES]: legacyAlert.getActiveCount(), // Set the time range ...(legacyAlert.getState().start ? { diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/build_recovered_alert.test.ts b/x-pack/plugins/alerting/server/alerts_client/lib/build_recovered_alert.test.ts index c8a1454870031..3b4f23ac7cb4c 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/build_recovered_alert.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/build_recovered_alert.test.ts @@ -27,6 +27,7 @@ import { VERSION, ALERT_TIME_RANGE, ALERT_END, + ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; import { alertRule, @@ -63,6 +64,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'close', [ALERT_ACTION_GROUP]: 'recovered', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: [], @@ -127,6 +129,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'close', [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], @@ -221,6 +224,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'close', [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], @@ -323,6 +327,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'close', [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], @@ -423,6 +428,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'close', [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], @@ -522,6 +528,7 @@ for (const flattened of [true, false]) { [TIMESTAMP]: '2023-03-29T12:27:28.159Z', [EVENT_ACTION]: 'close', [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [], [ALERT_MAINTENANCE_WINDOW_IDS]: [], diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/build_recovered_alert.ts b/x-pack/plugins/alerting/server/alerts_client/lib/build_recovered_alert.ts index 74bebca1bb955..46f36c9715e11 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/build_recovered_alert.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/build_recovered_alert.ts @@ -22,6 +22,7 @@ import { ALERT_END, ALERT_TIME_RANGE, ALERT_START, + ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; import { DeepPartial } from '@kbn/utility-types'; import { Alert as LegacyAlert } from '../../alert/alert'; @@ -92,6 +93,8 @@ export const buildRecoveredAlert = < [ALERT_FLAPPING_HISTORY]: legacyAlert.getFlappingHistory(), // Set latest maintenance window IDs [ALERT_MAINTENANCE_WINDOW_IDS]: legacyAlert.getMaintenanceWindowIds(), + // Set latest match count, should be 0 + [ALERT_CONSECUTIVE_MATCHES]: legacyAlert.getActiveCount(), // Set status to 'recovered' [ALERT_STATUS]: 'recovered', // Set latest duration as recovered alerts should have updated duration diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/test_fixtures.ts b/x-pack/plugins/alerting/server/alerts_client/lib/test_fixtures.ts index 1a8e2be1e16a4..096d8ab6a39a7 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/test_fixtures.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/test_fixtures.ts @@ -22,6 +22,7 @@ import { ALERT_FLAPPING_HISTORY, ALERT_INSTANCE_ID, ALERT_MAINTENANCE_WINDOW_IDS, + ALERT_CONSECUTIVE_MATCHES, ALERT_STATUS, ALERT_UUID, ALERT_WORKFLOW_STATUS, @@ -80,6 +81,7 @@ export const existingFlattenedNewAlert = { [ALERT_FLAPPING_HISTORY]: [true], [ALERT_INSTANCE_ID]: 'alert-A', [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_CONSECUTIVE_MATCHES]: 1, [ALERT_STATUS]: 'active', [ALERT_START]: '2023-03-28T12:27:28.159Z', [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, @@ -98,6 +100,7 @@ export const existingFlattenedActiveAlert = { [ALERT_DURATION]: '3600', [ALERT_FLAPPING_HISTORY]: [true, false], [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-x'], + [ALERT_CONSECUTIVE_MATCHES]: 2, }; export const existingFlattenedRecoveredAlert = { @@ -110,6 +113,7 @@ export const existingFlattenedRecoveredAlert = { [ALERT_TIME_RANGE]: { gte: '2023-03-27T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, [ALERT_FLAPPING_HISTORY]: [true, false, false, true], [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-x'], + [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_STATUS]: 'recovered', }; diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap index 84a93be0a115d..5c76328e59a8f 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap @@ -747,6 +747,11 @@ Object { "required": false, "type": "keyword", }, + "kibana.alert.consecutive_matches": Object { + "array": false, + "required": false, + "type": "long", + }, "kibana.alert.depth": Object { "array": false, "required": true, @@ -1794,6 +1799,11 @@ Object { "required": false, "type": "keyword", }, + "kibana.alert.consecutive_matches": Object { + "array": false, + "required": false, + "type": "long", + }, "kibana.alert.depth": Object { "array": false, "required": true, @@ -2841,6 +2851,11 @@ Object { "required": false, "type": "keyword", }, + "kibana.alert.consecutive_matches": Object { + "array": false, + "required": false, + "type": "long", + }, "kibana.alert.depth": Object { "array": false, "required": true, @@ -3888,6 +3903,11 @@ Object { "required": false, "type": "keyword", }, + "kibana.alert.consecutive_matches": Object { + "array": false, + "required": false, + "type": "long", + }, "kibana.alert.depth": Object { "array": false, "required": true, @@ -4935,6 +4955,11 @@ Object { "required": false, "type": "keyword", }, + "kibana.alert.consecutive_matches": Object { + "array": false, + "required": false, + "type": "long", + }, "kibana.alert.depth": Object { "array": false, "required": true, @@ -5988,6 +6013,11 @@ Object { "required": false, "type": "keyword", }, + "kibana.alert.consecutive_matches": Object { + "array": false, + "required": false, + "type": "long", + }, "kibana.alert.depth": Object { "array": false, "required": true, @@ -7035,6 +7065,11 @@ Object { "required": false, "type": "keyword", }, + "kibana.alert.consecutive_matches": Object { + "array": false, + "required": false, + "type": "long", + }, "kibana.alert.depth": Object { "array": false, "required": true, @@ -8082,6 +8117,11 @@ Object { "required": false, "type": "keyword", }, + "kibana.alert.consecutive_matches": Object { + "array": false, + "required": false, + "type": "long", + }, "kibana.alert.depth": Object { "array": false, "required": true, diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts index ec690bb8ba0f5..a1af1dccb9928 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts @@ -319,6 +319,7 @@ export class ExecutionHandler< actionParams: action.params, flapping: executableAlert.getFlapping(), ruleUrl: ruleUrl?.absoluteUrl, + consecutiveMatches: executableAlert.getActiveCount(), }; if (executableAlert.isAlertAsData()) { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts index 4274c320126de..246215ba9a154 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts @@ -96,6 +96,7 @@ import { SPACE_IDS, TAGS, VERSION, + ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; jest.mock('uuid', () => ({ @@ -554,6 +555,7 @@ describe('Task Runner', () => { [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', + [ALERT_CONSECUTIVE_MATCHES]: 1, [ALERT_DURATION]: 0, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true], diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts index ae60d5dd50f5c..c343411d494b7 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts @@ -789,6 +789,37 @@ describe('transformActionParams', () => { } `); }); + + test('consecutive matches is passed to templates', () => { + const actionParams = { + message: 'Value "{{alert.consecutiveMatches}}" exists', + }; + const result = transformActionParams({ + actionsPlugin, + actionTypeId, + actionParams, + state: {}, + context: {}, + alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', + alertName: 'alert-name', + tags: ['tag-A', 'tag-B'], + spaceId: 'spaceId-A', + alertInstanceId: '2', + alertUuid: 'uuid-1', + alertActionGroup: 'action-group', + alertActionGroupName: 'Action Group', + alertParams: {}, + flapping: true, + consecutiveMatches: 4, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "message": "Value \\"4\\" exists", + } + `); + }); }); describe('transformSummaryActionParams', () => { diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts index 2e215889291f8..65fa3dae5f89d 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts @@ -37,6 +37,7 @@ export interface TransformActionParamsOptions { ruleUrl?: string; flapping: boolean; aadAlert?: AADAlert; + consecutiveMatches?: number; } interface SummarizedAlertsWithAll { @@ -79,6 +80,7 @@ export function transformActionParams({ ruleUrl, flapping, aadAlert, + consecutiveMatches, }: TransformActionParamsOptions): RuleActionParams { // when the list of variables we pass in here changes, // the UI will need to be updated as well; see: @@ -111,6 +113,7 @@ export function transformActionParams({ actionGroup: alertActionGroup, actionGroupName: alertActionGroupName, flapping, + consecutiveMatches, }, ...(aadAlert ? { ...aadAlert } : {}), }; @@ -161,6 +164,7 @@ export function transformSummaryActionParams({ actionGroup: 'default', actionGroupName: 'Default', flapping: false, + consecutiveMatches: 0, }, kibanaBaseUrl, date: new Date().toISOString(), diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.test.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.test.ts index a2072a8d87c82..aa82fbdeb0b16 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.test.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.test.ts @@ -42,6 +42,11 @@ it('matches snapshot', () => { "required": false, "type": "keyword", }, + "kibana.alert.consecutive_matches": Object { + "array": false, + "required": false, + "type": "long", + }, "kibana.alert.duration.us": Object { "array": false, "required": false, diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index 2367167b49697..46cbe2eb1eb29 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -26,6 +26,7 @@ import { SPACE_IDS, ALERT_FLAPPING, TAGS, + ALERT_CONSECUTIVE_MATCHES, } from '../../common/technical_rule_data_field_names'; import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; import { createLifecycleExecutor } from './create_lifecycle_executor'; @@ -1931,6 +1932,473 @@ describe('createLifecycleExecutor', () => { ); }); }); + + describe('set consecutive matches on the document', () => { + it('updates documents with consecutive matches for active alerts', async () => { + const logger = loggerMock.create(); + const ruleDataClientMock = createRuleDataClientMock(); + ruleDataClientMock.getReader().search.mockResolvedValue({ + hits: { + hits: [ + { + _source: { + '@timestamp': '', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', + [ALERT_UUID]: 'ALERT_0_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: 'closed', + [SPACE_IDS]: ['fake-space-id'], + }, + _index: '.alerts-index-name', + _seq_no: 4, + _primary_term: 2, + }, + { + _source: { + '@timestamp': '', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', + [ALERT_UUID]: 'ALERT_1_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['fake-space-id'], + }, + _index: '.alerts-index-name', + _seq_no: 4, + _primary_term: 2, + }, + { + _source: { + '@timestamp': '', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', + [ALERT_UUID]: 'ALERT_2_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['fake-space-id'], + }, + _index: '.alerts-index-name', + _seq_no: 4, + _primary_term: 2, + }, + { + _source: { + '@timestamp': '', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', + [ALERT_UUID]: 'ALERT_3_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['fake-space-id'], + }, + _index: '.alerts-index-name', + _seq_no: 4, + _primary_term: 2, + }, + ], + }, + } as any); + const executor = createLifecycleExecutor( + logger, + ruleDataClientMock + )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { + services.alertWithLifecycle({ + id: 'TEST_ALERT_0', + fields: {}, + }); + services.alertWithLifecycle({ + id: 'TEST_ALERT_1', + fields: {}, + }); + services.alertWithLifecycle({ + id: 'TEST_ALERT_2', + fields: {}, + }); + services.alertWithLifecycle({ + id: 'TEST_ALERT_3', + fields: {}, + }); + + return { state }; + }); + + const serializedAlerts = await executor( + createDefaultAlertExecutorOptions({ + alertId: 'TEST_ALERT_0', + params: {}, + state: { + wrapped: initialRuleState, + trackedAlerts: { + TEST_ALERT_0: { + alertId: 'TEST_ALERT_0', + alertUuid: 'TEST_ALERT_0_UUID', + started: '2020-01-01T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + activeCount: 0, + }, + TEST_ALERT_1: { + alertId: 'TEST_ALERT_1', + alertUuid: 'TEST_ALERT_1_UUID', + started: '2020-01-02T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + activeCount: 0, + }, + TEST_ALERT_2: { + alertId: 'TEST_ALERT_2', + alertUuid: 'TEST_ALERT_2_UUID', + started: '2020-01-01T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + activeCount: 0, + }, + TEST_ALERT_3: { + alertId: 'TEST_ALERT_3', + alertUuid: 'TEST_ALERT_3_UUID', + started: '2020-01-02T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + activeCount: 0, + }, + }, + trackedAlertsRecovered: {}, + }, + logger, + }) + ); + + expect(serializedAlerts.state.trackedAlerts).toEqual({ + TEST_ALERT_0: { + activeCount: 1, + alertId: 'TEST_ALERT_0', + alertUuid: 'TEST_ALERT_0_UUID', + flapping: false, + flappingHistory: [false], + pendingRecoveredCount: 0, + started: '2020-01-01T12:00:00.000Z', + }, + TEST_ALERT_1: { + activeCount: 1, + alertId: 'TEST_ALERT_1', + alertUuid: 'TEST_ALERT_1_UUID', + flapping: false, + flappingHistory: [false], + pendingRecoveredCount: 0, + started: '2020-01-02T12:00:00.000Z', + }, + TEST_ALERT_2: { + activeCount: 1, + alertId: 'TEST_ALERT_2', + alertUuid: 'TEST_ALERT_2_UUID', + flapping: false, + flappingHistory: [false], + pendingRecoveredCount: 0, + started: '2020-01-01T12:00:00.000Z', + }, + TEST_ALERT_3: { + activeCount: 1, + alertId: 'TEST_ALERT_3', + alertUuid: 'TEST_ALERT_3_UUID', + flapping: false, + flappingHistory: [false], + pendingRecoveredCount: 0, + started: '2020-01-02T12:00:00.000Z', + }, + }); + + expect(serializedAlerts.state.trackedAlertsRecovered).toEqual({}); + + expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( + expect.objectContaining({ + body: [ + // alert document + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, + expect.objectContaining({ + [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', + [ALERT_WORKFLOW_STATUS]: 'closed', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_CONSECUTIVE_MATCHES]: 1, + [EVENT_ACTION]: 'active', + [EVENT_KIND]: 'signal', + }), + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, + expect.objectContaining({ + [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [EVENT_ACTION]: 'active', + [EVENT_KIND]: 'signal', + [ALERT_CONSECUTIVE_MATCHES]: 1, + }), + { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, + expect.objectContaining({ + [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [EVENT_ACTION]: 'active', + [EVENT_KIND]: 'signal', + [ALERT_CONSECUTIVE_MATCHES]: 1, + }), + { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, + expect.objectContaining({ + [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [EVENT_ACTION]: 'active', + [EVENT_KIND]: 'signal', + [ALERT_CONSECUTIVE_MATCHES]: 1, + }), + ], + }) + ); + }); + + it('updates existing documents for recovered alerts', async () => { + const logger = loggerMock.create(); + const ruleDataClientMock = createRuleDataClientMock(); + ruleDataClientMock.getReader().search.mockResolvedValue({ + hits: { + hits: [ + { + _source: { + '@timestamp': '', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', + [ALERT_UUID]: 'ALERT_0_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [SPACE_IDS]: ['fake-space-id'], + }, + _index: '.alerts-index-name', + _seq_no: 4, + _primary_term: 2, + }, + { + _source: { + '@timestamp': '', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', + [ALERT_UUID]: 'ALERT_1_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [SPACE_IDS]: ['fake-space-id'], + }, + _index: '.alerts-index-name', + _seq_no: 4, + _primary_term: 2, + }, + { + _source: { + '@timestamp': '', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', + [ALERT_UUID]: 'ALERT_2_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [SPACE_IDS]: ['fake-space-id'], + }, + _index: '.alerts-index-name', + _seq_no: 4, + _primary_term: 2, + }, + { + _source: { + '@timestamp': '', + [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', + [ALERT_UUID]: 'ALERT_3_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [SPACE_IDS]: ['fake-space-id'], + }, + _index: '.alerts-index-name', + _seq_no: 4, + _primary_term: 2, + }, + ], + }, + } as any); + const executor = createLifecycleExecutor( + logger, + ruleDataClientMock + )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { + return { state }; + }); + + const serializedAlerts = await executor( + createDefaultAlertExecutorOptions({ + alertId: 'TEST_ALERT_0', + params: {}, + state: { + wrapped: initialRuleState, + trackedAlerts: { + TEST_ALERT_0: { + alertId: 'TEST_ALERT_0', + alertUuid: 'TEST_ALERT_0_UUID', + started: '2020-01-01T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + activeCount: 0, + }, + TEST_ALERT_1: { + alertId: 'TEST_ALERT_1', + alertUuid: 'TEST_ALERT_1_UUID', + started: '2020-01-02T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + activeCount: 0, + }, + TEST_ALERT_2: { + alertId: 'TEST_ALERT_2', + alertUuid: 'TEST_ALERT_2_UUID', + started: '2020-01-02T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + activeCount: 0, + }, + TEST_ALERT_3: { + alertId: 'TEST_ALERT_3', + alertUuid: 'TEST_ALERT_3_UUID', + started: '2020-01-02T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + activeCount: 0, + }, + }, + trackedAlertsRecovered: {}, + }, + logger, + }) + ); + + expect(serializedAlerts.state.trackedAlerts).toEqual({}); + + expect(serializedAlerts.state.trackedAlertsRecovered).toEqual({ + TEST_ALERT_0: { + activeCount: 0, + alertId: 'TEST_ALERT_0', + alertUuid: 'TEST_ALERT_0_UUID', + flapping: false, + flappingHistory: [true], + pendingRecoveredCount: 0, + started: '2020-01-01T12:00:00.000Z', + }, + TEST_ALERT_1: { + activeCount: 0, + alertId: 'TEST_ALERT_1', + alertUuid: 'TEST_ALERT_1_UUID', + flapping: false, + flappingHistory: [true], + pendingRecoveredCount: 0, + started: '2020-01-02T12:00:00.000Z', + }, + TEST_ALERT_2: { + activeCount: 0, + alertId: 'TEST_ALERT_2', + alertUuid: 'TEST_ALERT_2_UUID', + flapping: false, + flappingHistory: [true], + pendingRecoveredCount: 0, + started: '2020-01-02T12:00:00.000Z', + }, + TEST_ALERT_3: { + activeCount: 0, + alertId: 'TEST_ALERT_3', + alertUuid: 'TEST_ALERT_3_UUID', + flapping: false, + flappingHistory: [true], + pendingRecoveredCount: 0, + started: '2020-01-02T12:00:00.000Z', + }, + }); + + expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.arrayContaining([ + // alert document + { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, + expect.objectContaining({ + [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', + [ALERT_STATUS]: ALERT_STATUS_RECOVERED, + [EVENT_ACTION]: 'close', + [EVENT_KIND]: 'signal', + [ALERT_CONSECUTIVE_MATCHES]: 0, + }), + { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, + expect.objectContaining({ + [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', + [ALERT_STATUS]: ALERT_STATUS_RECOVERED, + [EVENT_ACTION]: 'close', + [EVENT_KIND]: 'signal', + [ALERT_CONSECUTIVE_MATCHES]: 0, + }), + { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, + expect.objectContaining({ + [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', + [ALERT_STATUS]: ALERT_STATUS_RECOVERED, + [EVENT_ACTION]: 'close', + [EVENT_KIND]: 'signal', + [ALERT_CONSECUTIVE_MATCHES]: 0, + }), + { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, + expect.objectContaining({ + [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', + [ALERT_STATUS]: ALERT_STATUS_RECOVERED, + [EVENT_ACTION]: 'close', + [EVENT_KIND]: 'signal', + [ALERT_CONSECUTIVE_MATCHES]: 0, + }), + ]), + }) + ); + }); + }); }); type TestRuleState = Record & { diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 58aa875cf2344..465c49f9aa9b7 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -253,6 +253,7 @@ describe('createLifecycleRuleTypeFactory', () => { "@timestamp": "2021-06-16T09:01:00.000Z", "event.action": "open", "event.kind": "signal", + "kibana.alert.consecutive_matches": 1, "kibana.alert.duration.us": 0, "kibana.alert.flapping": false, "kibana.alert.instance.id": "opbeans-java", @@ -290,6 +291,7 @@ describe('createLifecycleRuleTypeFactory', () => { "@timestamp": "2021-06-16T09:01:00.000Z", "event.action": "open", "event.kind": "signal", + "kibana.alert.consecutive_matches": 1, "kibana.alert.duration.us": 0, "kibana.alert.flapping": false, "kibana.alert.instance.id": "opbeans-node", diff --git a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts index abb9ebba6d016..a6f428d598262 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts @@ -70,6 +70,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 2, "event": Object { + "kibana.alert.consecutive_matches": 2, "kibana.alert.status": "active", }, "flappingHistory": Array [], @@ -78,6 +79,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 1, "event": Object { + "kibana.alert.consecutive_matches": 1, "kibana.alert.status": "active", }, "flappingHistory": Array [ @@ -99,6 +101,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 0, "event": Object { + "kibana.alert.consecutive_matches": 0, "kibana.alert.status": "recovered", }, "flapping": true, @@ -107,6 +110,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 0, "event": Object { + "kibana.alert.consecutive_matches": 0, "kibana.alert.status": "recovered", }, "flapping": false, @@ -115,6 +119,7 @@ describe('getAlertsForNotification', () => { "activeCount": 0, "event": Object { "event.action": "active", + "kibana.alert.consecutive_matches": 0, "kibana.alert.status": "active", }, "flapping": true, @@ -133,6 +138,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 0, "event": Object { + "kibana.alert.consecutive_matches": 0, "kibana.alert.status": "recovered", }, "flapping": true, @@ -141,6 +147,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 0, "event": Object { + "kibana.alert.consecutive_matches": 0, "kibana.alert.status": "recovered", }, "flapping": false, @@ -149,6 +156,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 0, "event": Object { + "kibana.alert.consecutive_matches": 0, "kibana.alert.status": "recovered", }, "flapping": true, @@ -174,6 +182,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 2, "event": Object { + "kibana.alert.consecutive_matches": 2, "kibana.alert.status": "active", }, "flappingHistory": Array [], @@ -182,6 +191,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 1, "event": Object { + "kibana.alert.consecutive_matches": 1, "kibana.alert.status": "active", }, "flappingHistory": Array [ @@ -203,6 +213,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 0, "event": Object { + "kibana.alert.consecutive_matches": 0, "kibana.alert.status": "recovered", }, "flapping": true, @@ -211,6 +222,7 @@ describe('getAlertsForNotification', () => { Object { "activeCount": 0, "event": Object { + "kibana.alert.consecutive_matches": 0, "kibana.alert.status": "recovered", }, "flapping": false, @@ -243,6 +255,7 @@ describe('getAlertsForNotification', () => { "activeCount": 2, "event": Object { "event.action": "open", + "kibana.alert.consecutive_matches": 2, "kibana.alert.duration.us": 0, "kibana.alert.maintenance_window_ids": Array [ "maintenance-window-id", diff --git a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts index 5ec0e5b835eec..15dcedeaf88ca 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts @@ -16,6 +16,7 @@ import { EVENT_ACTION, ALERT_TIME_RANGE, ALERT_MAINTENANCE_WINDOW_IDS, + ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; export function getAlertsForNotification( @@ -72,6 +73,7 @@ export function getAlertsForNotification( trackedEvent.pendingRecoveredCount = 0; } } + trackedEvent.event[ALERT_CONSECUTIVE_MATCHES] = trackedEvent.activeCount; events.push(trackedEvent); } return events; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index 075f5a61a1d02..04ecb66e1692e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -69,6 +69,10 @@ const expectedTransformResult = [ 'A flag on the alert that indicates whether the alert status is changing repeatedly.', name: 'alert.flapping', }, + { + description: 'The number of consecutive runs that meet the rule conditions.', + name: 'alert.consecutiveMatches', + }, { description: 'The configured server.publicBaseUrl value or empty string if not configured.', name: 'kibanaBaseUrl', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index 50323690d5eed..4385db7521e22 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -76,6 +76,7 @@ export enum AlertProvidedActionVariables { alertActionSubgroup = 'alert.actionSubgroup', alertFlapping = 'alert.flapping', kibanaBaseUrl = 'kibanaBaseUrl', + alertConsecutiveMatches = 'alert.consecutiveMatches', } export enum LegacyAlertProvidedActionVariables { @@ -224,6 +225,16 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] { }), }); + result.push({ + name: AlertProvidedActionVariables.alertConsecutiveMatches, + description: i18n.translate( + 'xpack.triggersActionsUI.actionVariables.alertConsecutiveMatchesLabel', + { + defaultMessage: 'The number of consecutive runs that meet the rule conditions.', + } + ), + }); + result.push(AlertProvidedActionVariableDescriptions[AlertProvidedActionVariables.kibanaBaseUrl]); result.push({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts index bf6811b4ad175..ab3c9ad93d54f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts @@ -1506,6 +1506,7 @@ instanceStateValue: true uuid: expectExpect.any(String), tags: ['tag-A', 'tag-B'], }, + consecutive_matches: 1, duration: { us: 0 }, time_range: { gte: expectExpect.any(String) }, instance: { id: '1' }, @@ -1542,6 +1543,7 @@ instanceStateValue: true uuid: expectExpect.any(String), tags: ['tag-A', 'tag-B'], }, + consecutive_matches: 1, duration: { us: 0 }, time_range: { gte: expectExpect.any(String) }, instance: { id: '2' }, @@ -1594,6 +1596,7 @@ instanceStateValue: true uuid: expectExpect.any(String), tags: ['tag-A', 'tag-B'], }, + consecutive_matches: expectExpect.any(Number), duration: { us: expectExpect.any(Number) }, time_range: { gte: expectExpect.any(String) }, instance: { id: '1' }, @@ -1630,6 +1633,7 @@ instanceStateValue: true uuid: expectExpect.any(String), tags: ['tag-A', 'tag-B'], }, + consecutive_matches: expectExpect.any(Number), duration: { us: expectExpect.any(Number) }, time_range: { gte: expectExpect.any(String) }, instance: { id: '2' }, @@ -1730,6 +1734,7 @@ instanceStateValue: true uuid: expectExpect.any(String), tags: ['tag-A', 'tag-B'], }, + consecutive_matches: 1, duration: { us: 0 }, time_range: { gte: expectExpect.any(String) }, instance: { id: '1' }, @@ -1766,6 +1771,7 @@ instanceStateValue: true uuid: expectExpect.any(String), tags: ['tag-A', 'tag-B'], }, + consecutive_matches: 1, duration: { us: 0 }, time_range: { gte: expectExpect.any(String) }, instance: { id: '2' }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_alert_delay.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_alert_delay.ts index c900a08311add..991ed513ee984 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_alert_delay.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_alert_delay.ts @@ -31,6 +31,7 @@ import { EVENT_ACTION, EVENT_KIND, SPACE_IDS, + ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; import { ES_TEST_INDEX_NAME, ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; @@ -248,6 +249,8 @@ export default function createAlertsAsDataAlertDelayInstallResourcesTest({ expect(source[EVENT_KIND]).to.equal('signal'); // tags should equal rule tags because rule type doesn't set any tags expect(source.tags).to.eql(['foo']); + // alert consecutive matches should match the active count + expect(source[ALERT_CONSECUTIVE_MATCHES]).to.equal(3); // -------------------------- // RUN 4 - 1 active alert @@ -300,6 +303,8 @@ export default function createAlertsAsDataAlertDelayInstallResourcesTest({ expect(source[EVENT_KIND]).to.eql(run3Source[EVENT_KIND]); expect(source[ALERT_WORKFLOW_STATUS]).to.eql(run3Source[ALERT_WORKFLOW_STATUS]); expect(source[ALERT_TIME_RANGE]?.gte).to.equal(run3Source[ALERT_TIME_RANGE]?.gte); + // alert consecutive matches should match the active count + expect(source[ALERT_CONSECUTIVE_MATCHES]).to.equal(4); // -------------------------- // RUN 5 - 1 recovered alert @@ -357,6 +362,8 @@ export default function createAlertsAsDataAlertDelayInstallResourcesTest({ expect(source[ALERT_TIME_RANGE]?.gte).to.equal(run3Source[ALERT_TIME_RANGE]?.gte); // time_range.lte should be set to end time expect(source[ALERT_TIME_RANGE]?.lte).to.equal(source[ALERT_END]); + // alert consecutive matches should match the active count + expect(source[ALERT_CONSECUTIVE_MATCHES]).to.equal(0); // -------------------------- // RUN 6 - 0 new alerts @@ -548,6 +555,8 @@ export default function createAlertsAsDataAlertDelayInstallResourcesTest({ expect(source[EVENT_KIND]).to.equal('signal'); // tags should equal rule tags because rule type doesn't set any tags expect(source.tags).to.eql(['foo']); + // alert consecutive matches should match the active count + expect(source[ALERT_CONSECUTIVE_MATCHES]).to.equal(3); // -------------------------- // RUN 4 - 1 active alert @@ -608,6 +617,8 @@ export default function createAlertsAsDataAlertDelayInstallResourcesTest({ expect(source[EVENT_KIND]).to.eql(run3Source[EVENT_KIND]); expect(source[ALERT_WORKFLOW_STATUS]).to.eql(run3Source[ALERT_WORKFLOW_STATUS]); expect(source[ALERT_TIME_RANGE]?.gte).to.equal(run3Source[ALERT_TIME_RANGE]?.gte); + // alert consecutive matches should match the active count + expect(source[ALERT_CONSECUTIVE_MATCHES]).to.equal(4); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_conflicts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_conflicts.ts index ea53cbe33c98f..d13d321280aae 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_conflicts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_conflicts.ts @@ -20,6 +20,7 @@ import { ALERT_STATUS, ALERT_WORKFLOW_STATUS, ALERT_WORKFLOW_TAGS, + ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { Spaces } from '../../../../scenarios'; @@ -238,6 +239,9 @@ function compareAlertDocs( expect(get(updatedAlert, 'kibana.alert.workflow_status')).to.eql( get(DocUpdate, 'kibana.alert.workflow_status') ); + expect(get(updatedAlert, 'kibana.alert.consecutive_matches')).to.eql( + get(DocUpdate, 'kibana.alert.consecutive_matches') + 1 + ); expect(get(initialAlert, 'kibana.alert.status')).to.be('active'); expect(get(updatedAlert, 'kibana.alert.status')).to.be('untracked'); @@ -264,6 +268,7 @@ const DocUpdate = { [ALERT_WORKFLOW_TAGS]: ['fee', 'fi', 'fo', 'fum'], [ALERT_CASE_IDS]: ['123', '456', '789'], [ALERT_STATUS]: 'untracked', + [ALERT_CONSECUTIVE_MATCHES]: 1, }; const SkipFields = [ @@ -280,6 +285,7 @@ const SkipFields = [ 'kibana.alert.case_ids', 'kibana.alert.workflow_tags', 'kibana.alert.workflow_status', + 'kibana.alert.consecutive_matches', ]; function log(message: string) { diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/__snapshots__/create_rule.snap b/x-pack/test/rule_registry/spaces_only/tests/trial/__snapshots__/create_rule.snap index bc4b903dee43e..54ad6fe9f9847 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/__snapshots__/create_rule.snap +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/__snapshots__/create_rule.snap @@ -8,6 +8,9 @@ Object { "event.kind": Array [ "signal", ], + "kibana.alert.consecutive_matches": Array [ + 1, + ], "kibana.alert.duration.us": Array [ 0, ], @@ -97,6 +100,9 @@ Object { "event.kind": Array [ "signal", ], + "kibana.alert.consecutive_matches": Array [ + 0, + ], "kibana.alert.evaluation.threshold": Array [ 30, ], diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts index e8ee1743dc94e..2297e65d4824a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts @@ -36,6 +36,7 @@ import { SPACE_IDS, TAGS, VERSION, + ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { createEsQueryRule } from './helpers/alerting_api_helper'; @@ -113,6 +114,7 @@ export default function ({ getService }: FtrProviderContext) { expect(typeof hits1[ALERT_UUID]).to.be('string'); expect(typeof hits1[ALERT_URL]).to.be('string'); expect(typeof hits1[VERSION]).to.be('string'); + expect(typeof hits1[ALERT_CONSECUTIVE_MATCHES]).to.be('number'); // remove fields we aren't going to compare directly const fields = [ @@ -129,6 +131,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.uuid', 'kibana.alert.url', 'kibana.version', + 'kibana.alert.consecutive_matches', ]; for (const field of fields) { @@ -240,6 +243,7 @@ export default function ({ getService }: FtrProviderContext) { expect(hits2[EVENT_ACTION]).to.be('active'); expect(hits1[ALERT_DURATION]).to.not.be.lessThan(0); expect(hits2[ALERT_DURATION]).not.to.be(0); + expect(hits2[ALERT_CONSECUTIVE_MATCHES]).to.be.greaterThan(hits1[ALERT_CONSECUTIVE_MATCHES]); // remove fields we know will be different const fields = [ @@ -249,6 +253,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.flapping_history', 'kibana.alert.reason', 'kibana.alert.rule.execution.uuid', + 'kibana.alert.consecutive_matches', ]; for (const field of fields) { diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/summary_actions.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/summary_actions.ts index fcda3e0c62cb8..e104a7d255204 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/summary_actions.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/summary_actions.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.uuid', 'kibana.alert.url', 'kibana.version', + 'kibana.alert.consecutive_matches', ]; afterEach(async () => {