From c82a20d0cb6caedd2b63c9d97046200b53e8dd61 Mon Sep 17 00:00:00 2001 From: Ying Date: Tue, 27 Jun 2023 12:26:55 -0400 Subject: [PATCH 1/4] Adding missing values from lifecycle rule registry rule types. Augmenting alerts client to return alert info to rule executors --- .../src/field_maps/alert_field_map.ts | 18 + .../src/field_maps/legacy_alert_field_map.ts | 18 - .../src/schemas/generated/alert_schema.ts | 5 + .../schemas/generated/legacy_alert_schema.ts | 5 - .../field_maps/mapping_from_field_map.test.ts | 15 +- .../alerting/server/alert/alert.test.ts | 18 + x-pack/plugins/alerting/server/alert/alert.ts | 5 + .../alerts_client/alerts_client.test.ts | 707 ++++++++++++------ .../server/alerts_client/alerts_client.ts | 21 +- .../alerts_client/lib/build_new_alert.test.ts | 154 ++++ .../alerts_client/lib/build_new_alert.ts | 31 +- .../lib/build_ongoing_alert.test.ts | 149 ++++ .../alerts_client/lib/build_ongoing_alert.ts | 23 +- .../lib/build_recovered_alert.test.ts | 154 ++++ .../lib/build_recovered_alert.ts | 31 +- .../lib/strip_framework_fields.test.ts | 11 +- .../lib/strip_framework_fields.ts | 10 +- .../alerting/server/alerts_client/types.ts | 26 +- .../alerts_service/alerts_service.test.ts | 5 + .../server/alerts_service/alerts_service.ts | 1 + .../server/task_runner/task_runner.test.ts | 88 ++- .../task_runner_alerts_client.test.ts | 10 + .../task_runner/task_runner_cancel.test.ts | 10 +- .../alert_instance_factory_stub.ts | 3 + .../rule_preview/api/preview_rules/route.ts | 1 + .../rule_types/es_query/rule_type.test.ts | 3 +- .../index_threshold/rule_type.test.ts | 12 +- .../plugins/alerts/server/alert_types.ts | 2 +- .../group4/alerts_as_data/alerts_as_data.ts | 70 ++ 29 files changed, 1282 insertions(+), 324 deletions(-) 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 1b78ce21021fc..f22e902bbbeaa 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 @@ -37,6 +37,9 @@ import { SPACE_IDS, TIMESTAMP, VERSION, + EVENT_ACTION, + EVENT_KIND, + TAGS, } from '@kbn/rule-data-utils'; export const alertFieldMap = { @@ -179,11 +182,26 @@ export const alertFieldMap = { array: true, required: false, }, + [EVENT_ACTION]: { + type: 'keyword', + array: false, + required: false, + }, + [EVENT_KIND]: { + type: 'keyword', + array: false, + required: false, + }, [SPACE_IDS]: { type: 'keyword', array: true, required: true, }, + [TAGS]: { + type: 'keyword', + array: true, + required: false, + }, [TIMESTAMP]: { type: 'date', required: true, diff --git a/packages/kbn-alerts-as-data-utils/src/field_maps/legacy_alert_field_map.ts b/packages/kbn-alerts-as-data-utils/src/field_maps/legacy_alert_field_map.ts index 6faa403188fdb..05749816c823b 100644 --- a/packages/kbn-alerts-as-data-utils/src/field_maps/legacy_alert_field_map.ts +++ b/packages/kbn-alerts-as-data-utils/src/field_maps/legacy_alert_field_map.ts @@ -35,9 +35,6 @@ import { ALERT_WORKFLOW_REASON, ALERT_WORKFLOW_USER, ECS_VERSION, - EVENT_ACTION, - EVENT_KIND, - TAGS, } from '@kbn/rule-data-utils'; export const legacyAlertFieldMap = { @@ -182,21 +179,6 @@ export const legacyAlertFieldMap = { array: false, required: false, }, - [EVENT_ACTION]: { - type: 'keyword', - array: false, - required: false, - }, - [EVENT_KIND]: { - type: 'keyword', - array: false, - required: false, - }, - [TAGS]: { - type: 'keyword', - array: true, - required: false, - }, } as const; export type LegacyAlertFieldMap = typeof legacyAlertFieldMap; 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 112d41c243386..4978d8b1fa1e4 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 @@ -89,6 +89,10 @@ const AlertRequired = rt.type({ }), }); const AlertOptional = rt.partial({ + event: rt.partial({ + action: schemaString, + kind: schemaString, + }), kibana: rt.partial({ alert: rt.partial({ action_group: schemaString, @@ -117,6 +121,7 @@ const AlertOptional = rt.partial({ }), version: schemaString, }), + tags: schemaStringArray, }); // prettier-ignore diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/legacy_alert_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/legacy_alert_schema.ts index 2073541391ecc..107cdd65464bc 100644 --- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/legacy_alert_schema.ts +++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/legacy_alert_schema.ts @@ -72,10 +72,6 @@ const LegacyAlertOptional = rt.partial({ ecs: rt.partial({ version: schemaString, }), - event: rt.partial({ - action: schemaString, - kind: schemaString, - }), kibana: rt.partial({ alert: rt.partial({ risk_score: schemaNumber, @@ -113,7 +109,6 @@ const LegacyAlertOptional = rt.partial({ workflow_user: schemaString, }), }), - tags: schemaStringArray, }); // prettier-ignore 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 879d1f8212cb2..aea967a056028 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 @@ -190,6 +190,16 @@ describe('mappingFromFieldMap', () => { '@timestamp': { type: 'date', }, + event: { + properties: { + action: { + type: 'keyword', + }, + kind: { + type: 'keyword', + }, + }, + }, kibana: { properties: { alert: { @@ -305,6 +315,9 @@ describe('mappingFromFieldMap', () => { }, }, }, + tags: { + type: 'keyword', + }, }, }); expect(mappingFromFieldMap(legacyAlertFieldMap)).toEqual({ @@ -355,8 +368,6 @@ describe('mappingFromFieldMap', () => { }, }, ecs: { properties: { version: { type: 'keyword' } } }, - event: { properties: { action: { type: 'keyword' }, kind: { type: 'keyword' } } }, - tags: { type: 'keyword' }, }, }); }); diff --git a/x-pack/plugins/alerting/server/alert/alert.test.ts b/x-pack/plugins/alerting/server/alert/alert.test.ts index 46269d2e2843e..95894fe440107 100644 --- a/x-pack/plugins/alerting/server/alert/alert.test.ts +++ b/x-pack/plugins/alerting/server/alert/alert.test.ts @@ -250,6 +250,24 @@ describe('getUUID()', () => { }); }); +describe('getStart()', () => { + test('returns null for new alert', () => { + const alert = new Alert('1'); + expect(alert.getStart()).toBeNull(); + }); + + test('returns start time if set in state', () => { + const uuid = 'previous-uuid'; + const meta = { uuid }; + const state = { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }; + const alert = new Alert('1', { + state, + meta, + }); + expect(alert.getStart()).toEqual('2023-03-28T12:27:28.159Z'); + }); +}); + describe('scheduleActions()', () => { test('makes hasScheduledActions() return true', () => { const alert = new Alert('1', { diff --git a/x-pack/plugins/alerting/server/alert/alert.ts b/x-pack/plugins/alerting/server/alert/alert.ts index 541a76216e367..a506d7461b8e7 100644 --- a/x-pack/plugins/alerting/server/alert/alert.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -39,6 +39,7 @@ export type PublicAlert< | 'getContext' | 'getState' | 'getUuid' + | 'getStart' | 'hasContext' | 'replaceState' | 'scheduleActions' @@ -76,6 +77,10 @@ export class Alert< return this.meta.uuid!; } + getStart(): string | null { + return this.state.start ? (this.state.start as string) : null; + } + hasScheduledActions() { return this.scheduledExecutionOptions !== undefined; } 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 1a2bfe87045a4..4cc568c3be71d 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 @@ -9,8 +9,8 @@ import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { DEFAULT_FLAPPING_SETTINGS, RecoveredActionGroup, RuleNotifyWhen } from '../types'; import * as LegacyAlertsClientModule from './legacy_alerts_client'; import { Alert } from '../alert/alert'; -import { AlertsClient } from './alerts_client'; -import { AlertRuleData } from './types'; +import { AlertsClient, AlertsClientParams } from './alerts_client'; +import { AlertRuleData, ProcessAndLogAlertsOpts } from './types'; import { legacyAlertsClientMock } from './legacy_alerts_client.mock'; import { keys, range } from 'lodash'; import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock'; @@ -36,6 +36,7 @@ const ruleType: jest.Mocked = { cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', autoRecoverAlerts: true, + doesSetRecoveryContext: true, validate: { params: { validate: (params) => params }, }, @@ -48,10 +49,18 @@ const ruleType: jest.Mocked = { const mockLegacyAlertsClient = legacyAlertsClientMock.create(); const mockReplaceState = jest.fn(); -const mockScheduleActions = jest - .fn() - .mockImplementation(() => ({ replaceState: mockReplaceState })); -const mockCreate = jest.fn().mockImplementation(() => ({ scheduleActions: mockScheduleActions })); +const mockGetUuid = jest.fn().mockReturnValue('uuidabc'); +const mockGetStart = jest.fn().mockReturnValue(date); +const mockScheduleActions = jest.fn().mockImplementation(() => ({ + replaceState: mockReplaceState, + getUuid: mockGetUuid, + getStart: mockGetStart, +})); +const mockCreate = jest.fn().mockImplementation(() => ({ + scheduleActions: mockScheduleActions, + getUuid: mockGetUuid, + getStart: mockGetStart, +})); const mockSetContext = jest.fn(); const alertRuleData: AlertRuleData = { consumer: 'bar', @@ -67,6 +76,8 @@ const alertRuleData: AlertRuleData = { }; describe('Alerts Client', () => { + let alertsClientParams: AlertsClientParams; + let processAndLogAlertsOpts: ProcessAndLogAlertsOpts; beforeAll(() => { jest.useFakeTimers(); jest.setSystemTime(new Date(date)); @@ -75,6 +86,22 @@ describe('Alerts Client', () => { beforeEach(() => { jest.clearAllMocks(); logger = loggingSystemMock.createLogger(); + alertsClientParams = { + logger, + elasticsearchClientPromise: Promise.resolve(clusterClient), + ruleType, + namespace: 'default', + rule: alertRuleData, + kibanaVersion: '8.9.0', + }; + processAndLogAlertsOpts = { + eventLogger: alertingEventLogger, + ruleRunMetricsStore, + shouldLogAlerts: false, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + notifyWhen: RuleNotifyWhen.CHANGE, + maintenanceWindowIds: [], + }; }); afterAll(() => { @@ -91,13 +118,7 @@ describe('Alerts Client', () => { .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient(alertsClientParams); const opts = { maxAlerts, @@ -152,13 +173,7 @@ describe('Alerts Client', () => { .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient(alertsClientParams); const opts = { maxAlerts, @@ -209,13 +224,7 @@ describe('Alerts Client', () => { .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient(alertsClientParams); const opts = { maxAlerts, @@ -254,13 +263,7 @@ describe('Alerts Client', () => { .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient(alertsClientParams); const opts = { maxAlerts, @@ -297,13 +300,7 @@ describe('Alerts Client', () => { describe('persistAlerts()', () => { test('should index new alerts', async () => { - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -318,14 +315,7 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -342,6 +332,10 @@ describe('Alerts Client', () => { // new alert doc { '@timestamp': date, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -372,15 +366,25 @@ describe('Alerts Client', () => { }, start: date, status: 'active', + time_range: { + gte: date, + }, uuid: uuid1, + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }, { index: { _id: uuid2 } }, // new alert doc { '@timestamp': date, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -411,10 +415,16 @@ describe('Alerts Client', () => { }, start: date, status: 'active', + time_range: { + gte: date, + }, uuid: uuid2, + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }, ], }); @@ -436,6 +446,10 @@ describe('Alerts Client', () => { _index: '.internal.alerts-test.alerts-default-000001', _source: { '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -465,22 +479,22 @@ describe('Alerts Client', () => { }, start: '2023-03-28T12:27:28.159Z', status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abc', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.8.0', }, + tags: ['rule-', '-tags'], }, }, ], }, }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -506,14 +520,7 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -535,6 +542,10 @@ describe('Alerts Client', () => { // ongoing alert doc { '@timestamp': date, + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -565,15 +576,25 @@ describe('Alerts Client', () => { }, start: '2023-03-28T12:27:28.159Z', status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abc', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }, { index: { _id: uuid2 } }, // new alert doc { '@timestamp': date, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -604,10 +625,16 @@ describe('Alerts Client', () => { }, start: date, status: 'active', + time_range: { + gte: date, + }, uuid: uuid2, + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }, ], }); @@ -629,6 +656,10 @@ describe('Alerts Client', () => { _index: '.internal.alerts-test.alerts-default-000001', _source: { '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -658,10 +689,16 @@ describe('Alerts Client', () => { }, start: '2023-03-28T12:27:28.159Z', status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abc', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.8.0', }, + tags: ['rule-', '-tags'], }, }, { @@ -669,6 +706,10 @@ describe('Alerts Client', () => { _index: '.internal.alerts-test.alerts-default-000002', _source: { '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -698,22 +739,22 @@ describe('Alerts Client', () => { }, start: '2023-03-28T02:27:28.159Z', status: 'active', + time_range: { + gte: '2023-03-28T02:27:28.159Z', + }, uuid: 'def', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.8.0', }, + tags: ['rule-', '-tags'], }, }, ], }, }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -749,14 +790,7 @@ describe('Alerts Client', () => { alertExecutorService.create('2').scheduleActions('default'); alertExecutorService.create('3').scheduleActions('default'); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -778,6 +812,10 @@ describe('Alerts Client', () => { // ongoing alert doc { '@timestamp': date, + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -808,15 +846,25 @@ describe('Alerts Client', () => { }, start: '2023-03-28T02:27:28.159Z', status: 'active', + time_range: { + gte: '2023-03-28T02:27:28.159Z', + }, uuid: 'def', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }, { index: { _id: uuid3 } }, // new alert doc { '@timestamp': date, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -847,10 +895,16 @@ describe('Alerts Client', () => { }, start: date, status: 'active', + time_range: { + gte: date, + }, uuid: uuid3, + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }, { index: { @@ -862,6 +916,10 @@ describe('Alerts Client', () => { // recovered alert doc { '@timestamp': date, + event: { + action: 'close', + kind: 'signal', + }, kibana: { alert: { action_group: 'recovered', @@ -893,23 +951,24 @@ describe('Alerts Client', () => { }, start: '2023-03-28T12:27:28.159Z', status: 'recovered', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + lte: date, + }, uuid: 'abc', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }, ], }); }); test('should not try to index if no alerts', async () => { - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -921,14 +980,7 @@ describe('Alerts Client', () => { // Report no alerts - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -968,13 +1020,7 @@ describe('Alerts Client', () => { }, ], }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -989,14 +1035,7 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -1010,13 +1049,7 @@ describe('Alerts Client', () => { clusterClient.bulk.mockImplementation(() => { throw new Error('fail'); }); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -1031,14 +1064,7 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -1051,17 +1077,24 @@ describe('Alerts Client', () => { describe('report()', () => { test('should create legacy alert with id, action group', async () => { - mockLegacyAlertsClient.factory.mockImplementation(() => ({ create: mockCreate })); + const mockGetUuidCurrent = jest + .fn() + .mockReturnValueOnce('uuid1') + .mockReturnValueOnce('uuid2'); + const mockGetStartCurrent = jest.fn().mockReturnValue(null); + const mockScheduleActionsCurrent = jest.fn().mockImplementation(() => ({ + replaceState: mockReplaceState, + getUuid: mockGetUuidCurrent, + getStart: mockGetStartCurrent, + })); + const mockCreateCurrent = jest.fn().mockImplementation(() => ({ + scheduleActions: mockScheduleActionsCurrent, + })); + mockLegacyAlertsClient.factory.mockImplementation(() => ({ create: mockCreateCurrent })); const spy = jest .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -1072,18 +1105,33 @@ describe('Alerts Client', () => { }); // Report 2 new alerts - alertsClient.report({ id: '1', actionGroup: 'default', state: {}, context: {} }); - alertsClient.report({ id: '2', actionGroup: 'default', state: {}, context: {} }); + const { uuid: uuid1, start: start1 } = alertsClient.report({ + id: '1', + actionGroup: 'default', + state: {}, + context: {}, + }); + const { uuid: uuid2, start: start2 } = alertsClient.report({ + id: '2', + actionGroup: 'default', + state: {}, + context: {}, + }); - expect(mockCreate).toHaveBeenCalledTimes(2); - expect(mockCreate).toHaveBeenNthCalledWith(1, '1'); - expect(mockCreate).toHaveBeenNthCalledWith(2, '2'); - expect(mockScheduleActions).toHaveBeenCalledTimes(2); - expect(mockScheduleActions).toHaveBeenNthCalledWith(1, 'default', {}); - expect(mockScheduleActions).toHaveBeenNthCalledWith(2, 'default', {}); + expect(mockCreateCurrent).toHaveBeenCalledTimes(2); + expect(mockCreateCurrent).toHaveBeenNthCalledWith(1, '1'); + expect(mockCreateCurrent).toHaveBeenNthCalledWith(2, '2'); + expect(mockScheduleActionsCurrent).toHaveBeenCalledTimes(2); + expect(mockScheduleActionsCurrent).toHaveBeenNthCalledWith(1, 'default', {}); + expect(mockScheduleActionsCurrent).toHaveBeenNthCalledWith(2, 'default', {}); expect(mockReplaceState).not.toHaveBeenCalled(); spy.mockRestore(); + + expect(uuid1).toEqual('uuid1'); + expect(uuid2).toEqual('uuid2'); + expect(start1).toBeNull(); + expect(start2).toBeNull(); }); test('should set context if defined', async () => { @@ -1091,13 +1139,9 @@ describe('Alerts Client', () => { const spy = jest .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, {}, { foo?: string }, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, { foo?: string }, 'default', 'recovered'>( + alertsClientParams + ); await alertsClient.initializeExecution({ maxAlerts, @@ -1132,13 +1176,9 @@ describe('Alerts Client', () => { const spy = jest .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, { count: number }, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, { count: number }, {}, 'default', 'recovered'>( + alertsClientParams + ); await alertsClient.initializeExecution({ maxAlerts, @@ -1171,13 +1211,7 @@ describe('Alerts Client', () => { {}, 'default', 'recovered' - >({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + >(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -1203,14 +1237,7 @@ describe('Alerts Client', () => { payload: { count: 2, url: `https://url2` }, }); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -1228,6 +1255,10 @@ describe('Alerts Client', () => { { '@timestamp': date, count: 1, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -1258,10 +1289,16 @@ describe('Alerts Client', () => { }, start: date, status: 'active', + time_range: { + gte: date, + }, uuid: uuid1, + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], url: `https://url1`, }, { index: { _id: uuid2 } }, @@ -1269,6 +1306,10 @@ describe('Alerts Client', () => { { '@timestamp': date, count: 2, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -1299,10 +1340,16 @@ describe('Alerts Client', () => { }, start: date, status: 'active', + time_range: { + gte: date, + }, uuid: uuid2, + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], url: `https://url2`, }, ], @@ -1323,13 +1370,7 @@ describe('Alerts Client', () => { const spy = jest .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -1375,13 +1416,9 @@ describe('Alerts Client', () => { const spy = jest .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') .mockImplementation(() => mockLegacyAlertsClient); - const alertsClient = new AlertsClient<{}, {}, { foo?: string }, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, { foo?: string }, 'default', 'recovered'>( + alertsClientParams + ); await alertsClient.initializeExecution({ maxAlerts, @@ -1440,13 +1477,7 @@ describe('Alerts Client', () => { {}, 'default', 'recovered' - >({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + >(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -1471,14 +1502,7 @@ describe('Alerts Client', () => { payload: { count: 100, url: `https://elastic.co` }, }); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -1495,6 +1519,10 @@ describe('Alerts Client', () => { { '@timestamp': date, count: 100, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -1525,10 +1553,16 @@ describe('Alerts Client', () => { }, start: date, status: 'active', + time_range: { + gte: date, + }, uuid: expect.any(String), + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], url: 'https://elastic.co', }, ], @@ -1553,6 +1587,10 @@ describe('Alerts Client', () => { '@timestamp': '2023-03-28T12:27:28.159Z', count: 1, url: 'https://localhost:5601/abc', + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -1582,10 +1620,16 @@ describe('Alerts Client', () => { }, start: '2023-03-28T12:27:28.159Z', status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abc', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.8.0', }, + tags: ['rule-', '-tags'], }, }, ], @@ -1597,13 +1641,7 @@ describe('Alerts Client', () => { {}, 'default', 'recovered' - >({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + >(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -1639,14 +1677,7 @@ describe('Alerts Client', () => { payload: { count: 100, url: `https://elastic.co` }, }); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -1665,6 +1696,10 @@ describe('Alerts Client', () => { { '@timestamp': date, count: 100, + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -1695,10 +1730,16 @@ describe('Alerts Client', () => { }, start: '2023-03-28T12:27:28.159Z', status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abc', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], url: 'https://elastic.co', }, ], @@ -1723,6 +1764,10 @@ describe('Alerts Client', () => { '@timestamp': '2023-03-28T12:27:28.159Z', count: 1, url: 'https://localhost:5601/abc', + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -1750,12 +1795,18 @@ describe('Alerts Client', () => { tags: ['rule-', '-tags'], uuid: '1', }, - start: '2023-03-28T12:27:28.159Z', + start: '2023-03-28T11:27:28.159Z', status: 'active', + time_range: { + gte: '2023-03-28T11:27:28.159Z', + }, uuid: 'abc', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.8.0', }, + tags: ['rule-', '-tags'], }, }, ], @@ -1767,13 +1818,7 @@ describe('Alerts Client', () => { {}, 'default', 'recovered' - >({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + >(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -1781,7 +1826,7 @@ describe('Alerts Client', () => { flappingSettings: DEFAULT_FLAPPING_SETTINGS, activeAlertsFromState: { '1': { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + state: { foo: true, start: '2023-03-28T11:27:28.159Z', duration: '0' }, meta: { flapping: false, flappingHistory: [true], @@ -1803,14 +1848,7 @@ describe('Alerts Client', () => { payload: { count: 100, url: `https://elastic.co` }, }); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, - ruleRunMetricsStore, - shouldLogAlerts: false, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - notifyWhen: RuleNotifyWhen.CHANGE, - maintenanceWindowIds: [], - }); + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); await alertsClient.persistAlerts(); @@ -1829,11 +1867,15 @@ describe('Alerts Client', () => { { '@timestamp': date, count: 100, + event: { + action: 'close', + kind: 'signal', + }, kibana: { alert: { action_group: 'recovered', duration: { - us: '36000000000000', + us: '39600000000000', }, end: date, flapping: false, @@ -1858,12 +1900,19 @@ describe('Alerts Client', () => { tags: ['rule-', '-tags'], uuid: '1', }, - start: '2023-03-28T12:27:28.159Z', + start: '2023-03-28T11:27:28.159Z', status: 'recovered', + time_range: { + gte: '2023-03-28T11:27:28.159Z', + lte: date, + }, uuid: 'abc', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], url: 'https://elastic.co', }, ], @@ -1873,13 +1922,7 @@ describe('Alerts Client', () => { describe('client()', () => { test('only returns subset of functionality', async () => { - const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>({ - logger, - elasticsearchClientPromise: Promise.resolve(clusterClient), - ruleType, - namespace: 'default', - rule: alertRuleData, - }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); await alertsClient.initializeExecution({ maxAlerts, @@ -1899,5 +1942,191 @@ describe('Alerts Client', () => { 'getRecoveredAlerts', ]); }); + + test('should return recovered alert document with recovered alert, if it exists', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 1, + }, + hits: [ + { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + _source: { + '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T12:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, + uuid: 'abc', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.8.0', + }, + tags: ['rule-', '-tags'], + }, + }, + ], + }, + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date() }, + uuid: 'abc', + }, + }, + }, + recoveredAlertsFromState: {}, + }); + + // report no alerts to allow existing alert to recover + + const publicAlertsClient = alertsClient.client(); + const recoveredAlerts = publicAlertsClient.getRecoveredAlerts(); + expect(recoveredAlerts.length).toEqual(1); + const recoveredAlert = recoveredAlerts[0]; + expect(recoveredAlert.alert.getId()).toEqual('1'); + expect(recoveredAlert.alert.getUuid()).toEqual('abc'); + expect(recoveredAlert.alert.getStart()).toEqual('2023-03-28T12:27:28.159Z'); + expect(recoveredAlert.hit).toEqual({ + '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + duration: { + us: '0', + }, + flapping: false, + flapping_history: [true], + instance: { + id: '1', + }, + rule: { + category: 'My test rule', + consumer: 'bar', + execution: { + uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + }, + name: 'rule-name', + parameters: { + bar: true, + }, + producer: 'alerts', + revision: 0, + rule_type_id: 'test.rule-type', + tags: ['rule-', '-tags'], + uuid: '1', + }, + start: '2023-03-28T12:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, + uuid: 'abc', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.8.0', + }, + tags: ['rule-', '-tags'], + }); + }); + + test('should return undefined document with recovered alert, if it does not exists', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { + relation: 'eq', + value: 0, + }, + hits: [], + }, + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(alertsClientParams); + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + maintenanceWindowIds: [], + lastScheduledActions: { group: 'default', date: new Date() }, + uuid: 'abc', + }, + }, + }, + recoveredAlertsFromState: {}, + }); + + // report no alerts to allow existing alert to recover + + const publicAlertsClient = alertsClient.client(); + const recoveredAlerts = publicAlertsClient.getRecoveredAlerts(); + expect(recoveredAlerts.length).toEqual(1); + const recoveredAlert = recoveredAlerts[0]; + expect(recoveredAlert.alert.getId()).toEqual('1'); + expect(recoveredAlert.alert.getUuid()).toEqual('abc'); + expect(recoveredAlert.alert.getStart()).toEqual('2023-03-28T12:27:28.159Z'); + expect(recoveredAlert.hit).toBeUndefined(); + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 3e7e40347ac08..5e49632a2a095 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -30,7 +30,9 @@ import { ProcessAndLogAlertsOpts, TrackedAlerts, ReportedAlert, + ReportedAlertData, UpdateableAlert, + RecursivePartial, } from './types'; import { buildNewAlert, @@ -45,6 +47,7 @@ const CHUNK_SIZE = 10000; export interface AlertsClientParams extends CreateAlertsClientParams { elasticsearchClientPromise: Promise; + kibanaVersion: string; } export class AlertsClient< @@ -75,7 +78,7 @@ export class AlertsClient< private indexTemplateAndPattern: IIndexPatternString; - private reportedAlerts: Record = {}; + private reportedAlerts: Record> = {}; constructor(private readonly options: AlertsClientParams) { this.legacyAlertsClient = new LegacyAlertsClient< @@ -177,7 +180,7 @@ export class AlertsClient< LegacyContext, WithoutReservedActionGroups > - ) { + ): ReportedAlertData { const context = alert.context ? alert.context : ({} as LegacyContext); const state = !isEmpty(alert.state) ? alert.state : null; @@ -195,6 +198,11 @@ export class AlertsClient< if (alert.payload) { this.reportedAlerts[alert.id] = alert.payload; } + + return { + uuid: legacyAlert.getUuid(), + start: legacyAlert.getStart(), + }; } public setAlertData( @@ -273,6 +281,7 @@ export class AlertsClient< rule: this.rule, timestamp: currentTime, payload: this.reportedAlerts[id], + kibanaVersion: this.options.kibanaVersion, }) ); } else { @@ -288,6 +297,7 @@ export class AlertsClient< rule: this.rule, timestamp: currentTime, payload: this.reportedAlerts[id], + kibanaVersion: this.options.kibanaVersion, }) ); } @@ -313,6 +323,7 @@ export class AlertsClient< timestamp: currentTime, payload: this.reportedAlerts[id], recoveryActionGroup: this.options.ruleType.recoveryActionGroup.id, + kibanaVersion: this.options.kibanaVersion, }) : buildUpdatedRecoveredAlert({ alert: this.fetchedAlerts.data[id], @@ -409,7 +420,11 @@ export class AlertsClient< this.factory().alertLimit.setLimitReached(reached), getRecoveredAlerts: () => { const { getRecoveredAlerts } = this.factory().done(); - return getRecoveredAlerts(); + const recoveredLegacyAlerts = getRecoveredAlerts() ?? []; + return recoveredLegacyAlerts.map((alert) => ({ + alert, + hit: this.fetchedAlerts.data[alert.getId()], + })); }, }; } 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 010ce0340c7e6..d31d506854f49 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 @@ -7,6 +7,7 @@ import { Alert as LegacyAlert } from '../../alert/alert'; import { buildNewAlert } from './build_new_alert'; import type { AlertRule } from '../types'; +import { Alert } from '@kbn/alerts-as-data-utils'; const rule = { category: 'My test rule', @@ -43,9 +44,14 @@ describe('buildNewAlert', () => { legacyAlert, rule: alertRule, timestamp: '2023-03-28T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -58,9 +64,12 @@ describe('buildNewAlert', () => { rule, status: 'active', uuid: legacyAlert.getUuid(), + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }); }); @@ -74,9 +83,14 @@ describe('buildNewAlert', () => { legacyAlert, rule: alertRule, timestamp: '2023-03-28T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -92,10 +106,16 @@ describe('buildNewAlert', () => { start: now, rule, status: 'active', + time_range: { + gte: now, + }, uuid: legacyAlert.getUuid(), + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }); }); @@ -110,9 +130,14 @@ describe('buildNewAlert', () => { legacyAlert, rule: alertRule, timestamp: '2023-03-28T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -125,9 +150,12 @@ describe('buildNewAlert', () => { rule, status: 'active', uuid: legacyAlert.getUuid(), + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }); }); @@ -147,10 +175,67 @@ describe('buildNewAlert', () => { rule: alertRule, timestamp: '2023-03-28T12:27:28.159Z', payload: { count: 1, url: `https://url1`, kibana: { alert: { nested_field: 2 } } }, + kibanaVersion: '8.9.0', + }) + ).toEqual({ + '@timestamp': '2023-03-28T12:27:28.159Z', + count: 1, + event: { + action: 'open', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + flapping: false, + flapping_history: [], + instance: { + id: 'alert-A', + }, + maintenance_window_ids: [], + nested_field: 2, + rule, + status: 'active', + uuid: legacyAlert.getUuid(), + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.9.0', + }, + tags: ['rule-', '-tags'], + url: `https://url1`, + }); + }); + + test('should use workflow status from alert payload if set', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A'); + legacyAlert.scheduleActions('default'); + + expect( + buildNewAlert< + Alert & { count: number; url: string; kibana: { alert: { nested_field: number } } }, + {}, + {}, + 'default', + 'recovered' + >({ + legacyAlert, + rule: alertRule, + timestamp: '2023-03-28T12:27:28.159Z', + payload: { + count: 1, + url: `https://url1`, + kibana: { alert: { nested_field: 2, workflow_status: 'custom_workflow' } }, + }, + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-28T12:27:28.159Z', count: 1, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -164,9 +249,12 @@ describe('buildNewAlert', () => { rule, status: 'active', uuid: legacyAlert.getUuid(), + workflow_status: 'custom_workflow', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], url: `https://url1`, }); }); @@ -195,10 +283,73 @@ describe('buildNewAlert', () => { url: `https://url1`, kibana: { alert: { action_group: 'bad action group', nested_field: 2 } }, }, + kibanaVersion: '8.9.0', + }) + ).toEqual({ + '@timestamp': '2023-03-28T12:27:28.159Z', + count: 1, + event: { + action: 'open', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'default', + flapping: false, + flapping_history: [], + instance: { + id: 'alert-A', + }, + maintenance_window_ids: [], + nested_field: 2, + rule, + status: 'active', + uuid: legacyAlert.getUuid(), + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.9.0', + }, + tags: ['rule-', '-tags'], + url: `https://url1`, + }); + }); + + test('should merge and de-dupe rule tags and any tags from payload', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A'); + legacyAlert.scheduleActions('default'); + + expect( + buildNewAlert< + { + count: number; + url: string; + kibana: { alert: { action_group: string; nested_field: number } }; + tags: string[]; + }, + {}, + {}, + 'default', + 'recovered' + >({ + legacyAlert, + rule: alertRule, + timestamp: '2023-03-28T12:27:28.159Z', + payload: { + count: 1, + url: `https://url1`, + kibana: { alert: { action_group: 'bad action group', nested_field: 2 } }, + tags: ['custom-tag1', '-tags'], + }, + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-28T12:27:28.159Z', count: 1, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -212,9 +363,12 @@ describe('buildNewAlert', () => { rule, status: 'active', uuid: legacyAlert.getUuid(), + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['custom-tag1', '-tags', 'rule-'], url: `https://url1`, }); }); 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 b19d5d541082a..f43f944090265 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 @@ -5,10 +5,12 @@ * 2.0. */ import deepmerge from 'deepmerge'; +import { get } from 'lodash'; import type { Alert } from '@kbn/alerts-as-data-utils'; +import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { Alert as LegacyAlert } from '../../alert/alert'; import { AlertInstanceContext, AlertInstanceState, RuleAlertData } from '../../types'; -import type { AlertRule } from '../types'; +import type { AlertRule, RecursivePartial } from '../types'; import { stripFrameworkFields } from './strip_framework_fields'; interface BuildNewAlertOpts< @@ -20,8 +22,9 @@ interface BuildNewAlertOpts< > { legacyAlert: LegacyAlert; rule: AlertRule; - payload?: AlertData; + payload?: RecursivePartial; timestamp: string; + kibanaVersion: string; } /** @@ -40,6 +43,7 @@ export const buildNewAlert = < rule, timestamp, payload, + kibanaVersion, }: BuildNewAlertOpts< AlertData, LegacyState, @@ -47,12 +51,16 @@ export const buildNewAlert = < ActionGroupIds, RecoveryActionGroupId >): Alert & AlertData => { - const cleanedPayload = payload ? stripFrameworkFields(payload) : {}; + const cleanedPayload = stripFrameworkFields(payload); return deepmerge.all( [ cleanedPayload, { '@timestamp': timestamp, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: legacyAlert.getScheduledActionOptions()?.actionGroup, @@ -65,13 +73,28 @@ export const buildNewAlert = < rule: rule.kibana?.alert.rule, status: 'active', uuid: legacyAlert.getUuid(), + workflow_status: get(cleanedPayload, ALERT_WORKFLOW_STATUS, 'open'), ...(legacyAlert.getState().duration ? { duration: { us: legacyAlert.getState().duration } } : {}), - ...(legacyAlert.getState().start ? { start: legacyAlert.getState().start } : {}), + ...(legacyAlert.getState().start + ? { + start: legacyAlert.getState().start, + time_range: { + gte: legacyAlert.getState().start, + }, + } + : {}), }, space_ids: rule.kibana?.space_ids, + version: kibanaVersion, }, + tags: Array.from( + new Set([ + ...((cleanedPayload?.tags as string[]) ?? []), + ...(rule.kibana?.alert.rule.tags ?? []), + ]) + ), }, ], { arrayMerge: (_, sourceArray) => sourceArray } 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 caf45f2d7fccc..078d16602ac84 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 @@ -34,6 +34,10 @@ const alertRule: AlertRule = { }; const existingAlert = { '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'error', @@ -48,10 +52,16 @@ const existingAlert = { start: '2023-03-28T12:27:28.159Z', rule, status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.8.1', }, + tags: ['rule-', '-tags'], }; describe('buildOngoingAlert', () => { @@ -67,9 +77,14 @@ describe('buildOngoingAlert', () => { legacyAlert, rule: alertRule, timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'warning', @@ -85,10 +100,16 @@ describe('buildOngoingAlert', () => { start: '2023-03-28T12:27:28.159Z', rule, status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }); }); @@ -117,9 +138,14 @@ describe('buildOngoingAlert', () => { }, }, timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'warning', @@ -141,10 +167,16 @@ describe('buildOngoingAlert', () => { }, }, status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }); }); @@ -170,9 +202,14 @@ describe('buildOngoingAlert', () => { legacyAlert, rule: alertRule, timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'error', @@ -188,10 +225,16 @@ describe('buildOngoingAlert', () => { start: '2023-03-28T12:27:28.159Z', rule, status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }); }); @@ -222,10 +265,15 @@ describe('buildOngoingAlert', () => { url: `https://url2`, kibana: { alert: { nested_field: 2 } }, }, + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', count: 2, + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'warning', @@ -242,11 +290,17 @@ describe('buildOngoingAlert', () => { start: '2023-03-28T12:27:28.159Z', rule, status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, url: `https://url2`, + tags: ['rule-', '-tags'], }); }); @@ -281,10 +335,15 @@ describe('buildOngoingAlert', () => { url: `https://url2`, kibana: { alert: { action_group: 'bad action group', nested_field: 2 } }, }, + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', count: 2, + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'warning', @@ -301,11 +360,90 @@ describe('buildOngoingAlert', () => { start: '2023-03-28T12:27:28.159Z', rule, status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, url: `https://url2`, + tags: ['rule-', '-tags'], + }); + }); + + test('should merge and de-dupe tags from existing alert, reported payload and rule tags', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A'); + legacyAlert + .scheduleActions('warning') + .replaceState({ start: '0000-00-00T00:00:00.000Z', duration: '36000000' }); + + expect( + buildOngoingAlert< + { + count: number; + url: string; + kibana?: { alert?: { action_group: string; nested_field?: number } }; + tags?: string[]; + }, + {}, + {}, + 'error' | 'warning', + 'recovered' + >({ + alert: { + ...existingAlert, + count: 1, + tags: ['old-tag1', '-tags'], + url: `https://url1`, + }, + legacyAlert, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + payload: { + count: 2, + url: `https://url2`, + kibana: { alert: { action_group: 'bad action group', nested_field: 2 } }, + tags: ['-tags', 'custom-tag2'], + }, + kibanaVersion: '8.9.0', + }) + ).toEqual({ + '@timestamp': '2023-03-29T12:27:28.159Z', + count: 2, + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'warning', + duration: { + us: '36000000', + }, + flapping: false, + flapping_history: [], + instance: { + id: 'alert-A', + }, + maintenance_window_ids: [], + nested_field: 2, + start: '2023-03-28T12:27:28.159Z', + rule, + status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, + uuid: 'abcdefg', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.9.0', + }, + url: `https://url2`, + tags: ['-tags', 'custom-tag2', 'old-tag1', 'rule-'], }); }); @@ -325,10 +463,15 @@ describe('buildOngoingAlert', () => { legacyAlert, rule: alertRule, timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', count: 1, + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'warning', @@ -344,11 +487,17 @@ describe('buildOngoingAlert', () => { start: '2023-03-28T12:27:28.159Z', rule, status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, url: `https://url1`, + tags: ['rule-', '-tags'], }); }); }); 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 6b2944e4930c8..e72112cb91d04 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,7 +9,7 @@ import deepmerge from 'deepmerge'; import type { Alert } from '@kbn/alerts-as-data-utils'; import { Alert as LegacyAlert } from '../../alert/alert'; import { AlertInstanceContext, AlertInstanceState, RuleAlertData } from '../../types'; -import type { AlertRule } from '../types'; +import type { AlertRule, RecursivePartial } from '../types'; import { stripFrameworkFields } from './strip_framework_fields'; interface BuildOngoingAlertOpts< @@ -22,8 +22,9 @@ interface BuildOngoingAlertOpts< alert: Alert & AlertData; legacyAlert: LegacyAlert; rule: AlertRule; - payload?: AlertData; + payload?: RecursivePartial; timestamp: string; + kibanaVersion: string; } /** @@ -43,6 +44,7 @@ export const buildOngoingAlert = < payload, rule, timestamp, + kibanaVersion, }: BuildOngoingAlertOpts< AlertData, LegacyState, @@ -50,7 +52,7 @@ export const buildOngoingAlert = < ActionGroupIds, RecoveryActionGroupId >): Alert & AlertData => { - const cleanedPayload = payload ? stripFrameworkFields(payload) : {}; + const cleanedPayload = stripFrameworkFields(payload); return deepmerge.all( [ alert, @@ -58,6 +60,9 @@ export const buildOngoingAlert = < { // Update the timestamp to reflect latest update time '@timestamp': timestamp, + event: { + action: 'active', + }, kibana: { alert: { // Because we're building this alert after the action execution handler has been @@ -80,13 +85,25 @@ export const buildOngoingAlert = < ? { duration: { us: legacyAlert.getState().duration } } : {}), // Fields that are explicitly not updated: + // event.kind // instance.id // status - ongoing alerts should maintain 'active' status // uuid - ongoing alerts should carry over previous UUID // start - ongoing alerts should keep the initial start time + // time_range - ongoing alerts should keep the initial time_range + // workflow_status - ongoing alerts should keep the initial workflow status }, space_ids: rule.kibana?.space_ids, + // Set latest kibana version + version: kibanaVersion, }, + tags: Array.from( + new Set([ + ...((cleanedPayload?.tags as string[]) ?? []), + ...(alert.tags ?? []), + ...(rule.kibana?.alert.rule.tags ?? []), + ]) + ), }, ], { arrayMerge: (_, sourceArray) => sourceArray } 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 873d0982981ad..03f538dc07ef3 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 @@ -34,6 +34,10 @@ const alertRule: AlertRule = { }; const existingActiveAlert = { '@timestamp': '2023-03-28T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -49,10 +53,16 @@ const existingActiveAlert = { start: '2023-03-28T12:27:28.159Z', rule, status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.8.1', }, + tags: ['rule-', '-tags'], }; describe('buildRecoveredAlert', () => { @@ -69,9 +79,14 @@ describe('buildRecoveredAlert', () => { rule: alertRule, recoveryActionGroup: 'recovered', timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', + event: { + action: 'close', + kind: 'signal', + }, kibana: { alert: { action_group: 'recovered', @@ -88,10 +103,17 @@ describe('buildRecoveredAlert', () => { start: '2023-03-28T12:27:28.159Z', rule, status: 'recovered', + time_range: { + lte: '2023-03-30T12:27:28.159Z', + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }); }); @@ -122,9 +144,14 @@ describe('buildRecoveredAlert', () => { }, }, timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', + event: { + action: 'close', + kind: 'signal', + }, kibana: { alert: { action_group: 'NoLongerActive', @@ -147,10 +174,17 @@ describe('buildRecoveredAlert', () => { }, }, status: 'recovered', + time_range: { + lte: '2023-03-30T12:27:28.159Z', + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, + tags: ['rule-', '-tags'], }); }); @@ -196,10 +230,111 @@ describe('buildRecoveredAlert', () => { }, }, timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + }) + ).toEqual({ + '@timestamp': '2023-03-29T12:27:28.159Z', + count: 2, + event: { + action: 'close', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'NoLongerActive', + duration: { + us: '36000000', + }, + end: '2023-03-30T12:27:28.159Z', + flapping: false, + flapping_history: [], + instance: { + id: 'alert-A', + }, + maintenance_window_ids: ['maint-1', 'maint-321'], + nested_field: 2, + start: '2023-03-28T12:27:28.159Z', + rule: { + ...rule, + name: 'updated-rule-name', + parameters: { + bar: false, + }, + }, + status: 'recovered', + time_range: { + lte: '2023-03-30T12:27:28.159Z', + gte: '2023-03-28T12:27:28.159Z', + }, + uuid: 'abcdefg', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.9.0', + }, + url: `https://url2`, + tags: ['rule-', '-tags'], + }); + }); + + test('should merge and de-dupe tags from existing alert, reported recovery payload and rule tags', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A'); + legacyAlert + .scheduleActions('default') + .replaceState({ end: '2023-03-30T12:27:28.159Z', duration: '36000000' }); + legacyAlert.setMaintenanceWindowIds(['maint-1', 'maint-321']); + + expect( + buildRecoveredAlert< + { + count: number; + url: string; + kibana?: { alert?: { nested_field?: number } }; + tags?: string[]; + }, + {}, + {}, + 'default', + 'recovered' + >({ + alert: { + ...existingActiveAlert, + tags: ['active-alert-tag', 'rule-'], + count: 1, + url: `https://url1`, + }, + legacyAlert, + recoveryActionGroup: 'NoLongerActive', + payload: { + count: 2, + url: `https://url2`, + kibana: { alert: { nested_field: 2 } }, + tags: ['-tags', 'reported-recovery-tag'], + }, + rule: { + kibana: { + alert: { + rule: { + ...rule, + name: 'updated-rule-name', + parameters: { + bar: false, + }, + }, + }, + space_ids: ['default'], + }, + }, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', count: 2, + event: { + action: 'close', + kind: 'signal', + }, kibana: { alert: { action_group: 'NoLongerActive', @@ -223,11 +358,18 @@ describe('buildRecoveredAlert', () => { }, }, status: 'recovered', + time_range: { + lte: '2023-03-30T12:27:28.159Z', + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, url: `https://url2`, + tags: ['-tags', 'reported-recovery-tag', 'active-alert-tag', 'rule-'], }); }); @@ -277,10 +419,15 @@ describe('buildRecoveredAlert', () => { }, }, timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', }) ).toEqual({ '@timestamp': '2023-03-29T12:27:28.159Z', count: 2, + event: { + action: 'close', + kind: 'signal', + }, kibana: { alert: { action_group: 'NoLongerActive', @@ -304,11 +451,18 @@ describe('buildRecoveredAlert', () => { }, }, status: 'recovered', + time_range: { + lte: '2023-03-30T12:27:28.159Z', + gte: '2023-03-28T12:27:28.159Z', + }, uuid: 'abcdefg', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.9.0', }, url: `https://url2`, + tags: ['rule-', '-tags'], }); }); }); 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 4595466aef9b7..44ac250dc55c3 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 @@ -8,7 +8,7 @@ import deepmerge from 'deepmerge'; import type { Alert } from '@kbn/alerts-as-data-utils'; import { Alert as LegacyAlert } from '../../alert/alert'; import { AlertInstanceContext, AlertInstanceState, RuleAlertData } from '../../types'; -import type { AlertRule } from '../types'; +import type { AlertRule, RecursivePartial } from '../types'; import { stripFrameworkFields } from './strip_framework_fields'; interface BuildRecoveredAlertOpts< @@ -22,8 +22,9 @@ interface BuildRecoveredAlertOpts< legacyAlert: LegacyAlert; rule: AlertRule; recoveryActionGroup: string; - payload?: AlertData; + payload?: RecursivePartial; timestamp: string; + kibanaVersion: string; } /** @@ -44,6 +45,7 @@ export const buildRecoveredAlert = < timestamp, payload, recoveryActionGroup, + kibanaVersion, }: BuildRecoveredAlertOpts< AlertData, LegacyState, @@ -51,7 +53,7 @@ export const buildRecoveredAlert = < ActionGroupIds, RecoveryActionGroupId >): Alert & AlertData => { - const cleanedPayload = payload ? stripFrameworkFields(payload) : {}; + const cleanedPayload = stripFrameworkFields(payload); return deepmerge.all( [ alert, @@ -59,6 +61,9 @@ export const buildRecoveredAlert = < { // Update the timestamp to reflect latest update time '@timestamp': timestamp, + event: { + action: 'close', + }, kibana: { alert: { // Set the recovery action group @@ -78,16 +83,34 @@ export const buildRecoveredAlert = < ? { duration: { us: legacyAlert.getState().duration } } : {}), // Set end time - ...(legacyAlert.getState().end ? { end: legacyAlert.getState().end } : {}), + ...(legacyAlert.getState().end + ? { + end: legacyAlert.getState().end, + time_range: { + // this should get merged with a time_range.gte + lte: legacyAlert.getState().end, + }, + } + : {}), // Fields that are explicitly not updated: // instance.id // action_group // uuid - recovered alerts should carry over previous UUID // start - recovered alerts should keep the initial start time + // workflow_status - recovered alerts should keep the initial workflow_status }, space_ids: rule.kibana?.space_ids, + // Set latest kibana version + version: kibanaVersion, }, + tags: Array.from( + new Set([ + ...((cleanedPayload?.tags as string[]) ?? []), + ...(alert.tags ?? []), + ...(rule.kibana?.alert.rule.tags ?? []), + ]) + ), }, ], { arrayMerge: (_, sourceArray) => sourceArray } diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.test.ts b/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.test.ts index b78600e052901..5e61cbda2ac92 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.test.ts @@ -7,15 +7,22 @@ import { stripFrameworkFields } from './strip_framework_fields'; describe('stripFrameworkFields', () => { + test('should return empty object if payload is undefined', () => { + expect(stripFrameworkFields()).toEqual({}); + }); + test('should do nothing if payload has no framework fields', () => { const payload = { field1: 'test', kibana: { alert: { not_a_framework_field: 2 } } }; expect(stripFrameworkFields(payload)).toEqual(payload); }); - test(`should allow allowed fields like "kibana.alert.reason"`, () => { + test(`should allow fields from the allowlist`, () => { const payload = { field1: 'test', - kibana: { alert: { not_a_framework_field: 2, reason: 'because i said so' } }, + kibana: { + alert: { not_a_framework_field: 2, reason: 'because i said so', workflow_status: 'custom' }, + }, + tags: ['taggity-tag'], }; expect(stripFrameworkFields(payload)).toEqual(payload); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.ts b/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.ts index d55c8e5152620..9d91d7ec1beae 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.ts @@ -6,11 +6,11 @@ */ import { omit } from 'lodash'; -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_WORKFLOW_STATUS, TAGS } from '@kbn/rule-data-utils'; import { alertFieldMap } from '@kbn/alerts-as-data-utils'; import { RuleAlertData } from '../../types'; -const allowedFrameworkFields = new Set([ALERT_REASON]); +const allowedFrameworkFields = new Set([ALERT_REASON, ALERT_WORKFLOW_STATUS, TAGS]); /** * Remove framework fields from the alert payload reported by @@ -19,8 +19,12 @@ const allowedFrameworkFields = new Set([ALERT_REASON]); * set by the alerting framework during rule execution. */ export const stripFrameworkFields = ( - payload: AlertData + payload?: AlertData ): AlertData => { + if (!payload) { + return {} as AlertData; + } + const keysToStrip = Object.keys(alertFieldMap).filter( (key: string) => !allowedFrameworkFields.has(key) ); diff --git a/x-pack/plugins/alerting/server/alerts_client/types.ts b/x-pack/plugins/alerting/server/alerts_client/types.ts index ec29fc26461f0..fd5b41827635b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/types.ts +++ b/x-pack/plugins/alerting/server/alerts_client/types.ts @@ -96,17 +96,22 @@ export interface TrackedAlerts< recovered: Record>; } +// allows Partial on nested objects +export type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + export interface PublicAlertsClient< AlertData extends RuleAlertData, State extends AlertInstanceState, Context extends AlertInstanceContext, ActionGroupIds extends string > { - report(alert: ReportedAlert): void; + report(alert: ReportedAlert): ReportedAlertData; setAlertData(alert: UpdateableAlert): void; getAlertLimitValue: () => number; setAlertLimitReached: (reached: boolean) => void; - getRecoveredAlerts: () => Array>; + getRecoveredAlerts: () => Array>; } export interface ReportedAlert< @@ -119,7 +124,22 @@ export interface ReportedAlert< actionGroup: ActionGroupIds; state?: State; context?: Context; - payload?: AlertData; + payload?: RecursivePartial; +} + +export interface RecoveredAlertData< + AlertData extends RuleAlertData, + State extends AlertInstanceState, + Context extends AlertInstanceContext, + ActionGroupIds extends string +> { + alert: LegacyAlert; + hit?: AlertData; +} + +export interface ReportedAlertData { + uuid: string; + start: string | null; } export type UpdateableAlert< diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts index 9b20483b10571..61f7760f6c58f 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts @@ -1190,6 +1190,7 @@ describe('Alerts Service', () => { spaceId: 'default', tags: ['rule-', '-tags'], }, + kibanaVersion: '8.8.0', }); }); @@ -1291,6 +1292,7 @@ describe('Alerts Service', () => { spaceId: 'default', tags: ['rule-', '-tags'], }, + kibanaVersion: '8.8.0', }); expect(result).not.toBe(null); @@ -1394,6 +1396,7 @@ describe('Alerts Service', () => { spaceId: 'default', tags: ['rule-', '-tags'], }, + kibanaVersion: '8.8.0', }); expect(result[0]).not.toBe(null); @@ -1467,6 +1470,7 @@ describe('Alerts Service', () => { spaceId: 'default', tags: ['rule-', '-tags'], }, + kibanaVersion: '8.8.0', }); expect(result).not.toBe(null); @@ -1555,6 +1559,7 @@ describe('Alerts Service', () => { spaceId: 'default', tags: ['rule-', '-tags'], }, + kibanaVersion: '8.8.0', }); expect(result[0]).not.toBe(null); diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts index 7fcd03c93c80f..20a1c3cd453f0 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts @@ -222,6 +222,7 @@ export class AlertsService implements IAlertsService { ruleType: opts.ruleType, namespace: opts.namespace, rule: opts.rule, + kibanaVersion: this.options.kibanaVersion, }); } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index cbdd67f98d6ea..f26734e0dc447 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -17,6 +17,7 @@ import { Rule, RuleAction, MaintenanceWindow, + RuleAlertData, } from '../types'; import { ConcreteTaskInstance, isUnrecoverableError } from '@kbn/task-manager-plugin/server'; import { TaskRunnerContext } from './task_runner_factory'; @@ -339,7 +340,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -422,7 +424,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -548,7 +551,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -594,7 +598,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -672,7 +677,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); executorServices.alertFactory.create('2').scheduleActions('default'); @@ -739,7 +745,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); executorServices.alertFactory.create('2').scheduleActions('default'); @@ -802,7 +809,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); executorServices.alertFactory.create('2').scheduleActions('default'); @@ -844,7 +852,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -917,7 +926,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -993,7 +1003,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -1085,7 +1096,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -1226,7 +1238,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); @@ -1335,7 +1348,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -1435,7 +1449,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -1512,7 +1527,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -1584,7 +1600,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -1699,7 +1716,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { throw new Error(GENERIC_ERROR_MESSAGE); } @@ -1807,7 +1825,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { throw new Error(GENERIC_ERROR_MESSAGE); } @@ -1975,7 +1994,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); executorServices.alertFactory.create('2').scheduleActions('default'); @@ -2066,7 +2086,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); executorServices.alertFactory.create('2').scheduleActions('default'); @@ -2150,7 +2171,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); executorServices.alertFactory.create('2').scheduleActions('default'); @@ -2299,7 +2321,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { return { state: {} }; } @@ -2469,7 +2492,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { throw new Error(GENERIC_ERROR_MESSAGE); } @@ -2501,7 +2525,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { throw new Error(GENERIC_ERROR_MESSAGE); } @@ -2557,7 +2582,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { throw new Error(GENERIC_ERROR_MESSAGE); } @@ -2627,7 +2653,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -2795,7 +2822,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); executorServices.alertFactory.create('2').scheduleActions('default'); @@ -2961,7 +2989,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { throw new Error('OMG'); } @@ -2988,7 +3017,8 @@ describe('Task Runner', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; 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 b4ad1d812d4ae..dbffbb9ff2268 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 @@ -473,6 +473,10 @@ describe('Task Runner', () => { // new alert doc { '@timestamp': DATE_1970, + event: { + action: 'open', + kind: 'signal', + }, kibana: { alert: { action_group: 'default', @@ -503,12 +507,18 @@ describe('Task Runner', () => { }, start: DATE_1970, status: 'active', + time_range: { + gte: DATE_1970, + }, uuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + workflow_status: 'open', }, space_ids: ['default'], + version: '8.8.0', }, numericField: 27, textField: 'foo', + tags: ['rule-', '-tags'], }, ], }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 18cbaabd5a765..3aac319b4e8e6 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -14,6 +14,7 @@ import { AlertInstanceState, AlertInstanceContext, Rule, + RuleAlertData, } from '../types'; import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; import { TaskRunnerContext } from './task_runner_factory'; @@ -288,7 +289,8 @@ describe('Task Runner Cancel', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -356,7 +358,8 @@ describe('Task Runner Cancel', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; @@ -418,7 +421,8 @@ describe('Task Runner Cancel', () => { RuleTypeState, AlertInstanceState, AlertInstanceContext, - string + string, + RuleAlertData >) => { executorServices.alertFactory.create('1').scheduleActions('default'); return { state: {} }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/alert_instance_factory_stub.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/alert_instance_factory_stub.ts index c1d63acf00bdb..df087c7d9775b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/alert_instance_factory_stub.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/alert_instance_factory_stub.ts @@ -47,6 +47,9 @@ export const alertInstanceFactoryStub = < getContext() { return {} as unknown as TInstanceContext; }, + getStart() { + return null; + }, hasContext() { return false; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index b4bc1a2c03e72..e1107e7ae135b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -194,6 +194,7 @@ export const previewRulesRoute = async ( | 'getContext' | 'hasContext' | 'getUuid' + | 'getStart' >; alertLimit: { getValue: () => number; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts index fded7e8ee37b9..98a5fa5e1f05c 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts @@ -699,7 +699,8 @@ async function invokeExecutor({ services: ruleServices as unknown as RuleExecutorServices< EsQueryRuleState, ActionContext, - typeof ActionGroupId + typeof ActionGroupId, + never >, params: params as EsQueryRuleParams, state: { diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts index b424fb742b5ab..1af9d845c68a3 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts @@ -190,7 +190,8 @@ describe('ruleType', () => { services: alertServices as unknown as RuleExecutorServices< {}, ActionContext, - typeof ActionGroupId + typeof ActionGroupId, + never >, params, state: { @@ -257,7 +258,8 @@ describe('ruleType', () => { services: customAlertServices as unknown as RuleExecutorServices< {}, ActionContext, - typeof ActionGroupId + typeof ActionGroupId, + never >, params, state: { @@ -324,7 +326,8 @@ describe('ruleType', () => { services: customAlertServices as unknown as RuleExecutorServices< {}, ActionContext, - typeof ActionGroupId + typeof ActionGroupId, + never >, params, state: { @@ -390,7 +393,8 @@ describe('ruleType', () => { services: alertServices as unknown as RuleExecutorServices< {}, ActionContext, - typeof ActionGroupId + typeof ActionGroupId, + never >, params, state: { diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts index c8c8c5bba8bdb..5003acd160f29 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts @@ -597,7 +597,7 @@ function getPatternFiringAlertsAsDataRuleType() { // set recovery payload for (const recoveredAlert of alertsClient.getRecoveredAlerts()) { alertsClient.setAlertData({ - id: recoveredAlert.getId(), + id: recoveredAlert.alert.getId(), payload: { patternIndex: -1, instancePattern: [] }, }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data.ts index ab83fbde26c79..13f3c5d445916 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data.ts @@ -111,6 +111,9 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F // start should be defined expect(source.kibana.alert.start).to.match(timestampPattern); + // time_range.gte should be same as start + expect(source.kibana.alert.time_range?.gte).to.equal(source.kibana.alert.start); + // timestamp should be defined expect(source['@timestamp']).to.match(timestampPattern); @@ -120,6 +123,18 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F // flapping information for new alert expect(source.kibana.alert.flapping).to.equal(false); expect(source.kibana.alert.flapping_history).to.eql([true]); + + // workflow status should be 'open' + expect(source.kibana.alert.workflow_status).to.equal('open'); + + // event.action should be 'open' + expect(source.event?.action).to.equal('open'); + + // event.kind should be 'signal' + 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']); } let alertDoc: SearchHit | undefined = alertDocsRun1.find( @@ -198,6 +213,17 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F ...alertADocRun1.kibana.alert.flapping_history!, false, ]); + // event.action set to active + expect(alertADocRun2.event?.action).to.eql('active'); + expect(alertADocRun2.tags).to.eql(['foo']); + // these values should be the same as previous run + expect(alertADocRun2.event?.kind).to.eql(alertADocRun1.event?.kind); + expect(alertADocRun2.kibana.alert.workflow_status).to.eql( + alertADocRun1.kibana.alert.workflow_status + ); + expect(alertADocRun2.kibana.alert.time_range?.gte).to.equal( + alertADocRun1.kibana.alert.time_range?.gte + ); // alertB, run 2 // status is updated to recovered, duration is updated, end time is set @@ -226,6 +252,19 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F ...alertBDocRun1.kibana.alert.flapping_history!, true, ]); + // event.action set to close + expect(alertBDocRun2.event?.action).to.eql('close'); + expect(alertBDocRun2.tags).to.eql(['foo']); + // these values should be the same as previous run + expect(alertBDocRun2.event?.kind).to.eql(alertBDocRun1.event?.kind); + expect(alertBDocRun2.kibana.alert.workflow_status).to.eql( + alertBDocRun1.kibana.alert.workflow_status + ); + expect(alertBDocRun2.kibana.alert.time_range?.gte).to.equal( + alertBDocRun1.kibana.alert.time_range?.gte + ); + // time_range.lte should be set to end time + expect(alertBDocRun2.kibana.alert.time_range?.lte).to.equal(alertBDocRun2.kibana.alert.end); // alertC, run 2 // status is updated to recovered, duration is updated, end time is set @@ -254,6 +293,19 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F ...alertCDocRun1.kibana.alert.flapping_history!, true, ]); + // event.action set to close + expect(alertCDocRun2.event?.action).to.eql('close'); + expect(alertCDocRun2.tags).to.eql(['foo']); + // these values should be the same as previous run + expect(alertCDocRun2.event?.kind).to.eql(alertADocRun1.event?.kind); + expect(alertCDocRun2.kibana.alert.workflow_status).to.eql( + alertCDocRun1.kibana.alert.workflow_status + ); + expect(alertCDocRun2.kibana.alert.time_range?.gte).to.equal( + alertCDocRun1.kibana.alert.time_range?.gte + ); + // time_range.lte should be set to end time + expect(alertCDocRun2.kibana.alert.time_range?.lte).to.equal(alertCDocRun2.kibana.alert.end); // -------------------------- // RUN 3 - 1 re-active (alertC), 1 still recovered (alertB), 1 ongoing (alertA) @@ -312,6 +364,17 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F ...alertADocRun2.kibana.alert.flapping_history!, false, ]); + // event.action should still to active + expect(alertADocRun3.event?.action).to.eql('active'); + expect(alertADocRun3.tags).to.eql(['foo']); + // these values should be the same as previous run + expect(alertADocRun3.event?.kind).to.eql(alertADocRun2.event?.kind); + expect(alertADocRun3.kibana.alert.workflow_status).to.eql( + alertADocRun2.kibana.alert.workflow_status + ); + expect(alertADocRun3.kibana.alert.time_range?.gte).to.equal( + alertADocRun2.kibana.alert.time_range?.gte + ); // alertB doc should be unchanged from prior run because it is still recovered // but its flapping history should be updated @@ -361,6 +424,13 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F ...alertCDocRun2.kibana.alert.flapping_history!, true, ]); + // event.action should be 'open' + expect(alertCDocRun3.event?.action).to.eql('open'); + expect(alertCDocRun3.tags).to.eql(['foo']); + // these values should be the same as previous run + expect(alertCDocRun3.event?.kind).to.eql('signal'); + expect(alertCDocRun3.kibana.alert.workflow_status).to.eql('open'); + expect(alertCDocRun3.kibana.alert.time_range?.gte).to.equal(alertCDocRun3.kibana.alert.start); }); }); From dc5bebab1b86c7065fb9ef66b8d01da5491ae5d7 Mon Sep 17 00:00:00 2001 From: Ying Date: Wed, 28 Jun 2023 11:41:23 -0400 Subject: [PATCH 2/4] Moving metric threshold rule to FAAD --- .../metric_threshold_executor.ts | 241 +++++++++--------- .../lib/alerting/register_rule_types.ts | 1 + .../common/utils/alerting/alert_url.ts | 2 +- 3 files changed, 117 insertions(+), 127 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index cfe7a9cf94924..4ae8da88877d4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ALERT_ACTION_GROUP, ALERT_EVALUATION_VALUES, ALERT_REASON } from '@kbn/rule-data-utils'; +import { type ObservabilityMetricsAlert } from '@kbn/alerts-as-data-utils'; import { isEqual } from 'lodash'; import { ActionGroupIdsOf, @@ -14,7 +14,7 @@ import { AlertInstanceState as AlertState, RecoveredActionGroup, } from '@kbn/alerting-plugin/common'; -import { Alert, RuleTypeState } from '@kbn/alerting-plugin/server'; +import { RuleExecutorOptions, RuleTypeState } from '@kbn/alerting-plugin/server'; import type { TimeUnitChar } from '@kbn/observability-plugin/common'; import { getAlertUrl } from '@kbn/observability-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; @@ -30,13 +30,11 @@ import { } from '../common/messages'; import { createScopedLogger, - AdditionalContext, getContextForRecoveredAlerts, getViewInMetricsAppUrl, UNGROUPED_FACTORY_KEY, hasAdditionalContext, validGroupByForContext, - flattenAdditionalContext, getGroupByObject, } from '../common/utils'; @@ -68,28 +66,19 @@ type MetricThresholdAllowedActionGroups = ActionGroupIdsOf< typeof FIRED_ACTIONS | typeof WARNING_ACTIONS | typeof NO_DATA_ACTIONS >; -type MetricThresholdAlert = Alert< - MetricThresholdAlertState, - MetricThresholdAlertContext, - MetricThresholdAllowedActionGroups ->; - -type MetricThresholdAlertFactory = ( - id: string, - reason: string, - actionGroup: MetricThresholdActionGroup, - additionalContext?: AdditionalContext | null, - evaluationValues?: Array -) => MetricThresholdAlert; - -export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => - libs.metricsRules.createLifecycleRuleExecutor< - MetricThresholdRuleParams, - MetricThresholdRuleTypeState, - MetricThresholdAlertState, - MetricThresholdAlertContext, - MetricThresholdAllowedActionGroups - >(async function (options) { +// type MetricsAlert = Omit +export const createMetricThresholdExecutor = + (libs: InfraBackendLibs) => + async ( + options: RuleExecutorOptions< + MetricThresholdRuleParams, + MetricThresholdRuleTypeState, + MetricThresholdAlertState, + MetricThresholdAlertContext, + MetricThresholdAllowedActionGroups, + ObservabilityMetricsAlert + > + ) => { const startTime = Date.now(); const { @@ -110,31 +99,11 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => executionId, }); - const { - alertWithLifecycle, - savedObjectsClient, - getAlertUuid, - getAlertStartedDate, - getAlertByAlertUuid, - } = services; - - const alertFactory: MetricThresholdAlertFactory = ( - id, - reason, - actionGroup, - additionalContext, - evaluationValues - ) => - alertWithLifecycle({ - id, - fields: { - [ALERT_REASON]: reason, - [ALERT_ACTION_GROUP]: actionGroup, - [ALERT_EVALUATION_VALUES]: evaluationValues, - ...flattenAdditionalContext(additionalContext), - }, - }); + const { alertsClient, savedObjectsClient } = services; + if (!alertsClient) { + throw new Error(`Expected alertsClient to be defined but it was not!`); + } const { sourceId, alertOnNoData, @@ -154,26 +123,37 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const timestamp = startedAt.toISOString(); const actionGroupId = FIRED_ACTIONS_ID; // Change this to an Error action group when able const reason = buildInvalidQueryAlertReason(params.filterQueryText); - const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason, actionGroupId); - const alertUuid = getAlertUuid(UNGROUPED_FACTORY_KEY); - const indexedStartedAt = - getAlertStartedDate(UNGROUPED_FACTORY_KEY) ?? startedAt.toISOString(); - - alert.scheduleActions(actionGroupId, { - alertDetailsUrl: await getAlertUrl( - alertUuid, - spaceId, - indexedStartedAt, - libs.alertsLocator, - libs.basePath.publicBaseUrl - ), - alertState: stateToAlertMessage[AlertStates.ERROR], - group: UNGROUPED_FACTORY_KEY, - metric: mapToConditionsLookup(criteria, (c) => c.metric), - reason, - timestamp, - value: null, - viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId), + + const { uuid, start } = alertsClient.report({ + id: UNGROUPED_FACTORY_KEY, + actionGroup: actionGroupId, + }); + + alertsClient.setAlertData({ + id: UNGROUPED_FACTORY_KEY, + payload: { + kibana: { + alert: { + reason, + }, + }, + }, + context: { + alertDetailsUrl: await getAlertUrl( + uuid, + spaceId, + start, + libs.alertsLocator, + libs.basePath.publicBaseUrl + ), + alertState: stateToAlertMessage[AlertStates.ERROR], + group: UNGROUPED_FACTORY_KEY, + metric: mapToConditionsLookup(criteria, (c) => c.metric), + reason, + timestamp, + value: null, + viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId), + }, }); return { @@ -315,77 +295,86 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => return acc; }, []); - const alert = alertFactory( - `${group}`, - reason, - actionGroupId, - additionalContext, - evaluationValues - ); - const alertUuid = getAlertUuid(group); - const indexedStartedAt = getAlertStartedDate(group) ?? startedAt.toISOString(); - scheduledActionsCount++; + const { uuid, start } = alertsClient?.report({ + id: `${group}`, + actionGroup: actionGroupId, + }); - alert.scheduleActions(actionGroupId, { - alertDetailsUrl: await getAlertUrl( - alertUuid, - spaceId, - indexedStartedAt, - libs.alertsLocator, - libs.basePath.publicBaseUrl - ), - alertState: stateToAlertMessage[nextState], - group, - groupByKeys: groupByKeysObjectMapping[group], - metric: mapToConditionsLookup(criteria, (c) => { - if (c.aggType === 'count') { - return 'count'; - } - return c.metric; - }), - reason, - threshold: mapToConditionsLookup(alertResults, (result, index) => { - const evaluation = result[group]; - if (!evaluation) { - return criteria[index].threshold; - } - return formatAlertResult(evaluation).threshold; - }), - timestamp, - value: mapToConditionsLookup(alertResults, (result, index) => { - const evaluation = result[group]; - if (!evaluation && criteria[index].aggType === 'count') { - return 0; - } else if (!evaluation) { - return null; - } - return formatAlertResult(evaluation).currentValue; - }), - viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId), - ...additionalContext, + alertsClient?.setAlertData({ + id: `${group}`, + context: { + alertDetailsUrl: await getAlertUrl( + uuid, + spaceId, + start, + libs.alertsLocator, + libs.basePath.publicBaseUrl + ), + alertState: stateToAlertMessage[nextState], + group, + groupByKeys: groupByKeysObjectMapping[group], + metric: mapToConditionsLookup(criteria, (c) => { + if (c.aggType === 'count') { + return 'count'; + } + return c.metric; + }), + reason, + threshold: mapToConditionsLookup(alertResults, (result, index) => { + const evaluation = result[group]; + if (!evaluation) { + return criteria[index].threshold; + } + return formatAlertResult(evaluation).threshold; + }), + timestamp, + value: mapToConditionsLookup(alertResults, (result, index) => { + const evaluation = result[group]; + if (!evaluation && criteria[index].aggType === 'count') { + return 0; + } else if (!evaluation) { + return null; + } + return formatAlertResult(evaluation).currentValue; + }), + viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId), + ...additionalContext, + }, + payload: { + kibana: { + alert: { + evaluation: { + values: evaluationValues, + }, + reason, + }, + }, + ...additionalContext, + }, }); + + scheduledActionsCount++; } } - const { getRecoveredAlerts } = services.alertFactory.done(); - const recoveredAlerts = getRecoveredAlerts(); + const recoveredAlerts = alertsClient?.getRecoveredAlerts() ?? []; const groupByKeysObjectForRecovered = getGroupByObject( params.groupBy, - new Set(recoveredAlerts.map((recoveredAlert) => recoveredAlert.getId())) + new Set(recoveredAlerts.map((recoveredAlert) => recoveredAlert.alert.getId())) ); for (const alert of recoveredAlerts) { - const recoveredAlertId = alert.getId(); - const alertUuid = getAlertUuid(recoveredAlertId); + const recoveredAlertId = alert.alert.getId(); + const alertUuid = alert.alert.getUuid(); const timestamp = startedAt.toISOString(); - const indexedStartedAt = getAlertStartedDate(recoveredAlertId) ?? timestamp; + const indexedStartedAt = alert.alert.getStart() ?? timestamp; - const alertHits = alertUuid ? await getAlertByAlertUuid(alertUuid) : undefined; + const alertHits = alert.hit; const additionalContext = getContextForRecoveredAlerts(alertHits); const originalActionGroup = getOriginalActionGroup(alertHits); - alert.setContext({ + alert.alert.setContext({ alertDetailsUrl: await getAlertUrl( alertUuid, spaceId, @@ -425,7 +414,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => filterQuery: params.filterQuery, }, }; - }); + }; export const FIRED_ACTIONS = { id: 'metrics.threshold.fired', diff --git a/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts index ee05dc38cc1f5..d893338d08da4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts @@ -30,6 +30,7 @@ export const MetricsRulesTypeAlertDefinition: IRuleTypeAlerts = { mappings: { fieldMap: legacyExperimentalFieldMap }, useEcs: true, useLegacyAlerts: true, + shouldWrite: true, }; const registerRuleTypes = ( diff --git a/x-pack/plugins/observability/common/utils/alerting/alert_url.ts b/x-pack/plugins/observability/common/utils/alerting/alert_url.ts index cf3f81c2e9e08..77b53819abd83 100644 --- a/x-pack/plugins/observability/common/utils/alerting/alert_url.ts +++ b/x-pack/plugins/observability/common/utils/alerting/alert_url.ts @@ -14,7 +14,7 @@ import { AlertsLocatorParams } from '../..'; export const getAlertUrl = async ( alertUuid: string | null, spaceId: string, - startedAt: string, + startedAt: string | null, alertsLocator?: LocatorPublic, publicBaseUrl?: string ) => { From e82cff9963f561511e7291470c6244242f849022 Mon Sep 17 00:00:00 2001 From: Ying Date: Wed, 28 Jun 2023 11:41:34 -0400 Subject: [PATCH 3/4] Revert "Moving metric threshold rule to FAAD" This reverts commit dc5bebab1b86c7065fb9ef66b8d01da5491ae5d7. --- .../metric_threshold_executor.ts | 241 +++++++++--------- .../lib/alerting/register_rule_types.ts | 1 - .../common/utils/alerting/alert_url.ts | 2 +- 3 files changed, 127 insertions(+), 117 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 4ae8da88877d4..cfe7a9cf94924 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { type ObservabilityMetricsAlert } from '@kbn/alerts-as-data-utils'; +import { ALERT_ACTION_GROUP, ALERT_EVALUATION_VALUES, ALERT_REASON } from '@kbn/rule-data-utils'; import { isEqual } from 'lodash'; import { ActionGroupIdsOf, @@ -14,7 +14,7 @@ import { AlertInstanceState as AlertState, RecoveredActionGroup, } from '@kbn/alerting-plugin/common'; -import { RuleExecutorOptions, RuleTypeState } from '@kbn/alerting-plugin/server'; +import { Alert, RuleTypeState } from '@kbn/alerting-plugin/server'; import type { TimeUnitChar } from '@kbn/observability-plugin/common'; import { getAlertUrl } from '@kbn/observability-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; @@ -30,11 +30,13 @@ import { } from '../common/messages'; import { createScopedLogger, + AdditionalContext, getContextForRecoveredAlerts, getViewInMetricsAppUrl, UNGROUPED_FACTORY_KEY, hasAdditionalContext, validGroupByForContext, + flattenAdditionalContext, getGroupByObject, } from '../common/utils'; @@ -66,19 +68,28 @@ type MetricThresholdAllowedActionGroups = ActionGroupIdsOf< typeof FIRED_ACTIONS | typeof WARNING_ACTIONS | typeof NO_DATA_ACTIONS >; -// type MetricsAlert = Omit -export const createMetricThresholdExecutor = - (libs: InfraBackendLibs) => - async ( - options: RuleExecutorOptions< - MetricThresholdRuleParams, - MetricThresholdRuleTypeState, - MetricThresholdAlertState, - MetricThresholdAlertContext, - MetricThresholdAllowedActionGroups, - ObservabilityMetricsAlert - > - ) => { +type MetricThresholdAlert = Alert< + MetricThresholdAlertState, + MetricThresholdAlertContext, + MetricThresholdAllowedActionGroups +>; + +type MetricThresholdAlertFactory = ( + id: string, + reason: string, + actionGroup: MetricThresholdActionGroup, + additionalContext?: AdditionalContext | null, + evaluationValues?: Array +) => MetricThresholdAlert; + +export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => + libs.metricsRules.createLifecycleRuleExecutor< + MetricThresholdRuleParams, + MetricThresholdRuleTypeState, + MetricThresholdAlertState, + MetricThresholdAlertContext, + MetricThresholdAllowedActionGroups + >(async function (options) { const startTime = Date.now(); const { @@ -99,11 +110,31 @@ export const createMetricThresholdExecutor = executionId, }); - const { alertsClient, savedObjectsClient } = services; + const { + alertWithLifecycle, + savedObjectsClient, + getAlertUuid, + getAlertStartedDate, + getAlertByAlertUuid, + } = services; + + const alertFactory: MetricThresholdAlertFactory = ( + id, + reason, + actionGroup, + additionalContext, + evaluationValues + ) => + alertWithLifecycle({ + id, + fields: { + [ALERT_REASON]: reason, + [ALERT_ACTION_GROUP]: actionGroup, + [ALERT_EVALUATION_VALUES]: evaluationValues, + ...flattenAdditionalContext(additionalContext), + }, + }); - if (!alertsClient) { - throw new Error(`Expected alertsClient to be defined but it was not!`); - } const { sourceId, alertOnNoData, @@ -123,37 +154,26 @@ export const createMetricThresholdExecutor = const timestamp = startedAt.toISOString(); const actionGroupId = FIRED_ACTIONS_ID; // Change this to an Error action group when able const reason = buildInvalidQueryAlertReason(params.filterQueryText); - - const { uuid, start } = alertsClient.report({ - id: UNGROUPED_FACTORY_KEY, - actionGroup: actionGroupId, - }); - - alertsClient.setAlertData({ - id: UNGROUPED_FACTORY_KEY, - payload: { - kibana: { - alert: { - reason, - }, - }, - }, - context: { - alertDetailsUrl: await getAlertUrl( - uuid, - spaceId, - start, - libs.alertsLocator, - libs.basePath.publicBaseUrl - ), - alertState: stateToAlertMessage[AlertStates.ERROR], - group: UNGROUPED_FACTORY_KEY, - metric: mapToConditionsLookup(criteria, (c) => c.metric), - reason, - timestamp, - value: null, - viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId), - }, + const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason, actionGroupId); + const alertUuid = getAlertUuid(UNGROUPED_FACTORY_KEY); + const indexedStartedAt = + getAlertStartedDate(UNGROUPED_FACTORY_KEY) ?? startedAt.toISOString(); + + alert.scheduleActions(actionGroupId, { + alertDetailsUrl: await getAlertUrl( + alertUuid, + spaceId, + indexedStartedAt, + libs.alertsLocator, + libs.basePath.publicBaseUrl + ), + alertState: stateToAlertMessage[AlertStates.ERROR], + group: UNGROUPED_FACTORY_KEY, + metric: mapToConditionsLookup(criteria, (c) => c.metric), + reason, + timestamp, + value: null, + viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId), }); return { @@ -295,86 +315,77 @@ export const createMetricThresholdExecutor = return acc; }, []); - const { uuid, start } = alertsClient?.report({ - id: `${group}`, - actionGroup: actionGroupId, - }); + const alert = alertFactory( + `${group}`, + reason, + actionGroupId, + additionalContext, + evaluationValues + ); + const alertUuid = getAlertUuid(group); + const indexedStartedAt = getAlertStartedDate(group) ?? startedAt.toISOString(); + scheduledActionsCount++; - alertsClient?.setAlertData({ - id: `${group}`, - context: { - alertDetailsUrl: await getAlertUrl( - uuid, - spaceId, - start, - libs.alertsLocator, - libs.basePath.publicBaseUrl - ), - alertState: stateToAlertMessage[nextState], - group, - groupByKeys: groupByKeysObjectMapping[group], - metric: mapToConditionsLookup(criteria, (c) => { - if (c.aggType === 'count') { - return 'count'; - } - return c.metric; - }), - reason, - threshold: mapToConditionsLookup(alertResults, (result, index) => { - const evaluation = result[group]; - if (!evaluation) { - return criteria[index].threshold; - } - return formatAlertResult(evaluation).threshold; - }), - timestamp, - value: mapToConditionsLookup(alertResults, (result, index) => { - const evaluation = result[group]; - if (!evaluation && criteria[index].aggType === 'count') { - return 0; - } else if (!evaluation) { - return null; - } - return formatAlertResult(evaluation).currentValue; - }), - viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId), - ...additionalContext, - }, - payload: { - kibana: { - alert: { - evaluation: { - values: evaluationValues, - }, - reason, - }, - }, - ...additionalContext, - }, + alert.scheduleActions(actionGroupId, { + alertDetailsUrl: await getAlertUrl( + alertUuid, + spaceId, + indexedStartedAt, + libs.alertsLocator, + libs.basePath.publicBaseUrl + ), + alertState: stateToAlertMessage[nextState], + group, + groupByKeys: groupByKeysObjectMapping[group], + metric: mapToConditionsLookup(criteria, (c) => { + if (c.aggType === 'count') { + return 'count'; + } + return c.metric; + }), + reason, + threshold: mapToConditionsLookup(alertResults, (result, index) => { + const evaluation = result[group]; + if (!evaluation) { + return criteria[index].threshold; + } + return formatAlertResult(evaluation).threshold; + }), + timestamp, + value: mapToConditionsLookup(alertResults, (result, index) => { + const evaluation = result[group]; + if (!evaluation && criteria[index].aggType === 'count') { + return 0; + } else if (!evaluation) { + return null; + } + return formatAlertResult(evaluation).currentValue; + }), + viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId), + ...additionalContext, }); - - scheduledActionsCount++; } } - const recoveredAlerts = alertsClient?.getRecoveredAlerts() ?? []; + const { getRecoveredAlerts } = services.alertFactory.done(); + const recoveredAlerts = getRecoveredAlerts(); const groupByKeysObjectForRecovered = getGroupByObject( params.groupBy, - new Set(recoveredAlerts.map((recoveredAlert) => recoveredAlert.alert.getId())) + new Set(recoveredAlerts.map((recoveredAlert) => recoveredAlert.getId())) ); for (const alert of recoveredAlerts) { - const recoveredAlertId = alert.alert.getId(); - const alertUuid = alert.alert.getUuid(); + const recoveredAlertId = alert.getId(); + const alertUuid = getAlertUuid(recoveredAlertId); const timestamp = startedAt.toISOString(); - const indexedStartedAt = alert.alert.getStart() ?? timestamp; + const indexedStartedAt = getAlertStartedDate(recoveredAlertId) ?? timestamp; - const alertHits = alert.hit; + const alertHits = alertUuid ? await getAlertByAlertUuid(alertUuid) : undefined; const additionalContext = getContextForRecoveredAlerts(alertHits); const originalActionGroup = getOriginalActionGroup(alertHits); - alert.alert.setContext({ + alert.setContext({ alertDetailsUrl: await getAlertUrl( alertUuid, spaceId, @@ -414,7 +425,7 @@ export const createMetricThresholdExecutor = filterQuery: params.filterQuery, }, }; - }; + }); export const FIRED_ACTIONS = { id: 'metrics.threshold.fired', diff --git a/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts index d893338d08da4..ee05dc38cc1f5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts @@ -30,7 +30,6 @@ export const MetricsRulesTypeAlertDefinition: IRuleTypeAlerts = { mappings: { fieldMap: legacyExperimentalFieldMap }, useEcs: true, useLegacyAlerts: true, - shouldWrite: true, }; const registerRuleTypes = ( diff --git a/x-pack/plugins/observability/common/utils/alerting/alert_url.ts b/x-pack/plugins/observability/common/utils/alerting/alert_url.ts index 77b53819abd83..cf3f81c2e9e08 100644 --- a/x-pack/plugins/observability/common/utils/alerting/alert_url.ts +++ b/x-pack/plugins/observability/common/utils/alerting/alert_url.ts @@ -14,7 +14,7 @@ import { AlertsLocatorParams } from '../..'; export const getAlertUrl = async ( alertUuid: string | null, spaceId: string, - startedAt: string | null, + startedAt: string, alertsLocator?: LocatorPublic, publicBaseUrl?: string ) => { From 425c34f78e5632eea4cbce5c3e5c58712b34df8a Mon Sep 17 00:00:00 2001 From: Ying Date: Wed, 28 Jun 2023 17:00:29 -0400 Subject: [PATCH 4/4] PR feedback --- x-pack/plugins/alerting/server/alert/alert.ts | 2 +- .../alerting/server/alerts_client/alerts_client.ts | 4 ++-- .../alerting/server/alerts_client/lib/build_new_alert.ts | 5 +++-- .../server/alerts_client/lib/build_ongoing_alert.ts | 5 +++-- .../server/alerts_client/lib/build_recovered_alert.ts | 5 +++-- x-pack/plugins/alerting/server/alerts_client/types.ts | 8 ++------ 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/alerting/server/alert/alert.ts b/x-pack/plugins/alerting/server/alert/alert.ts index a506d7461b8e7..4e08a314bb3a4 100644 --- a/x-pack/plugins/alerting/server/alert/alert.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -78,7 +78,7 @@ export class Alert< } getStart(): string | null { - return this.state.start ? (this.state.start as string) : null; + return this.state.start ? `${this.state.start}` : null; } hasScheduledActions() { diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 5e49632a2a095..e5a18f5f17632 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -11,6 +11,7 @@ import { chunk, flatMap, isEmpty, keys } from 'lodash'; import { SearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Alert } from '@kbn/alerts-as-data-utils'; import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; +import { DeepPartial } from '@kbn/utility-types'; import { AlertInstanceContext, AlertInstanceState, @@ -32,7 +33,6 @@ import { ReportedAlert, ReportedAlertData, UpdateableAlert, - RecursivePartial, } from './types'; import { buildNewAlert, @@ -78,7 +78,7 @@ export class AlertsClient< private indexTemplateAndPattern: IIndexPatternString; - private reportedAlerts: Record> = {}; + private reportedAlerts: Record> = {}; constructor(private readonly options: AlertsClientParams) { this.legacyAlertsClient = new LegacyAlertsClient< 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 f43f944090265..5d163504a9606 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 @@ -8,9 +8,10 @@ import deepmerge from 'deepmerge'; import { get } from 'lodash'; import type { Alert } from '@kbn/alerts-as-data-utils'; import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; +import { DeepPartial } from '@kbn/utility-types'; import { Alert as LegacyAlert } from '../../alert/alert'; import { AlertInstanceContext, AlertInstanceState, RuleAlertData } from '../../types'; -import type { AlertRule, RecursivePartial } from '../types'; +import type { AlertRule } from '../types'; import { stripFrameworkFields } from './strip_framework_fields'; interface BuildNewAlertOpts< @@ -22,7 +23,7 @@ interface BuildNewAlertOpts< > { legacyAlert: LegacyAlert; rule: AlertRule; - payload?: RecursivePartial; + payload?: DeepPartial; timestamp: string; kibanaVersion: string; } 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 e72112cb91d04..491c4dfe7cca7 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 @@ -7,9 +7,10 @@ import deepmerge from 'deepmerge'; import type { Alert } from '@kbn/alerts-as-data-utils'; +import { DeepPartial } from '@kbn/utility-types'; import { Alert as LegacyAlert } from '../../alert/alert'; import { AlertInstanceContext, AlertInstanceState, RuleAlertData } from '../../types'; -import type { AlertRule, RecursivePartial } from '../types'; +import type { AlertRule } from '../types'; import { stripFrameworkFields } from './strip_framework_fields'; interface BuildOngoingAlertOpts< @@ -22,7 +23,7 @@ interface BuildOngoingAlertOpts< alert: Alert & AlertData; legacyAlert: LegacyAlert; rule: AlertRule; - payload?: RecursivePartial; + payload?: DeepPartial; timestamp: string; kibanaVersion: string; } 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 44ac250dc55c3..b283844acbc63 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 @@ -6,9 +6,10 @@ */ import deepmerge from 'deepmerge'; import type { Alert } from '@kbn/alerts-as-data-utils'; +import { DeepPartial } from '@kbn/utility-types'; import { Alert as LegacyAlert } from '../../alert/alert'; import { AlertInstanceContext, AlertInstanceState, RuleAlertData } from '../../types'; -import type { AlertRule, RecursivePartial } from '../types'; +import type { AlertRule } from '../types'; import { stripFrameworkFields } from './strip_framework_fields'; interface BuildRecoveredAlertOpts< @@ -22,7 +23,7 @@ interface BuildRecoveredAlertOpts< legacyAlert: LegacyAlert; rule: AlertRule; recoveryActionGroup: string; - payload?: RecursivePartial; + payload?: DeepPartial; timestamp: string; kibanaVersion: string; } diff --git a/x-pack/plugins/alerting/server/alerts_client/types.ts b/x-pack/plugins/alerting/server/alerts_client/types.ts index fd5b41827635b..5502e01e9d1d6 100644 --- a/x-pack/plugins/alerting/server/alerts_client/types.ts +++ b/x-pack/plugins/alerting/server/alerts_client/types.ts @@ -6,6 +6,7 @@ */ import type { Alert } from '@kbn/alerts-as-data-utils'; +import { DeepPartial } from '@kbn/utility-types'; import { Alert as LegacyAlert } from '../alert/alert'; import { AlertInstanceContext, @@ -96,11 +97,6 @@ export interface TrackedAlerts< recovered: Record>; } -// allows Partial on nested objects -export type RecursivePartial = { - [P in keyof T]?: RecursivePartial; -}; - export interface PublicAlertsClient< AlertData extends RuleAlertData, State extends AlertInstanceState, @@ -124,7 +120,7 @@ export interface ReportedAlert< actionGroup: ActionGroupIds; state?: State; context?: Context; - payload?: RecursivePartial; + payload?: DeepPartial; } export interface RecoveredAlertData<