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 18d4a7bc019b6..36542065799f5 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 @@ -110,6 +110,149 @@ const mockCreate = jest.fn().mockImplementation(() => ({ })); const mockSetContext = jest.fn(); +const trackedAlert1Raw = { + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + meta: { + flapping: false, + flappingHistory: [true], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'abc', + }, +}; +const trackedAlert1 = new Alert('1', trackedAlert1Raw); +const trackedAlert2Raw = { + state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, + meta: { + flapping: false, + flappingHistory: [true, false, false], + lastScheduledActions: { group: 'default', date: new Date().toISOString() }, + uuid: 'def', + }, +}; +const trackedAlert2 = new Alert('2', trackedAlert2Raw); +const trackedRecovered3 = new Alert('3', { + state: { foo: false }, + meta: { + flapping: false, + flappingHistory: [true, false, false], + uuid: 'xyz', + }, +}); + +const fetchedAlert1 = { + [TIMESTAMP]: '2023-03-28T12:27:28.159Z', + [EVENT_ACTION]: 'open', + [EVENT_KIND]: 'signal', + [ALERT_ACTION_GROUP]: 'default', + [ALERT_DURATION]: '0', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [true], + [ALERT_INSTANCE_ID]: '1', + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_RULE_CATEGORY]: 'My test rule', + [ALERT_RULE_CONSUMER]: 'bar', + [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + [ALERT_RULE_NAME]: 'rule-name', + [ALERT_RULE_PARAMETERS]: { bar: true }, + [ALERT_RULE_PRODUCER]: 'alerts', + [ALERT_RULE_REVISION]: 0, + [ALERT_RULE_TYPE_ID]: 'test.rule-type', + [ALERT_RULE_TAGS]: ['rule-', '-tags'], + [ALERT_RULE_UUID]: '1', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_STATUS]: 'active', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [ALERT_UUID]: 'abc', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['default'], + [VERSION]: '8.8.0', + [TAGS]: ['rule-', '-tags'], +}; + +const fetchedAlert2 = { + [TIMESTAMP]: '2023-03-28T13:27:28.159Z', + [EVENT_ACTION]: 'active', + [EVENT_KIND]: 'signal', + [ALERT_ACTION_GROUP]: 'default', + [ALERT_DURATION]: '36000000000000', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [true, false], + [ALERT_INSTANCE_ID]: '2', + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_RULE_CATEGORY]: 'My test rule', + [ALERT_RULE_CONSUMER]: 'bar', + [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + [ALERT_RULE_NAME]: 'rule-name', + [ALERT_RULE_PARAMETERS]: { bar: true }, + [ALERT_RULE_PRODUCER]: 'alerts', + [ALERT_RULE_REVISION]: 0, + [ALERT_RULE_TYPE_ID]: 'test.rule-type', + [ALERT_RULE_TAGS]: ['rule-', '-tags'], + [ALERT_RULE_UUID]: '1', + [ALERT_START]: '2023-03-28T02:27:28.159Z', + [ALERT_STATUS]: 'active', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T02:27:28.159Z' }, + [ALERT_UUID]: 'def', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['default'], + [VERSION]: '8.8.0', + [TAGS]: ['rule-', '-tags'], +}; + +const getNewIndexedAlertDoc = (overrides = {}) => ({ + [TIMESTAMP]: date, + [EVENT_ACTION]: 'open', + [EVENT_KIND]: 'signal', + [ALERT_ACTION_GROUP]: 'default', + [ALERT_DURATION]: '0', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [true], + [ALERT_INSTANCE_ID]: '1', + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_RULE_CATEGORY]: 'My test rule', + [ALERT_RULE_CONSUMER]: 'bar', + [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + [ALERT_RULE_NAME]: 'rule-name', + [ALERT_RULE_PARAMETERS]: { bar: true }, + [ALERT_RULE_PRODUCER]: 'alerts', + [ALERT_RULE_REVISION]: 0, + [ALERT_RULE_TYPE_ID]: 'test.rule-type', + [ALERT_RULE_TAGS]: ['rule-', '-tags'], + [ALERT_RULE_UUID]: '1', + [ALERT_START]: date, + [ALERT_STATUS]: 'active', + [ALERT_TIME_RANGE]: { gte: date }, + [ALERT_UUID]: 'uuid', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...overrides, +}); + +const getOngoingIndexedAlertDoc = (overrides = {}) => ({ + ...getNewIndexedAlertDoc(), + [EVENT_ACTION]: 'active', + [ALERT_DURATION]: '36000000000000', + [ALERT_FLAPPING_HISTORY]: [true, false], + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + ...overrides, +}); + +const getRecoveredIndexedAlertDoc = (overrides = {}) => ({ + ...getNewIndexedAlertDoc(), + [EVENT_ACTION]: 'close', + [ALERT_DURATION]: '36000000000000', + [ALERT_ACTION_GROUP]: 'recovered', + [ALERT_FLAPPING_HISTORY]: [true, true], + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_END]: date, + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: date }, + [ALERT_STATUS]: 'recovered', + ...overrides, +}); + describe('Alerts Client', () => { let alertsClientParams: AlertsClientParams; let processAndLogAlertsOpts: ProcessAndLogAlertsOpts; @@ -209,36 +352,8 @@ describe('Alerts Client', () => { test('should query for alert UUIDs if they exist', async () => { mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ - active: { - '1': new Alert('1', { - state: { foo: true }, - meta: { - flapping: false, - flappingHistory: [true, false], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }), - '2': new Alert('2', { - state: { foo: false }, - meta: { - flapping: false, - flappingHistory: [true, false, false], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'def', - }, - }), - }, - recovered: { - '3': new Alert('3', { - state: { foo: false }, - meta: { - flapping: false, - flappingHistory: [true, false, false], - uuid: 'xyz', - }, - }), - }, + active: { '1': trackedAlert1, '2': trackedAlert2 }, + recovered: { '3': trackedRecovered3 }, })); const spy = jest .spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient') @@ -321,17 +436,7 @@ describe('Alerts Client', () => { throw new Error('search failed!'); }); mockLegacyAlertsClient.getTrackedAlerts.mockImplementation(() => ({ - active: { - '1': new Alert('1', { - state: { foo: true }, - meta: { - flapping: false, - flappingHistory: [true, false], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }), - }, + active: { '1': trackedAlert1 }, recovered: {}, })); const spy = jest @@ -413,68 +518,12 @@ describe('Alerts Client', () => { create: { _id: uuid1, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, }, // new alert doc - { - [TIMESTAMP]: date, - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_START]: date, - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: date }, - [ALERT_UUID]: uuid1, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], - }, + getNewIndexedAlertDoc({ [ALERT_UUID]: uuid1 }), { create: { _id: uuid2, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, }, // new alert doc - { - [TIMESTAMP]: date, - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '2', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_START]: date, - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: date }, - [ALERT_UUID]: uuid2, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], - }, + getNewIndexedAlertDoc({ [ALERT_UUID]: uuid2, [ALERT_INSTANCE_ID]: '2' }), ], }); }); @@ -485,45 +534,14 @@ describe('Alerts Client', () => { timed_out: false, _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, hits: { - total: { - relation: 'eq', - value: 1, - }, + total: { relation: 'eq', value: 1 }, hits: [ { _id: 'abc', _index: '.internal.alerts-test.alerts-default-000001', _seq_no: 41, _primary_term: 665, - _source: { - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_UUID]: 'abc', - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.0', - [TAGS]: ['rule-', '-tags'], - }, + _source: fetchedAlert1, }, ], }, @@ -537,16 +555,76 @@ describe('Alerts Client', () => { 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().toISOString() }, - uuid: 'abc', + '1': trackedAlert1Raw, + }, + recoveredAlertsFromState: {}, + }); + + // Report 1 new alert and 1 active alert + const alertExecutorService = alertsClient.factory(); + alertExecutorService.create('1').scheduleActions('default'); + alertExecutorService.create('2').scheduleActions('default'); + + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + + await alertsClient.persistAlerts(); + + const { alertsToReturn } = alertsClient.getAlertsToSerialize(); + const uuid2 = alertsToReturn['2'].meta?.uuid; + + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: true, + require_alias: !useDataStreamForAlerts, + body: [ + { + index: { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + if_seq_no: 41, + if_primary_term: 665, + require_alias: false, }, }, + // ongoing alert doc + getOngoingIndexedAlertDoc({ [ALERT_UUID]: 'abc' }), + { + create: { _id: uuid2, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, + }, + // new alert doc + getNewIndexedAlertDoc({ [ALERT_UUID]: uuid2, [ALERT_INSTANCE_ID]: '2' }), + ], + }); + }); + + test('should update unflattened ongoing alerts in existing index', 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', + _seq_no: 41, + _primary_term: 665, + _source: expandFlattenedAlert(fetchedAlert1), + }, + ], + }, + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': trackedAlert1Raw, }, recoveredAlertsFromState: {}, }); @@ -579,14 +657,20 @@ describe('Alerts Client', () => { }, // ongoing alert doc { + event: { kind: 'signal' }, + kibana: { + alert: { + instance: { id: '1' }, + start: '2023-03-28T12:27:28.159Z', + uuid: 'abc', + }, + }, [TIMESTAMP]: date, [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', [ALERT_DURATION]: '36000000000000', [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true, false], - [ALERT_INSTANCE_ID]: '1', [ALERT_MAINTENANCE_WINDOW_IDS]: [], [ALERT_RULE_CATEGORY]: 'My test rule', [ALERT_RULE_CONSUMER]: 'bar', @@ -598,10 +682,8 @@ describe('Alerts Client', () => { [ALERT_RULE_TYPE_ID]: 'test.rule-type', [ALERT_RULE_TAGS]: ['rule-', '-tags'], [ALERT_RULE_UUID]: '1', - [ALERT_START]: '2023-03-28T12:27:28.159Z', [ALERT_STATUS]: 'active', [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_UUID]: 'abc', [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: ['default'], [VERSION]: '8.9.0', @@ -611,42 +693,14 @@ describe('Alerts Client', () => { create: { _id: uuid2, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, }, // new alert doc - { - [TIMESTAMP]: date, - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '2', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_START]: date, - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: date }, - [ALERT_UUID]: uuid2, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], - }, + getNewIndexedAlertDoc({ [ALERT_UUID]: uuid2, [ALERT_INSTANCE_ID]: '2' }), ], }); }); test('should not update ongoing alerts in existing index when they are not in the processed alerts', async () => { const activeAlert = { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, + state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '36000000000000' }, meta: { flapping: false, flappingHistory: [true, false], @@ -670,43 +724,12 @@ describe('Alerts Client', () => { timed_out: false, _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, hits: { - total: { - relation: 'eq', - value: 1, - }, + 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', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_UUID]: 'abc', - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.0', - [TAGS]: ['rule-', '-tags'], - }, + _source: fetchedAlert1, }, ], }, @@ -755,35 +778,7 @@ describe('Alerts Client', () => { }, }, // ongoing alert doc - { - [TIMESTAMP]: date, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true, false], - [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_UUID]: 'abc', - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], - }, + getOngoingIndexedAlertDoc({ [ALERT_UUID]: 'abc' }), ], }); }); @@ -794,80 +789,21 @@ describe('Alerts Client', () => { timed_out: false, _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, hits: { - total: { - relation: 'eq', - value: 1, - }, + total: { relation: 'eq', value: 2 }, hits: [ { _id: 'abc', _index: '.internal.alerts-test.alerts-default-000001', _seq_no: 41, _primary_term: 665, - _source: { - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_UUID]: 'abc', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.0', - [TAGS]: ['rule-', '-tags'], - }, + _source: fetchedAlert1, }, { _id: 'def', _index: '.internal.alerts-test.alerts-default-000002', _seq_no: 42, _primary_term: 666, - _source: { - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '36000000000000', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true, false], - [ALERT_INSTANCE_ID]: '2', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_UUID]: 'def', - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.0', - [TAGS]: ['rule-', '-tags'], - }, + _source: fetchedAlert2, }, ], }, @@ -881,26 +817,103 @@ describe('Alerts Client', () => { 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().toISOString() }, - uuid: 'abc', + '1': trackedAlert1Raw, + '2': trackedAlert2Raw, + }, + recoveredAlertsFromState: {}, + }); + + // Report 1 new alert and 1 active alert, recover 1 alert + const alertExecutorService = alertsClient.factory(); + alertExecutorService.create('2').scheduleActions('default'); + alertExecutorService.create('3').scheduleActions('default'); + + alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + + await alertsClient.persistAlerts(); + + const { alertsToReturn } = alertsClient.getAlertsToSerialize(); + const uuid3 = alertsToReturn['3'].meta?.uuid; + + expect(clusterClient.bulk).toHaveBeenCalledWith({ + index: '.alerts-test.alerts-default', + refresh: true, + require_alias: !useDataStreamForAlerts, + body: [ + { + index: { + _id: 'def', + _index: '.internal.alerts-test.alerts-default-000002', + if_seq_no: 42, + if_primary_term: 666, + require_alias: false, }, }, - '2': { - state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, - meta: { - flapping: false, - flappingHistory: [true, false], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'def', + // ongoing alert doc + getOngoingIndexedAlertDoc({ + [ALERT_UUID]: 'def', + [ALERT_INSTANCE_ID]: '2', + [ALERT_FLAPPING_HISTORY]: [true, false, false, false], + [ALERT_DURATION]: '72000000000000', + [ALERT_START]: '2023-03-28T02:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T02:27:28.159Z' }, + }), + { + create: { _id: uuid3, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, + }, + // new alert doc + getNewIndexedAlertDoc({ [ALERT_UUID]: uuid3, [ALERT_INSTANCE_ID]: '3' }), + { + index: { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + if_seq_no: 41, + if_primary_term: 665, + require_alias: false, }, }, + // recovered alert doc + getRecoveredIndexedAlertDoc({ [ALERT_UUID]: 'abc' }), + ], + }); + }); + + test('should recover unflattened recovered alerts in existing index', async () => { + clusterClient.search.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, + hits: { + total: { relation: 'eq', value: 2 }, + hits: [ + { + _id: 'abc', + _index: '.internal.alerts-test.alerts-default-000001', + _seq_no: 41, + _primary_term: 665, + _source: expandFlattenedAlert(fetchedAlert1), + }, + { + _id: 'def', + _index: '.internal.alerts-test.alerts-default-000002', + _seq_no: 42, + _primary_term: 666, + _source: expandFlattenedAlert(fetchedAlert2), + }, + ], + }, + }); + const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( + alertsClientParams + ); + + await alertsClient.initializeExecution({ + maxAlerts, + ruleLabel: `test: rule-name`, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + activeAlertsFromState: { + '1': trackedAlert1Raw, + '2': trackedAlert2Raw, }, recoveredAlertsFromState: {}, }); @@ -933,14 +946,20 @@ describe('Alerts Client', () => { }, // ongoing alert doc { + event: { kind: 'signal' }, + kibana: { + alert: { + instance: { id: '2' }, + start: '2023-03-28T02:27:28.159Z', + uuid: 'def', + }, + }, [TIMESTAMP]: date, [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'default', [ALERT_DURATION]: '72000000000000', [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true, false, false], - [ALERT_INSTANCE_ID]: '2', + [ALERT_FLAPPING_HISTORY]: [true, false, false, false], [ALERT_MAINTENANCE_WINDOW_IDS]: [], [ALERT_RULE_CATEGORY]: 'My test rule', [ALERT_RULE_CONSUMER]: 'bar', @@ -952,10 +971,8 @@ describe('Alerts Client', () => { [ALERT_RULE_TYPE_ID]: 'test.rule-type', [ALERT_RULE_TAGS]: ['rule-', '-tags'], [ALERT_RULE_UUID]: '1', - [ALERT_START]: '2023-03-28T12:27:28.159Z', [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_UUID]: 'def', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T02:27:28.159Z' }, [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: ['default'], [VERSION]: '8.9.0', @@ -965,35 +982,7 @@ describe('Alerts Client', () => { create: { _id: uuid3, ...(useDataStreamForAlerts ? {} : { require_alias: true }) }, }, // new alert doc - { - [TIMESTAMP]: date, - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '3', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_START]: date, - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: date }, - [ALERT_UUID]: uuid3, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], - }, + getNewIndexedAlertDoc({ [ALERT_UUID]: uuid3, [ALERT_INSTANCE_ID]: '3' }), { index: { _id: 'abc', @@ -1005,14 +994,19 @@ describe('Alerts Client', () => { }, // recovered alert doc { + event: { kind: 'signal' }, + kibana: { + alert: { + instance: { id: '1' }, + uuid: 'abc', + }, + }, [TIMESTAMP]: date, [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'recovered', [ALERT_DURATION]: '36000000000000', [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true, true], - [ALERT_INSTANCE_ID]: '1', [ALERT_MAINTENANCE_WINDOW_IDS]: [], [ALERT_RULE_CATEGORY]: 'My test rule', [ALERT_RULE_CONSUMER]: 'bar', @@ -1028,7 +1022,6 @@ describe('Alerts Client', () => { [ALERT_END]: date, [ALERT_STATUS]: 'recovered', [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: date }, - [ALERT_UUID]: 'abc', [ALERT_WORKFLOW_STATUS]: 'open', [SPACE_IDS]: ['default'], [VERSION]: '8.9.0', @@ -1081,11 +1074,7 @@ describe('Alerts Client', () => { _id: '1', _version: 1, result: 'created', - _shards: { - total: 2, - successful: 1, - failed: 0, - }, + _shards: { total: 2, successful: 1, failed: 0 }, status: 201, _seq_no: 0, _primary_term: 1, @@ -1126,80 +1115,21 @@ describe('Alerts Client', () => { timed_out: false, _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, hits: { - total: { - relation: 'eq', - value: 2, - }, + total: { relation: 'eq', value: 2 }, hits: [ { _id: 'abc', _index: 'partial-.internal.alerts-test.alerts-default-000001', _seq_no: 41, _primary_term: 665, - _source: { - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_UUID]: 'abc', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.0', - [TAGS]: ['rule-', '-tags'], - }, + _source: fetchedAlert1, }, { - _id: 'xyz', + _id: 'def', _index: '.internal.alerts-test.alerts-default-000002', - _seq_no: 41, - _primary_term: 665, - _source: { - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '2', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_UUID]: 'xyz', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.0', - [TAGS]: ['rule-', '-tags'], - }, + _seq_no: 42, + _primary_term: 666, + _source: fetchedAlert2, }, ], }, @@ -1213,26 +1143,8 @@ describe('Alerts Client', () => { 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().toISOString() }, - uuid: 'abc', - }, - }, - '2': { - state: { foo: true, start: '2023-03-28T12:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'xyz', - }, - }, + '1': trackedAlert1Raw, + '2': trackedAlert2Raw, }, recoveredAlertsFromState: {}, }); @@ -1253,43 +1165,22 @@ describe('Alerts Client', () => { body: [ { index: { - _id: 'xyz', + _id: 'def', _index: '.internal.alerts-test.alerts-default-000002', - if_seq_no: 41, - if_primary_term: 665, + if_seq_no: 42, + if_primary_term: 666, require_alias: false, }, }, // ongoing alert doc - { - [TIMESTAMP]: date, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '36000000000000', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true, false], + getOngoingIndexedAlertDoc({ + [ALERT_UUID]: 'def', [ALERT_INSTANCE_ID]: '2', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_UUID]: 'xyz', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], - }, + [ALERT_DURATION]: '72000000000000', + [ALERT_FLAPPING_HISTORY]: [true, false, false, false], + [ALERT_START]: '2023-03-28T02:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T02:27:28.159Z' }, + }), ], }); @@ -1901,7 +1792,7 @@ describe('Alerts Client', () => { }, // new alert doc { - [TIMESTAMP]: date, + ...getNewIndexedAlertDoc({ [ALERT_UUID]: uuid1 }), count: 1, url: `https://url1`, [EVENT_ACTION]: 'open', @@ -1936,7 +1827,7 @@ describe('Alerts Client', () => { }, // new alert doc { - [TIMESTAMP]: date, + ...getNewIndexedAlertDoc({ [ALERT_UUID]: uuid2, [ALERT_INSTANCE_ID]: '2' }), count: 2, url: `https://url2`, [EVENT_ACTION]: 'open', @@ -1993,26 +1884,8 @@ describe('Alerts Client', () => { 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().toISOString() }, - uuid: 'abc', - }, - }, - '2': { - state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, - meta: { - flapping: false, - flappingHistory: [true, false], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'def', - }, - }, + '1': trackedAlert1Raw, + '2': trackedAlert2Raw, }, recoveredAlertsFromState: {}, }); @@ -2041,26 +1914,8 @@ describe('Alerts Client', () => { 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().toISOString() }, - uuid: 'abc', - }, - }, - '2': { - state: { foo: true, start: '2023-03-28T02:27:28.159Z', duration: '36000000000000' }, - meta: { - flapping: false, - flappingHistory: [true, false], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'def', - }, - }, + '1': trackedAlert1Raw, + '2': trackedAlert2Raw, }, recoveredAlertsFromState: {}, }); @@ -2080,10 +1935,7 @@ describe('Alerts Client', () => { timed_out: false, _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, hits: { - total: { - relation: 'eq', - value: 0, - }, + total: { relation: 'eq', value: 0 }, hits: [], }, }); @@ -2134,7 +1986,7 @@ describe('Alerts Client', () => { }, }, { - [TIMESTAMP]: date, + ...getNewIndexedAlertDoc({ [ALERT_UUID]: expect.any(String) }), count: 100, url: 'https://elastic.co', [EVENT_ACTION]: 'open', @@ -2174,44 +2026,15 @@ describe('Alerts Client', () => { timed_out: false, _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, hits: { - total: { - relation: 'eq', - value: 1, - }, + total: { relation: 'eq', value: 1 }, hits: [ { _id: 'abc', _index: '.internal.alerts-test.alerts-default-000001', _source: { - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', + ...fetchedAlert1, count: 1, url: 'https://localhost:5601/abc', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_UUID]: 'abc', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.0', - [TAGS]: ['rule-', '-tags'], }, }, ], @@ -2230,16 +2053,7 @@ describe('Alerts Client', () => { 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().toISOString() }, - uuid: 'abc', - }, - }, + '1': trackedAlert1Raw, }, recoveredAlertsFromState: {}, }); @@ -2275,7 +2089,7 @@ describe('Alerts Client', () => { }, }, { - [TIMESTAMP]: date, + ...getOngoingIndexedAlertDoc({ [ALERT_UUID]: 'abc' }), count: 100, url: 'https://elastic.co', [EVENT_ACTION]: 'active', @@ -2315,10 +2129,7 @@ describe('Alerts Client', () => { timed_out: false, _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, hits: { - total: { - relation: 'eq', - value: 1, - }, + total: { relation: 'eq', value: 1 }, hits: [ { _id: 'abc', @@ -2326,35 +2137,9 @@ describe('Alerts Client', () => { _seq_no: 42, _primary_term: 666, _source: { - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', + ...fetchedAlert1, count: 1, url: 'https://localhost:5601/abc', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_RULE_CATEGORY]: 'My test rule', - [ALERT_RULE_CONSUMER]: 'bar', - [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', - [ALERT_RULE_NAME]: 'rule-name', - [ALERT_RULE_PARAMETERS]: { bar: true }, - [ALERT_RULE_PRODUCER]: 'alerts', - [ALERT_RULE_REVISION]: 0, - [ALERT_RULE_TYPE_ID]: 'test.rule-type', - [ALERT_RULE_TAGS]: ['rule-', '-tags'], - [ALERT_RULE_UUID]: '1', - [ALERT_UUID]: 'abc', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_STATUS]: 'active', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.0', - [TAGS]: ['rule-', '-tags'], }, }, ], @@ -2373,16 +2158,7 @@ describe('Alerts Client', () => { ruleLabel: `test: rule-name`, flappingSettings: DEFAULT_FLAPPING_SETTINGS, activeAlertsFromState: { - '1': { - state: { foo: true, start: '2023-03-28T11:27:28.159Z', duration: '0' }, - meta: { - flapping: false, - flappingHistory: [true], - maintenanceWindowIds: [], - lastScheduledActions: { group: 'default', date: new Date().toISOString() }, - uuid: 'abc', - }, - }, + '1': trackedAlert1Raw, }, recoveredAlertsFromState: {}, }); @@ -2415,13 +2191,13 @@ describe('Alerts Client', () => { }, }, { - [TIMESTAMP]: date, + ...getRecoveredIndexedAlertDoc({ [ALERT_UUID]: 'abc' }), count: 100, url: 'https://elastic.co', [EVENT_ACTION]: 'close', [EVENT_KIND]: 'signal', [ALERT_ACTION_GROUP]: 'recovered', - [ALERT_DURATION]: '39600000000000', + [ALERT_DURATION]: '36000000000000', [ALERT_END]: date, [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true, true], @@ -2482,60 +2258,14 @@ describe('Alerts Client', () => { timed_out: false, _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, hits: { - total: { - relation: 'eq', - value: 1, - }, + 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'], - }, + _seq_no: 42, + _primary_term: 666, + _source: fetchedAlert1, }, ], }, @@ -2548,16 +2278,7 @@ describe('Alerts Client', () => { 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().toISOString() }, - uuid: 'abc', - }, - }, + '1': trackedAlert1Raw, }, recoveredAlertsFromState: {}, }); @@ -2571,52 +2292,7 @@ describe('Alerts Client', () => { 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'], - }); + expect(recoveredAlert.hit).toEqual(fetchedAlert1); }); test('should return undefined document with recovered alert, if it does not exists', async () => { @@ -2625,10 +2301,7 @@ describe('Alerts Client', () => { timed_out: false, _shards: { failed: 0, successful: 1, total: 1, skipped: 0 }, hits: { - total: { - relation: 'eq', - value: 0, - }, + total: { relation: 'eq', value: 0 }, hits: [], }, }); @@ -2640,16 +2313,7 @@ describe('Alerts Client', () => { 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().toISOString() }, - uuid: 'abc', - }, - }, + '1': trackedAlert1Raw, }, recoveredAlertsFromState: {}, }); diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/alert_conflict_resolver.ts b/x-pack/plugins/alerting/server/alerts_client/lib/alert_conflict_resolver.ts index ed81c76b7987e..3c5ce6e25a1a8 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/alert_conflict_resolver.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/alert_conflict_resolver.ts @@ -27,7 +27,7 @@ import { zip, get } from 'lodash'; // these fields are the one's we'll refresh from the fresh mget'd docs const REFRESH_FIELDS_ALWAYS = [ALERT_WORKFLOW_STATUS, ALERT_WORKFLOW_TAGS, ALERT_CASE_IDS]; const REFRESH_FIELDS_CONDITIONAL = [ALERT_STATUS]; -const REFRESH_FIELDS_ALL = [...REFRESH_FIELDS_ALWAYS, ...REFRESH_FIELDS_CONDITIONAL]; +export const REFRESH_FIELDS_ALL = [...REFRESH_FIELDS_ALWAYS, ...REFRESH_FIELDS_CONDITIONAL]; export interface ResolveAlertConflictsParams { esClient: ElasticsearchClient; 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 88c63a2a3dd6a..4e317d5f5592f 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 @@ -6,19 +6,8 @@ */ 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'; import { - ALERT_RULE_CATEGORY, - ALERT_RULE_CONSUMER, - ALERT_RULE_EXECUTION_UUID, - ALERT_RULE_NAME, - ALERT_RULE_PARAMETERS, - ALERT_RULE_PRODUCER, - ALERT_RULE_REVISION, - ALERT_RULE_TAGS, - ALERT_RULE_TYPE_ID, - ALERT_RULE_UUID, SPACE_IDS, ALERT_ACTION_GROUP, ALERT_DURATION, @@ -37,36 +26,7 @@ import { VERSION, ALERT_TIME_RANGE, } from '@kbn/rule-data-utils'; - -const 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', -}; -const alertRule: AlertRule = { - [ALERT_RULE_CATEGORY]: rule.category, - [ALERT_RULE_CONSUMER]: rule.consumer, - [ALERT_RULE_EXECUTION_UUID]: rule.execution.uuid, - [ALERT_RULE_NAME]: rule.name, - [ALERT_RULE_PARAMETERS]: rule.parameters, - [ALERT_RULE_PRODUCER]: rule.producer, - [ALERT_RULE_REVISION]: rule.revision, - [ALERT_RULE_TYPE_ID]: rule.rule_type_id, - [ALERT_RULE_TAGS]: rule.tags, - [ALERT_RULE_UUID]: rule.uuid, - [SPACE_IDS]: ['default'], -}; +import { alertRule } from './test_fixtures'; describe('buildNewAlert', () => { test('should build alert document with info from legacy alert', () => { 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 201153d41a096..7ccef435a5a49 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 @@ -6,18 +6,9 @@ */ import { Alert as LegacyAlert } from '../../alert/alert'; import { buildOngoingAlert } from './build_ongoing_alert'; -import type { AlertRule } from '../types'; import { - ALERT_RULE_CATEGORY, - ALERT_RULE_CONSUMER, - ALERT_RULE_EXECUTION_UUID, ALERT_RULE_NAME, ALERT_RULE_PARAMETERS, - ALERT_RULE_PRODUCER, - ALERT_RULE_REVISION, - ALERT_RULE_TAGS, - ALERT_RULE_TYPE_ID, - ALERT_RULE_UUID, SPACE_IDS, ALERT_ACTION_GROUP, ALERT_DURATION, @@ -36,392 +27,662 @@ import { VERSION, ALERT_TIME_RANGE, } from '@kbn/rule-data-utils'; +import { alertRule, existingFlattenedNewAlert, existingExpandedNewAlert } from './test_fixtures'; -const 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', -}; -const alertRule: AlertRule = { - [ALERT_RULE_CATEGORY]: rule.category, - [ALERT_RULE_CONSUMER]: rule.consumer, - [ALERT_RULE_EXECUTION_UUID]: rule.execution.uuid, - [ALERT_RULE_NAME]: rule.name, - [ALERT_RULE_PARAMETERS]: rule.parameters, - [ALERT_RULE_PRODUCER]: rule.producer, - [ALERT_RULE_REVISION]: rule.revision, - [ALERT_RULE_TYPE_ID]: rule.rule_type_id, - [ALERT_RULE_TAGS]: rule.tags, - [ALERT_RULE_UUID]: rule.uuid, - [SPACE_IDS]: ['default'], -}; +for (const flattened of [true, false]) { + const existingAlert = flattened ? existingFlattenedNewAlert : existingExpandedNewAlert; -const existingAlert = { - ...alertRule, - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'error', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_STATUS]: 'active', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.1', - [TAGS]: ['rule-', '-tags'], -}; + describe(`buildOngoingAlert for ${flattened ? 'flattened' : 'expanded'} existing alert`, () => { + test('should return alert document with updated info from legacy alert', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert + .scheduleActions('warning') + .replaceState({ start: '2023-03-28T12:27:28.159Z', duration: '36000000' }); -describe('buildOngoingAlert', () => { - test('should update alert document with updated info from legacy alert', () => { - const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A'); - legacyAlert - .scheduleActions('warning') - .replaceState({ start: '0000-00-00T00:00:00.000Z', duration: '36000000' }); + expect( + buildOngoingAlert<{}, {}, {}, 'error' | 'warning', 'recovered'>({ + // @ts-expect-error + alert: existingAlert, + legacyAlert, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...alertRule, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'warning', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_DURATION]: '36000000', + [ALERT_STATUS]: 'active', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + start: '2023-03-28T12:27:28.159Z', + uuid: 'abcdefg', + }, + }, + }), + }); + }); + + test('should return alert document with updated rule data if rule definition has changed', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert + .scheduleActions('warning') + .replaceState({ start: '2023-03-28T12:27:28.159Z', duration: '36000000' }); - expect( - buildOngoingAlert<{}, {}, {}, 'error' | 'warning', 'recovered'>({ - alert: existingAlert, - legacyAlert, - rule: alertRule, - timestamp: '2023-03-29T12:27:28.159Z', - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...alertRule, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'warning', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_STATUS]: 'active', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + const updatedRule = { + ...alertRule, + [ALERT_RULE_NAME]: 'updated-rule-name', + [ALERT_RULE_PARAMETERS]: { bar: false }, + }; + expect( + buildOngoingAlert<{}, {}, {}, 'error' | 'warning', 'recovered'>({ + // @ts-expect-error + alert: existingAlert, + legacyAlert, + rule: updatedRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...updatedRule, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'warning', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'active', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + start: '2023-03-28T12:27:28.159Z', + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - test('should update alert document with updated rule data if rule definition has changed', () => { - const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A'); - legacyAlert - .scheduleActions('warning') - .replaceState({ start: '0000-00-00T00:00:00.000Z', duration: '36000000' }); + test('should return alert document with updated flapping history and maintenance window ids if set', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert + .scheduleActions('error') + .replaceState({ start: '2023-03-28T12:27:28.159Z', duration: '36000000' }); + legacyAlert.setFlappingHistory([false, false, true, true]); + legacyAlert.setMaintenanceWindowIds(['maint-xyz']); + + const alert = flattened + ? { + ...existingAlert, + [ALERT_FLAPPING_HISTORY]: [true, false, false, false, true, true], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], + } + : { + ...existingAlert, + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + flapping_history: [true, false, false, false, true, true], + maintenance_window_ids: ['maint-1', 'maint-321'], + }, + }, + }; - const updatedRule = { - ...alertRule, - [ALERT_RULE_NAME]: 'updated-rule-name', - [ALERT_RULE_PARAMETERS]: { bar: false }, - }; - expect( - buildOngoingAlert<{}, {}, {}, 'error' | 'warning', 'recovered'>({ - alert: existingAlert, - legacyAlert, - rule: updatedRule, - timestamp: '2023-03-29T12:27:28.159Z', - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...updatedRule, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'warning', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_STATUS]: 'active', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + expect( + buildOngoingAlert<{}, {}, {}, 'error' | 'warning', 'recovered'>({ + // @ts-expect-error + alert, + legacyAlert, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...alertRule, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'error', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [false, false, true, true], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-xyz'], + [ALERT_STATUS]: 'active', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + start: '2023-03-28T12:27:28.159Z', + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - test('should update alert document with updated flapping history and maintenance window ids if set', () => { - const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('1'); - legacyAlert.scheduleActions('error'); - legacyAlert.setFlappingHistory([false, false, true, true]); - legacyAlert.setMaintenanceWindowIds(['maint-xyz']); + test('should return alert document with updated payload if specified', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert + .scheduleActions('warning') + .replaceState({ start: '2023-03-28T12:27:28.159Z', duration: '36000000' }); - expect( - buildOngoingAlert<{}, {}, {}, 'error' | 'warning', 'recovered'>({ - alert: { - ...existingAlert, - [ALERT_FLAPPING_HISTORY]: [true, false, false, false, true, true], - [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], - }, - legacyAlert, - rule: alertRule, - timestamp: '2023-03-29T12:27:28.159Z', - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...alertRule, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'error', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [false, false, true, true], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-xyz'], - [ALERT_STATUS]: 'active', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '0', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + const alert = flattened + ? { + ...existingAlert, + count: 1, + url: `https://url1`, + 'kibana.alert.nested_field': 3, + } + : { + ...existingAlert, + count: 1, + url: `https://url1`, + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + nested_field: 3, + }, + }, + }; + expect( + buildOngoingAlert< + { count: number; url: string; 'kibana.alert.nested_field'?: number }, + {}, + {}, + 'error' | 'warning', + 'recovered' + >({ + // @ts-expect-error + alert, + legacyAlert, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + payload: { + count: 2, + url: `https://url2`, + 'kibana.alert.nested_field': 2, + }, + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...alertRule, + count: 2, + url: `https://url2`, + 'kibana.alert.nested_field': 2, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'warning', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'active', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + start: '2023-03-28T12:27:28.159Z', + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - test('should update alert document with updated payload if specified', () => { - const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A'); - legacyAlert - .scheduleActions('warning') - .replaceState({ start: '0000-00-00T00:00:00.000Z', duration: '36000000' }); + test('should return alert document with updated payload if specified but not overwrite any framework fields', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert + .scheduleActions('warning') + .replaceState({ start: '2023-03-28T12:27:28.159Z', duration: '36000000' }); + + const alert = flattened + ? { + ...existingAlert, + count: 1, + url: `https://url1`, + 'kibana.alert.nested_field': 3, + } + : { + ...existingAlert, + count: 1, + url: `https://url1`, + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + nested_field: 3, + }, + }, + }; - expect( - buildOngoingAlert< - { count: number; url: string; 'kibana.alert.nested_field'?: number }, - {}, - {}, - 'error' | 'warning', - 'recovered' - >({ - alert: { - ...existingAlert, - count: 1, - url: `https://url1`, - }, - legacyAlert, - rule: alertRule, - timestamp: '2023-03-29T12:27:28.159Z', - payload: { - count: 2, - url: `https://url2`, - 'kibana.alert.nested_field': 2, - }, - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...alertRule, - count: 2, - url: `https://url2`, - 'kibana.alert.nested_field': 2, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'warning', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_STATUS]: 'active', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + expect( + buildOngoingAlert< + { + count: number; + url: string; + [ALERT_ACTION_GROUP]: string; + 'kibana.alert.nested_field'?: number; + }, + {}, + {}, + 'error' | 'warning', + 'recovered' + >({ + // @ts-expect-error + alert, + legacyAlert, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + payload: { + count: 2, + url: `https://url2`, + [ALERT_ACTION_GROUP]: 'bad action group', + 'kibana.alert.nested_field': 2, + }, + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...alertRule, + count: 2, + url: `https://url2`, + 'kibana.alert.nested_field': 2, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'warning', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'active', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + start: '2023-03-28T12:27:28.159Z', + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - test('should update alert document with updated payload is specified but not overwrite any framework fields', () => { - const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A'); - legacyAlert - .scheduleActions('warning') - .replaceState({ start: '0000-00-00T00:00:00.000Z', duration: '36000000' }); + test('should merge and de-dupe tags from existing alert, reported payload and rule tags', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert + .scheduleActions('warning') + .replaceState({ start: '2023-03-28T12:27:28.159Z', duration: '36000000' }); + + const alert = flattened + ? { + ...existingAlert, + count: 1, + url: `https://url1`, + tags: ['old-tag1', '-tags'], + 'kibana.alert.nested_field': 3, + } + : { + ...existingAlert, + count: 1, + url: `https://url1`, + tags: ['old-tag1', '-tags'], + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + nested_field: 3, + }, + }, + }; - expect( - buildOngoingAlert< - { - count: number; - url: string; - [ALERT_ACTION_GROUP]: string; - 'kibana.alert.nested_field'?: number; - }, - {}, - {}, - 'error' | 'warning', - 'recovered' - >({ - alert: { - ...existingAlert, - count: 1, - url: `https://url1`, - }, - legacyAlert, - rule: alertRule, - timestamp: '2023-03-29T12:27:28.159Z', - payload: { - count: 2, - url: `https://url2`, - [ALERT_ACTION_GROUP]: 'bad action group', - 'kibana.alert.nested_field': 2, - }, - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...alertRule, - count: 2, - url: `https://url2`, - 'kibana.alert.nested_field': 2, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'warning', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_STATUS]: 'active', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + expect( + buildOngoingAlert< + { + count: number; + url: string; + [ALERT_ACTION_GROUP]: string; + 'kibana.alert.nested_field'?: number; + tags?: string[]; + }, + {}, + {}, + 'error' | 'warning', + 'recovered' + >({ + // @ts-expect-error + alert, + legacyAlert, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + payload: { + count: 2, + url: `https://url2`, + [ALERT_ACTION_GROUP]: 'bad action group', + 'kibana.alert.nested_field': 2, + tags: ['-tags', 'custom-tag2'], + }, + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...alertRule, + count: 2, + url: `https://url2`, + 'kibana.alert.nested_field': 2, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'warning', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'active', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['-tags', 'custom-tag2', 'old-tag1', 'rule-'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + start: '2023-03-28T12:27:28.159Z', + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - 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' }); + test('should not update alert document if no payload is specified', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert + .scheduleActions('warning') + .replaceState({ start: '2023-03-28T12:27:28.159Z', duration: '36000000' }); - expect( - buildOngoingAlert< - { - count: number; - url: string; - [ALERT_ACTION_GROUP]: string; - 'kibana.alert.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`, - [ALERT_ACTION_GROUP]: 'bad action group', - 'kibana.alert.nested_field': 2, - tags: ['-tags', 'custom-tag2'], - }, - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...alertRule, - count: 2, - url: `https://url2`, - 'kibana.alert.nested_field': 2, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'warning', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_STATUS]: 'active', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['-tags', 'custom-tag2', 'old-tag1', 'rule-'], + const alert = flattened + ? { + ...existingAlert, + count: 1, + url: `https://url1`, + 'kibana.alert.nested_field': 3, + } + : { + ...existingAlert, + count: 1, + url: `https://url1`, + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + nested_field: 3, + }, + }, + }; + + expect( + buildOngoingAlert<{ count: number; url: string }, {}, {}, 'error' | 'warning', 'recovered'>( + { + // @ts-expect-error + alert, + legacyAlert, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + } + ) + ).toEqual({ + ...alertRule, + count: 1, + url: `https://url1`, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'warning', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'active', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + 'kibana.alert.nested_field': 3, + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + nested_field: 3, + instance: { id: 'alert-A' }, + start: '2023-03-28T12:27:28.159Z', + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - test('should not update alert document if no payload is specified', () => { - const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A'); - legacyAlert - .scheduleActions('warning') - .replaceState({ start: '0000-00-00T00:00:00.000Z', duration: '36000000' }); + test('should use workflow_status from payload if specified', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert + .scheduleActions('warning') + .replaceState({ start: '2023-03-28T12:27:28.159Z', duration: '36000000' }); + + const alert = flattened + ? { + ...existingAlert, + count: 1, + url: `https://url1`, + 'kibana.alert.deeply.nested_field': 3, + } + : { + ...existingAlert, + count: 1, + url: `https://url1`, + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + deeply: { nested_field: 3 }, + }, + }, + }; - expect( - buildOngoingAlert<{ count: number; url: string }, {}, {}, 'error' | 'warning', 'recovered'>({ - alert: { - ...existingAlert, - count: 1, - url: `https://url1`, - }, - legacyAlert, - rule: alertRule, - timestamp: '2023-03-29T12:27:28.159Z', - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...alertRule, - count: 1, - url: `https://url1`, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'warning', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_STATUS]: 'active', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + expect( + buildOngoingAlert< + { + count: number; + url: string; + [ALERT_WORKFLOW_STATUS]: string; + 'kibana.alert.deeply.nested_field'?: number; + }, + {}, + {}, + 'error' | 'warning', + 'recovered' + >({ + // @ts-expect-error + alert, + legacyAlert, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + payload: { + count: 2, + url: `https://url2`, + [ALERT_WORKFLOW_STATUS]: 'custom_status', + 'kibana.alert.deeply.nested_field': 2, + }, + }) + ).toEqual({ + ...alertRule, + count: 2, + url: `https://url2`, + 'kibana.alert.deeply.nested_field': 2, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'warning', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'active', + [ALERT_WORKFLOW_STATUS]: 'custom_status', + [ALERT_DURATION]: '36000000', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + start: '2023-03-28T12:27:28.159Z', + uuid: 'abcdefg', + }, + }, + }), + }); }); }); -}); +} 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 ed0e193aed508..f22c14e62b464 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 @@ -14,6 +14,7 @@ import { ALERT_FLAPPING_HISTORY, ALERT_MAINTENANCE_WINDOW_IDS, ALERT_RULE_TAGS, + ALERT_TIME_RANGE, EVENT_ACTION, SPACE_IDS, TAGS, @@ -25,6 +26,7 @@ import { Alert as LegacyAlert } from '../../alert/alert'; import { AlertInstanceContext, AlertInstanceState, RuleAlertData } from '../../types'; import type { AlertRule } from '../types'; import { stripFrameworkFields } from './strip_framework_fields'; +import { removeUnflattenedFieldsFromAlert, replaceRefreshableAlertFields } from './format_alert'; interface BuildOngoingAlertOpts< AlertData extends RuleAlertData, @@ -67,52 +69,71 @@ export const buildOngoingAlert = < RecoveryActionGroupId >): Alert & AlertData => { const cleanedPayload = stripFrameworkFields(payload); - return deepmerge.all( - [ - alert, - cleanedPayload, - // Set the latest rule configuration - rule, - { - // Update the timestamp to reflect latest update time - [TIMESTAMP]: timestamp, - [EVENT_ACTION]: 'active', - // Because we're building this alert after the action execution handler has been - // run, the scheduledExecutionOptions for the alert has been cleared and - // the lastScheduledActions has been set. If we ever change the order of operations - // to build and persist the alert before action execution handler, we will need to - // update where we pull the action group from. - // Set latest action group as this may have changed during execution (ex: error -> warning) - [ALERT_ACTION_GROUP]: legacyAlert.getScheduledActionOptions()?.actionGroup, - // Set latest flapping state - [ALERT_FLAPPING]: legacyAlert.getFlapping(), - // Set latest flapping_history - [ALERT_FLAPPING_HISTORY]: legacyAlert.getFlappingHistory(), - // Set latest maintenance window IDs - [ALERT_MAINTENANCE_WINDOW_IDS]: legacyAlert.getMaintenanceWindowIds(), - // Set latest duration as ongoing alerts should have updated duration - ...(legacyAlert.getState().duration - ? { [ALERT_DURATION]: 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[SPACE_IDS], - [VERSION]: kibanaVersion, - [TAGS]: Array.from( - new Set([ - ...((cleanedPayload?.tags as string[]) ?? []), - ...(alert.tags ?? []), - ...(rule[ALERT_RULE_TAGS] ?? []), - ]) - ), - }, - ], - { arrayMerge: (_, sourceArray) => sourceArray } - ) as Alert & AlertData; + + // Make sure that any alert fields that are updateable are flattened. + const refreshableAlertFields = replaceRefreshableAlertFields(alert); + + const alertUpdates = { + // Set latest rule configuration + ...rule, + // Update the timestamp to reflect latest update time + [TIMESTAMP]: timestamp, + [EVENT_ACTION]: 'active', + // Because we're building this alert after the action execution handler has been + // run, the scheduledExecutionOptions for the alert has been cleared and + // the lastScheduledActions has been set. If we ever change the order of operations + // to build and persist the alert before action execution handler, we will need to + // update where we pull the action group from. + // Set latest action group as this may have changed during execution (ex: error -> warning) + [ALERT_ACTION_GROUP]: legacyAlert.getScheduledActionOptions()?.actionGroup, + // Set latest flapping state + [ALERT_FLAPPING]: legacyAlert.getFlapping(), + // Set latest flapping_history + [ALERT_FLAPPING_HISTORY]: legacyAlert.getFlappingHistory(), + // Set latest maintenance window IDs + [ALERT_MAINTENANCE_WINDOW_IDS]: legacyAlert.getMaintenanceWindowIds(), + // Set the time range + ...(legacyAlert.getState().start + ? { + [ALERT_TIME_RANGE]: { gte: legacyAlert.getState().start }, + } + : {}), + // Set latest duration as ongoing alerts should have updated duration + ...(legacyAlert.getState().duration + ? { [ALERT_DURATION]: legacyAlert.getState().duration } + : {}), + [SPACE_IDS]: rule[SPACE_IDS], + [VERSION]: kibanaVersion, + [TAGS]: Array.from( + new Set([ + ...((cleanedPayload?.tags as string[]) ?? []), + ...(alert.tags ?? []), + ...(rule[ALERT_RULE_TAGS] ?? []), + ]) + ), + }; + + // Clean the existing alert document so any nested fields that will be updated + // are removed, to avoid duplicate data. + // e.g. if the existing alert document has the field: + // { + // kibana: { + // alert: { + // field1: 'value1' + // } + // } + // } + // and the updated alert has the field + // { + // 'kibana.alert.field1': 'value2' + // } + // the expanded field from the existing alert is removed + const cleanedAlert = removeUnflattenedFieldsFromAlert(alert, { + ...cleanedPayload, + ...alertUpdates, + ...refreshableAlertFields, + }); + return deepmerge.all([cleanedAlert, refreshableAlertFields, cleanedPayload, alertUpdates], { + arrayMerge: (_, sourceArray) => sourceArray, + }) as Alert & AlertData; }; 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 ec499cb921deb..73ff3e0a0f6e4 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 @@ -6,18 +6,9 @@ */ import { Alert as LegacyAlert } from '../../alert/alert'; import { buildRecoveredAlert } from './build_recovered_alert'; -import type { AlertRule } from '../types'; import { - ALERT_RULE_CATEGORY, - ALERT_RULE_CONSUMER, - ALERT_RULE_EXECUTION_UUID, ALERT_RULE_NAME, ALERT_RULE_PARAMETERS, - ALERT_RULE_PRODUCER, - ALERT_RULE_REVISION, - ALERT_RULE_TAGS, - ALERT_RULE_TYPE_ID, - ALERT_RULE_UUID, SPACE_IDS, ALERT_ACTION_GROUP, ALERT_DURATION, @@ -37,341 +28,530 @@ import { ALERT_TIME_RANGE, ALERT_END, } from '@kbn/rule-data-utils'; +import { + alertRule, + existingFlattenedActiveAlert, + existingExpandedActiveAlert, +} from './test_fixtures'; + +for (const flattened of [true, false]) { + const existingAlert = flattened ? existingFlattenedActiveAlert : existingExpandedActiveAlert; -const 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', -}; -const alertRule: AlertRule = { - [ALERT_RULE_CATEGORY]: rule.category, - [ALERT_RULE_CONSUMER]: rule.consumer, - [ALERT_RULE_EXECUTION_UUID]: rule.execution.uuid, - [ALERT_RULE_NAME]: rule.name, - [ALERT_RULE_PARAMETERS]: rule.parameters, - [ALERT_RULE_PRODUCER]: rule.producer, - [ALERT_RULE_REVISION]: rule.revision, - [ALERT_RULE_TYPE_ID]: rule.rule_type_id, - [ALERT_RULE_TAGS]: rule.tags, - [ALERT_RULE_UUID]: rule.uuid, - [SPACE_IDS]: ['default'], -}; + describe(`buildRecoveredAlert for ${flattened ? 'flattened' : 'expanded'} existing alert`, () => { + test('should return alert document with recovered status and info from legacy alert', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert.scheduleActions('default').replaceState({ + start: '2023-03-28T12:27:28.159Z', + end: '2023-03-30T12:27:28.159Z', + duration: '36000000', + }); + + expect( + buildRecoveredAlert<{}, {}, {}, 'default', 'recovered'>({ + // @ts-expect-error + alert: existingAlert, + legacyAlert, + rule: alertRule, + recoveryActionGroup: 'recovered', + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...alertRule, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'close', + [ALERT_ACTION_GROUP]: 'recovered', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'recovered', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_END]: '2023-03-30T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + uuid: 'abcdefg', + }, + }, + }), + }); + }); -const existingActiveAlert = { - ...alertRule, - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'default', - [ALERT_DURATION]: '0', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true, false], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-x'], - [ALERT_STATUS]: 'active', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.1', - [TAGS]: ['rule-', '-tags'], -}; + test('should return alert document with recovery status and updated rule data if rule definition has changed', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert.scheduleActions('default').replaceState({ + start: '2023-03-28T12:27:28.159Z', + end: '2023-03-30T12:27:28.159Z', + duration: '36000000', + }); + legacyAlert.setMaintenanceWindowIds(['maint-1', 'maint-321']); -describe('buildRecoveredAlert', () => { - test('should update active alert document with recovered status and info from legacy alert', () => { - const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A'); - legacyAlert - .scheduleActions('default') - .replaceState({ end: '2023-03-30T12:27:28.159Z', duration: '36000000' }); + const updatedRule = { + ...alertRule, + [ALERT_RULE_NAME]: 'updated-rule-name', + [ALERT_RULE_PARAMETERS]: { bar: false }, + }; - expect( - buildRecoveredAlert<{}, {}, {}, 'default', 'recovered'>({ - alert: existingActiveAlert, - legacyAlert, - rule: alertRule, - recoveryActionGroup: 'recovered', - timestamp: '2023-03-29T12:27:28.159Z', - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...alertRule, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'recovered', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], - [ALERT_STATUS]: 'recovered', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_END]: '2023-03-30T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + expect( + buildRecoveredAlert<{}, {}, {}, 'default', 'recovered'>({ + // @ts-expect-error + alert: existingAlert, + legacyAlert, + recoveryActionGroup: 'NoLongerActive', + rule: updatedRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...updatedRule, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'close', + [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], + [ALERT_STATUS]: 'recovered', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_END]: '2023-03-30T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - test('should update active alert document with recovery status and updated rule data if rule definition has changed', () => { - 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']); + test('should return alert document with updated payload if specified', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert.scheduleActions('default').replaceState({ + start: '2023-03-28T12:27:28.159Z', + end: '2023-03-30T12:27:28.159Z', + duration: '36000000', + }); + legacyAlert.setMaintenanceWindowIds(['maint-1', 'maint-321']); - const updatedRule = { - ...alertRule, - [ALERT_RULE_NAME]: 'updated-rule-name', - [ALERT_RULE_PARAMETERS]: { bar: false }, - }; + const alert = flattened + ? { + ...existingAlert, + count: 1, + url: `https://url1`, + 'kibana.alert.nested_field': 3, + } + : { + ...existingAlert, + count: 1, + url: `https://url1`, + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + nested_field: 3, + }, + }, + }; - expect( - buildRecoveredAlert<{}, {}, {}, 'default', 'recovered'>({ - alert: existingActiveAlert, - legacyAlert, - recoveryActionGroup: 'NoLongerActive', - rule: updatedRule, - timestamp: '2023-03-29T12:27:28.159Z', - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...updatedRule, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'NoLongerActive', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], - [ALERT_STATUS]: 'recovered', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_END]: '2023-03-30T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + expect( + buildRecoveredAlert< + { count: number; url: string; 'kibana.alert.nested_field'?: number }, + {}, + {}, + 'default', + 'recovered' + >({ + // @ts-expect-error + alert, + legacyAlert, + recoveryActionGroup: 'NoLongerActive', + payload: { + count: 2, + url: `https://url2`, + 'kibana.alert.nested_field': 2, + }, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...alertRule, + count: 2, + url: `https://url2`, + 'kibana.alert.nested_field': 2, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'close', + [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], + [ALERT_STATUS]: 'recovered', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_END]: '2023-03-30T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - test('should update active alert document with updated payload if specified', () => { - 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']); + test('should merge and de-dupe tags from existing flattened alert, reported recovery payload and rule tags', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert.scheduleActions('default').replaceState({ + start: '2023-03-28T12:27:28.159Z', + end: '2023-03-30T12:27:28.159Z', + duration: '36000000', + }); + legacyAlert.setMaintenanceWindowIds(['maint-1', 'maint-321']); - const updatedRule = { - ...alertRule, - [ALERT_RULE_NAME]: 'updated-rule-name', - [ALERT_RULE_PARAMETERS]: { bar: false }, - }; + const alert = flattened + ? { + ...existingAlert, + count: 1, + url: `https://url1`, + tags: ['active-alert-tag', 'rule-'], + 'kibana.alert.nested_field': 3, + } + : { + ...existingAlert, + count: 1, + url: `https://url1`, + tags: ['active-alert-tag', 'rule-'], + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + nested_field: 3, + }, + }, + }; - expect( - buildRecoveredAlert< - { count: number; url: string; 'kibana.alert.nested_field'?: number }, - {}, - {}, - 'default', - 'recovered' - >({ - alert: { - ...existingActiveAlert, - count: 1, - url: `https://url1`, - }, - legacyAlert, - recoveryActionGroup: 'NoLongerActive', - payload: { - count: 2, - url: `https://url2`, - 'kibana.alert.nested_field': 2, - }, - rule: updatedRule, - timestamp: '2023-03-29T12:27:28.159Z', - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...updatedRule, - count: 2, - url: `https://url2`, - 'kibana.alert.nested_field': 2, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'NoLongerActive', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], - [ALERT_STATUS]: 'recovered', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_END]: '2023-03-30T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + expect( + buildRecoveredAlert< + { + count: number; + url: string; + 'kibana.alert.nested_field'?: number; + tags?: string[]; + }, + {}, + {}, + 'default', + 'recovered' + >({ + // @ts-expect-error + alert, + legacyAlert, + recoveryActionGroup: 'NoLongerActive', + payload: { + count: 2, + url: `https://url2`, + 'kibana.alert.nested_field': 2, + tags: ['-tags', 'reported-recovery-tag'], + }, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...alertRule, + count: 2, + url: `https://url2`, + 'kibana.alert.nested_field': 2, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'close', + [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], + [ALERT_STATUS]: 'recovered', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_END]: '2023-03-30T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['-tags', 'reported-recovery-tag', 'active-alert-tag', 'rule-'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - 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']); + test('should update flattened active alert document with updated payload if specified but not overwrite any framework fields', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert.scheduleActions('default').replaceState({ + start: '2023-03-28T12:27:28.159Z', + end: '2023-03-30T12:27:28.159Z', + duration: '36000000', + }); + legacyAlert.setMaintenanceWindowIds(['maint-1', 'maint-321']); - const updatedRule = { - ...alertRule, - [ALERT_RULE_NAME]: 'updated-rule-name', - [ALERT_RULE_PARAMETERS]: { bar: false }, - }; + const alert = flattened + ? { + ...existingAlert, + count: 1, + url: `https://url1`, + 'kibana.alert.nested_field': 3, + } + : { + ...existingAlert, + count: 1, + url: `https://url1`, + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + nested_field: 3, + }, + }, + }; - 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: updatedRule, - timestamp: '2023-03-29T12:27:28.159Z', - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...updatedRule, - count: 2, - url: `https://url2`, - 'kibana.alert.nested_field': 2, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'NoLongerActive', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], - [ALERT_STATUS]: 'recovered', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_END]: '2023-03-30T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['-tags', 'reported-recovery-tag', 'active-alert-tag', 'rule-'], + expect( + buildRecoveredAlert< + { + count: number; + url: string; + [ALERT_ACTION_GROUP]: string; + 'kibana.alert.nested_field'?: number; + }, + {}, + {}, + 'default', + 'recovered' + >({ + // @ts-expect-error + alert, + legacyAlert, + recoveryActionGroup: 'NoLongerActive', + payload: { + count: 2, + url: `https://url2`, + [ALERT_ACTION_GROUP]: 'bad action group', + 'kibana.alert.nested_field': 2, + }, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + }) + ).toEqual({ + ...alertRule, + count: 2, + url: `https://url2`, + 'kibana.alert.nested_field': 2, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'close', + [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], + [ALERT_STATUS]: 'recovered', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_END]: '2023-03-30T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + uuid: 'abcdefg', + }, + }, + }), + }); }); - }); - test('should update active alert document with updated payload if specified but not overwrite any framework fields', () => { - 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']); + test('should use workflow_status from payload if specified', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'error' | 'warning'>('alert-A', { + meta: { uuid: 'abcdefg' }, + }); + legacyAlert.scheduleActions('warning').replaceState({ + start: '2023-03-28T12:27:28.159Z', + end: '2023-03-30T12:27:28.159Z', + duration: '36000000', + }); - const updatedRule = { - ...alertRule, - [ALERT_RULE_NAME]: 'updated-rule-name', - [ALERT_RULE_PARAMETERS]: { bar: false }, - }; + const alert = flattened + ? { + ...existingAlert, + count: 1, + url: `https://url1`, + 'kibana.alert.deeply.nested_field': 3, + } + : { + ...existingAlert, + count: 1, + url: `https://url1`, + kibana: { + // @ts-expect-error + ...existingAlert.kibana, + alert: { + // @ts-expect-error + ...existingAlert.kibana.alert, + deeply: { nested_field: 3 }, + }, + }, + }; - expect( - buildRecoveredAlert< - { - count: number; - url: string; - [ALERT_ACTION_GROUP]: string; - 'kibana.alert.nested_field'?: number; - }, - {}, - {}, - 'default', - 'recovered' - >({ - alert: { - ...existingActiveAlert, - count: 1, - url: `https://url1`, - }, - legacyAlert, - recoveryActionGroup: 'NoLongerActive', - payload: { - count: 2, - url: `https://url2`, - [ALERT_ACTION_GROUP]: 'bad action group', - 'kibana.alert.nested_field': 2, - }, - rule: updatedRule, - timestamp: '2023-03-29T12:27:28.159Z', - kibanaVersion: '8.9.0', - }) - ).toEqual({ - ...updatedRule, - count: 2, - url: `https://url2`, - 'kibana.alert.nested_field': 2, - [TIMESTAMP]: '2023-03-29T12:27:28.159Z', - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'NoLongerActive', - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-1', 'maint-321'], - [ALERT_STATUS]: 'recovered', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_END]: '2023-03-30T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, - [SPACE_IDS]: ['default'], - [VERSION]: '8.9.0', - [TAGS]: ['rule-', '-tags'], + expect( + buildRecoveredAlert< + { + count: number; + url: string; + [ALERT_WORKFLOW_STATUS]: string; + 'kibana.alert.deeply.nested_field'?: number; + }, + {}, + {}, + 'error' | 'warning', + 'recovered' + >({ + // @ts-expect-error + alert, + legacyAlert, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + kibanaVersion: '8.9.0', + recoveryActionGroup: 'NoLongerActive', + payload: { + count: 2, + url: `https://url2`, + [ALERT_WORKFLOW_STATUS]: 'custom_status', + 'kibana.alert.deeply.nested_field': 2, + }, + }) + ).toEqual({ + ...alertRule, + count: 2, + url: `https://url2`, + 'kibana.alert.deeply.nested_field': 2, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'close', + [ALERT_ACTION_GROUP]: 'NoLongerActive', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'recovered', + [ALERT_WORKFLOW_STATUS]: 'custom_status', + [ALERT_DURATION]: '36000000', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_END]: '2023-03-30T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + ...(flattened + ? { + [EVENT_KIND]: 'signal', + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_UUID]: 'abcdefg', + } + : { + event: { + kind: 'signal', + }, + kibana: { + alert: { + instance: { id: 'alert-A' }, + uuid: 'abcdefg', + }, + }, + }), + }); }); }); -}); +} 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 d05f2e2ae1cec..be6d7ff033dd1 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 @@ -21,12 +21,14 @@ import { VERSION, ALERT_END, ALERT_TIME_RANGE, + ALERT_START, } 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 } from '../types'; import { stripFrameworkFields } from './strip_framework_fields'; +import { removeUnflattenedFieldsFromAlert, replaceRefreshableAlertFields } from './format_alert'; interface BuildRecoveredAlertOpts< AlertData extends RuleAlertData, @@ -71,56 +73,76 @@ export const buildRecoveredAlert = < RecoveryActionGroupId >): Alert & AlertData => { const cleanedPayload = stripFrameworkFields(payload); - return deepmerge.all( - [ - alert, - cleanedPayload, - // Set latest rule configuration - rule, - { - // Update the timestamp to reflect latest update time - [TIMESTAMP]: timestamp, - [EVENT_ACTION]: 'close', - // Set the recovery action group - [ALERT_ACTION_GROUP]: recoveryActionGroup, - // Set latest flapping state - [ALERT_FLAPPING]: legacyAlert.getFlapping(), - // Set latest flapping_history - [ALERT_FLAPPING_HISTORY]: legacyAlert.getFlappingHistory(), - // Set latest maintenance window IDs - [ALERT_MAINTENANCE_WINDOW_IDS]: legacyAlert.getMaintenanceWindowIds(), - // Set status to 'recovered' - [ALERT_STATUS]: 'recovered', - // Set latest duration as recovered alerts should have updated duration - ...(legacyAlert.getState().duration - ? { [ALERT_DURATION]: legacyAlert.getState().duration } - : {}), - // Set end time - ...(legacyAlert.getState().end - ? { - [ALERT_END]: legacyAlert.getState().end, - // this should get merged with a ALERT_TIME_RANGE_GTE - [ALERT_TIME_RANGE]: { 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[SPACE_IDS], - // Set latest kibana version - [VERSION]: kibanaVersion, - [TAGS]: Array.from( - new Set([ - ...((cleanedPayload?.tags as string[]) ?? []), - ...(alert.tags ?? []), - ...(rule[ALERT_RULE_TAGS] ?? []), - ]) - ), - }, - ], - { arrayMerge: (_, sourceArray) => sourceArray } - ) as Alert & AlertData; + + // Make sure that any alert fields that are updateable are flattened. + const refreshableAlertFields = replaceRefreshableAlertFields(alert); + + const alertUpdates = { + // Set latest rule configuration + ...rule, + // Update the timestamp to reflect latest update time + [TIMESTAMP]: timestamp, + [EVENT_ACTION]: 'close', + // Set the recovery action group + [ALERT_ACTION_GROUP]: recoveryActionGroup, + // Set latest flapping state + [ALERT_FLAPPING]: legacyAlert.getFlapping(), + // Set latest flapping_history + [ALERT_FLAPPING_HISTORY]: legacyAlert.getFlappingHistory(), + // Set latest maintenance window IDs + [ALERT_MAINTENANCE_WINDOW_IDS]: legacyAlert.getMaintenanceWindowIds(), + // Set status to 'recovered' + [ALERT_STATUS]: 'recovered', + // Set latest duration as recovered alerts should have updated duration + ...(legacyAlert.getState().duration + ? { [ALERT_DURATION]: legacyAlert.getState().duration } + : {}), + // Set end time + ...(legacyAlert.getState().end && legacyAlert.getState().start + ? { + [ALERT_START]: legacyAlert.getState().start, + [ALERT_END]: legacyAlert.getState().end, + [ALERT_TIME_RANGE]: { + gte: legacyAlert.getState().start, + lte: legacyAlert.getState().end, + }, + } + : {}), + + [SPACE_IDS]: rule[SPACE_IDS], + // Set latest kibana version + [VERSION]: kibanaVersion, + [TAGS]: Array.from( + new Set([ + ...((cleanedPayload?.tags as string[]) ?? []), + ...(alert.tags ?? []), + ...(rule[ALERT_RULE_TAGS] ?? []), + ]) + ), + }; + + // Clean the existing alert document so any nested fields that will be updated + // are removed, to avoid duplicate data. + // e.g. if the existing alert document has the field: + // { + // kibana: { + // alert: { + // field1: 'value1' + // } + // } + // } + // and the updated alert has the field + // { + // 'kibana.alert.field1': 'value2' + // } + // the expanded field from the existing alert is removed + const cleanedAlert = removeUnflattenedFieldsFromAlert(alert, { + ...cleanedPayload, + ...alertUpdates, + ...refreshableAlertFields, + }); + + return deepmerge.all([cleanedAlert, refreshableAlertFields, cleanedPayload, alertUpdates], { + arrayMerge: (_, sourceArray) => sourceArray, + }) as Alert & AlertData; }; diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/build_updated_recovered_alert.test.ts b/x-pack/plugins/alerting/server/alerts_client/lib/build_updated_recovered_alert.test.ts index 458a9f6c7a7b3..1c646f08d7828 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/build_updated_recovered_alert.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/build_updated_recovered_alert.test.ts @@ -5,19 +5,8 @@ * 2.0. */ import { Alert as LegacyAlert } from '../../alert/alert'; -import { AlertRule } from '../types'; import { buildUpdatedRecoveredAlert } from './build_updated_recovered_alert'; import { - ALERT_RULE_CATEGORY, - ALERT_RULE_CONSUMER, - ALERT_RULE_EXECUTION_UUID, - ALERT_RULE_NAME, - ALERT_RULE_PARAMETERS, - ALERT_RULE_PRODUCER, - ALERT_RULE_REVISION, - ALERT_RULE_TAGS, - ALERT_RULE_TYPE_ID, - ALERT_RULE_UUID, SPACE_IDS, ALERT_ACTION_GROUP, ALERT_DURATION, @@ -37,63 +26,14 @@ import { ALERT_TIME_RANGE, ALERT_END, } from '@kbn/rule-data-utils'; - -const 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', -}; - -const alertRule: AlertRule = { - [ALERT_RULE_CATEGORY]: rule.category, - [ALERT_RULE_CONSUMER]: rule.consumer, - [ALERT_RULE_EXECUTION_UUID]: rule.execution.uuid, - [ALERT_RULE_NAME]: rule.name, - [ALERT_RULE_PARAMETERS]: rule.parameters, - [ALERT_RULE_PRODUCER]: rule.producer, - [ALERT_RULE_REVISION]: rule.revision, - [ALERT_RULE_TYPE_ID]: rule.rule_type_id, - [ALERT_RULE_TAGS]: rule.tags, - [ALERT_RULE_UUID]: rule.uuid, - [SPACE_IDS]: ['default'], -}; - -const existingRecoveredAlert = { - ...alertRule, - [TIMESTAMP]: '2023-03-28T12:27:28.159Z', - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_ACTION_GROUP]: 'recovered', - [ALERT_DURATION]: '36000000', - [ALERT_START]: '2023-03-27T12:27:28.159Z', - [ALERT_END]: '2023-03-30T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, - [ALERT_FLAPPING]: false, - [ALERT_FLAPPING_HISTORY]: [true, false, false], - [ALERT_INSTANCE_ID]: 'alert-A', - [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-x'], - [ALERT_STATUS]: 'recovered', - [ALERT_START]: '2023-03-28T12:27:28.159Z', - [ALERT_UUID]: 'abcdefg', - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: '8.8.1', - [TAGS]: ['rule-', '-tags'], -}; +import { + alertRule, + existingFlattenedRecoveredAlert, + existingExpandedRecoveredAlert, +} from './test_fixtures'; describe('buildUpdatedRecoveredAlert', () => { - test('should update already recovered alert document with updated flapping values and timestamp only', () => { + test('should update already recovered flattened alert document with updated flapping values and timestamp only', () => { const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A'); legacyAlert.scheduleActions('default'); legacyAlert.setFlappingHistory([false, false, true, true]); @@ -101,7 +41,7 @@ describe('buildUpdatedRecoveredAlert', () => { expect( buildUpdatedRecoveredAlert<{}>({ - alert: existingRecoveredAlert, + alert: existingFlattenedRecoveredAlert, legacyRawAlert: { meta: { flapping: true, @@ -124,7 +64,7 @@ describe('buildUpdatedRecoveredAlert', () => { [ALERT_DURATION]: '36000000', [ALERT_START]: '2023-03-27T12:27:28.159Z', [ALERT_END]: '2023-03-30T12:27:28.159Z', - [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, + [ALERT_TIME_RANGE]: { gte: '2023-03-27T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, [ALERT_FLAPPING]: true, [ALERT_FLAPPING_HISTORY]: [false, false, true, true], [ALERT_INSTANCE_ID]: 'alert-A', @@ -138,4 +78,63 @@ describe('buildUpdatedRecoveredAlert', () => { [TAGS]: ['rule-', '-tags'], }); }); + + test('should update already recovered expanded alert document with updated flapping values and timestamp only', () => { + const legacyAlert = new LegacyAlert<{}, {}, 'default'>('alert-A'); + legacyAlert.scheduleActions('default'); + legacyAlert.setFlappingHistory([false, false, true, true]); + legacyAlert.setMaintenanceWindowIds(['maint-1', 'maint-321']); + + expect( + buildUpdatedRecoveredAlert<{}>({ + // @ts-expect-error + alert: existingExpandedRecoveredAlert, + legacyRawAlert: { + meta: { + flapping: true, + flappingHistory: [false, false, true, true], + maintenanceWindowIds: ['maint-1', 'maint-321'], + }, + state: { + start: '3023-03-27T12:27:28.159Z', + }, + }, + rule: alertRule, + timestamp: '2023-03-29T12:27:28.159Z', + }) + ).toEqual({ + ...alertRule, + event: { + action: 'close', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'recovered', + duration: { + us: '36000000', + }, + end: '2023-03-30T12:27:28.159Z', + instance: { + id: 'alert-A', + }, + maintenance_window_ids: ['maint-x'], + start: '2023-03-28T12:27:28.159Z', + time_range: { + gte: '2023-03-27T12:27:28.159Z', + lte: '2023-03-30T12:27:28.159Z', + }, + uuid: 'abcdefg', + }, + version: '8.8.1', + }, + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [ALERT_FLAPPING]: true, + [ALERT_FLAPPING_HISTORY]: [false, false, true, true], + [ALERT_STATUS]: 'recovered', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['default'], + [TAGS]: ['rule-', '-tags'], + }); + }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/build_updated_recovered_alert.ts b/x-pack/plugins/alerting/server/alerts_client/lib/build_updated_recovered_alert.ts index e8910d75040d3..59c9a5cb35874 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/build_updated_recovered_alert.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/build_updated_recovered_alert.ts @@ -11,6 +11,7 @@ import { ALERT_FLAPPING, ALERT_FLAPPING_HISTORY, TIMESTAMP } from '@kbn/rule-dat import { RawAlertInstance } from '@kbn/alerting-state-types'; import { RuleAlertData } from '../../types'; import { AlertRule } from '../types'; +import { removeUnflattenedFieldsFromAlert, replaceRefreshableAlertFields } from './format_alert'; interface BuildUpdatedRecoveredAlertOpts { alert: Alert & AlertData; @@ -30,20 +31,41 @@ export const buildUpdatedRecoveredAlert = ({ rule, timestamp, }: BuildUpdatedRecoveredAlertOpts): Alert & AlertData => { - return deepmerge.all( - [ - alert, - // Set latest rule configuration - rule, - { - // Update the timestamp to reflect latest update time - [TIMESTAMP]: timestamp, - // Set latest flapping state - [ALERT_FLAPPING]: legacyRawAlert.meta?.flapping, - // Set latest flapping history - [ALERT_FLAPPING_HISTORY]: legacyRawAlert.meta?.flappingHistory, - }, - ], - { arrayMerge: (_, sourceArray) => sourceArray } - ) as Alert & AlertData; + // Make sure that any alert fields that are updateable are flattened. + const refreshableAlertFields = replaceRefreshableAlertFields(alert); + + const alertUpdates = { + // Set latest rule configuration + ...rule, + // Update the timestamp to reflect latest update time + [TIMESTAMP]: timestamp, + // Set latest flapping state + [ALERT_FLAPPING]: legacyRawAlert.meta?.flapping, + // Set latest flapping history + [ALERT_FLAPPING_HISTORY]: legacyRawAlert.meta?.flappingHistory, + }; + + // Clean the existing alert document so any nested fields that will be updated + // are removed, to avoid duplicate data. + // e.g. if the existing alert document has the field: + // { + // kibana: { + // alert: { + // field1: 'value1' + // } + // } + // } + // and the updated alert has the field + // { + // 'kibana.alert.field1': 'value2' + // } + // the expanded field from the existing alert is removed + const cleanedAlert = removeUnflattenedFieldsFromAlert(alert, { + ...alertUpdates, + ...refreshableAlertFields, + }); + + return deepmerge.all([cleanedAlert, refreshableAlertFields, alertUpdates], { + arrayMerge: (_, sourceArray) => sourceArray, + }) as Alert & AlertData; }; diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/format_alert.test.ts b/x-pack/plugins/alerting/server/alerts_client/lib/format_alert.test.ts new file mode 100644 index 0000000000000..a96cfb715d559 --- /dev/null +++ b/x-pack/plugins/alerting/server/alerts_client/lib/format_alert.test.ts @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + expandFlattenedAlert, + compactObject, + removeUnflattenedFieldsFromAlert, +} from './format_alert'; +import { + ALERT_ACTION_GROUP, + ALERT_DURATION, + ALERT_FLAPPING, + ALERT_FLAPPING_HISTORY, + ALERT_INSTANCE_ID, + ALERT_MAINTENANCE_WINDOW_IDS, + ALERT_REASON, + ALERT_RULE_CATEGORY, + ALERT_RULE_CONSUMER, + ALERT_RULE_EXECUTION_UUID, + ALERT_RULE_NAME, + ALERT_RULE_PARAMETERS, + ALERT_RULE_PRODUCER, + ALERT_RULE_REVISION, + ALERT_RULE_TAGS, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UUID, + ALERT_START, + ALERT_STATUS, + ALERT_TIME_RANGE, + ALERT_URL, + ALERT_UUID, + ALERT_WORKFLOW_STATUS, + EVENT_ACTION, + EVENT_KIND, + SPACE_IDS, + TAGS, + TIMESTAMP, + VERSION, +} from '@kbn/rule-data-utils'; + +describe('expandFlattenedAlert', () => { + test('should correctly expand flattened alert', () => { + expect( + expandFlattenedAlert({ + [TIMESTAMP]: '2023-03-29T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [EVENT_KIND]: 'signal', + [ALERT_ACTION_GROUP]: 'warning', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'active', + [ALERT_UUID]: 'abcdefg', + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DURATION]: '36000000', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + [ALERT_RULE_CATEGORY]: 'My test rule', + [ALERT_RULE_CONSUMER]: 'bar', + [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + [ALERT_RULE_NAME]: 'rule-name', + [ALERT_RULE_PARAMETERS]: { bar: true }, + [ALERT_RULE_PRODUCER]: 'alerts', + [ALERT_RULE_REVISION]: 0, + [ALERT_RULE_TYPE_ID]: 'test.rule-type', + [ALERT_RULE_TAGS]: ['rule-', '-tags'], + [ALERT_RULE_UUID]: '1', + }) + ).toEqual({ + '@timestamp': '2023-03-29T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'warning', + duration: { + us: '36000000', + }, + flapping: false, + flapping_history: [], + instance: { + id: 'alert-A', + }, + maintenance_window_ids: [], + 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: 'abcdefg', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.9.0', + }, + tags: ['rule-', '-tags'], + }); + }); +}); + +describe('removeUnflattenedFieldsFromAlert', () => { + test('should correctly remove duplicate data from alert', () => { + expect( + removeUnflattenedFieldsFromAlert( + { + '@timestamp': '2023-03-29T12:27:28.159Z', + event: { + action: 'active', + kind: 'signal', + }, + kibana: { + alert: { + action_group: 'warning', + duration: { + us: '36000000', + }, + evaluation: { + conditions: 'matched query', + value: '123', + }, + flapping: false, + flapping_history: [], + instance: { + id: 'alert-A', + }, + maintenance_window_ids: [], + reason: 'because i said so', + 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', + }, + title: 'this is an alert', + uuid: 'abcdefg', + url: 'https://alert.url/abcdefg', + workflow_status: 'open', + }, + space_ids: ['default'], + version: '8.9.0', + }, + tags: ['rule-', '-tags'], + }, + { + [TIMESTAMP]: '2023-03-30T12:27:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'warning', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_DURATION]: '36000000', + [SPACE_IDS]: ['default'], + [VERSION]: '8.9.0', + [TAGS]: ['rule-', '-tags'], + [ALERT_RULE_CATEGORY]: 'My test rule', + [ALERT_RULE_CONSUMER]: 'bar', + [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + [ALERT_RULE_NAME]: 'rule-name', + [ALERT_RULE_PARAMETERS]: { bar: true }, + [ALERT_RULE_PRODUCER]: 'alerts', + [ALERT_RULE_REVISION]: 0, + [ALERT_RULE_TYPE_ID]: 'test.rule-type', + [ALERT_RULE_TAGS]: ['rule-', '-tags'], + [ALERT_RULE_UUID]: '1', + [ALERT_URL]: 'https://abc', + [ALERT_REASON]: 'because', + 'kibana.alert.evaluation.conditions': 'condition', + } + ) + ).toEqual({ + '@timestamp': '2023-03-29T12:27:28.159Z', + event: { + kind: 'signal', + }, + kibana: { + alert: { + evaluation: { + value: '123', + }, + instance: { + id: 'alert-A', + }, + start: '2023-03-28T12:27:28.159Z', + status: 'active', + time_range: { + gte: '2023-03-28T12:27:28.159Z', + }, + title: 'this is an alert', + uuid: 'abcdefg', + workflow_status: 'open', + }, + }, + tags: ['rule-', '-tags'], + }); + }); +}); + +describe('compactObject', () => { + test('should compact object as expected', () => { + expect(compactObject({ kibana: { alert: { rule: { execution: {} } }, rule: {} } })).toEqual({}); + expect( + compactObject({ + kibana: { + rule: 34, + testField: [], + alert: { rule: { execution: {}, nested_field: ['a', 'b'] } }, + }, + }) + ).toEqual({ + kibana: { rule: 34, testField: [], alert: { rule: { nested_field: ['a', 'b'] } } }, + }); + }); + expect(compactObject({ 'kibana.alert.rule.execution': {} })).toEqual({}); + expect( + compactObject({ 'kibana.alert.rule.execution': {}, 'kibana.alert.nested_field': ['a', 'b'] }) + ).toEqual({ 'kibana.alert.nested_field': ['a', 'b'] }); +}); diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/format_alert.ts b/x-pack/plugins/alerting/server/alerts_client/lib/format_alert.ts new file mode 100644 index 0000000000000..a81bdb35ce175 --- /dev/null +++ b/x-pack/plugins/alerting/server/alerts_client/lib/format_alert.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep, get, isEmpty, merge, omit } from 'lodash'; +import type { Alert } from '@kbn/alerts-as-data-utils'; +import { RuleAlertData } from '../../types'; +import { REFRESH_FIELDS_ALL } from './alert_conflict_resolver'; + +const expandDottedField = (dottedFieldName: string, val: unknown): object => { + const parts = dottedFieldName.split('.'); + if (parts.length === 1) { + return { [parts[0]]: val }; + } else { + return { [parts[0]]: expandDottedField(parts.slice(1).join('.'), val) }; + } +}; + +export const expandFlattenedAlert = (alert: object) => { + return Object.entries(alert).reduce( + (acc, [key, val]) => merge(acc, expandDottedField(key, val)), + {} + ); +}; + +type Obj = Record; + +// Removes empty nested objects +export const compactObject = (obj: Obj) => { + return Object.keys(obj) + .filter((key: string) => { + // just filter out empty objects + // keep any primitives or arrays, even empty arrays + return ( + !!obj[key] && + (Array.isArray(obj[key]) || + typeof obj[key] !== 'object' || + (typeof obj[key] === 'object' && !isEmpty(obj[key]))) + ); + }) + .reduce((acc, curr) => { + if (typeof obj[curr] !== 'object' || Array.isArray(obj[curr])) { + acc[curr] = obj[curr]; + } else { + const compacted = compactObject(obj[curr] as Obj); + if (!isEmpty(compacted)) { + acc[curr] = compacted; + } + } + return acc; + }, {}); +}; + +/** + * If we're replacing field values in an unflattened alert + * with the flattened version, we want to remove the unflattened version + * to avoid duplicate data in the doc + */ + +export const removeUnflattenedFieldsFromAlert = ( + alert: Record, + flattenedData: object +) => { + // make a copy of the alert + let alertCopy = cloneDeep(alert); + + // for each flattened field in the flattened data object, + // check whether that path exists in the unflattened alert + // and omit it if it does + Object.keys(flattenedData).forEach((payloadKey: string) => { + const val = get(alertCopy, payloadKey, null); + if (null == alertCopy[payloadKey] && null != val) { + alertCopy = omit(alertCopy, payloadKey); + } + }); + return compactObject(alertCopy); +}; + +export const replaceRefreshableAlertFields = ( + alert: Alert & AlertData +) => { + // Make sure that any alert fields that are updateable are flattened. + return REFRESH_FIELDS_ALL.reduce>( + (acc: Record, currField) => { + const value = get(alert, currField); + if (null != value) { + acc[currField] = value; + } + return acc; + }, + {} + ); +}; diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/get_summarized_alerts_query.ts b/x-pack/plugins/alerting/server/alerts_client/lib/get_summarized_alerts_query.ts index 302d244aa9153..917a6dad803d7 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/get_summarized_alerts_query.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/get_summarized_alerts_query.ts @@ -10,7 +10,6 @@ import { SearchRequest, SearchTotalHits, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { merge } from 'lodash'; import { ALERT_END, ALERT_INSTANCE_ID, @@ -33,6 +32,7 @@ import { } from '../types'; import { SummarizedAlertsChunk } from '../..'; import { FormatAlert } from '../../types'; +import { expandFlattenedAlert } from './format_alert'; const MAX_ALERT_DOCS_TO_RETURN = 100; enum AlertTypes { @@ -270,20 +270,6 @@ const getQueryByTimeRange = ({ }; }; -const expandFlattenedAlert = (alert: object) => { - return Object.entries(alert).reduce( - (acc, [key, val]) => merge(acc, expandDottedField(key, val)), - {} - ); -}; -const expandDottedField = (dottedFieldName: string, val: unknown): object => { - const parts = dottedFieldName.split('.'); - if (parts.length === 1) { - return { [parts[0]]: val }; - } else { - return { [parts[0]]: expandDottedField(parts.slice(1).join('.'), val) }; - } -}; const generateAlertsFilterDSL = (alertsFilter: AlertsFilter): QueryDslQueryContainer[] => { const filter: QueryDslQueryContainer[] = []; @@ -426,9 +412,4 @@ const getContinualAlertsQuery = ({ return queryBody; }; -export { - getHitsWithCount, - expandFlattenedAlert, - getLifecycleAlertsQueries, - getContinualAlertsQuery, -}; +export { getHitsWithCount, getLifecycleAlertsQueries, getContinualAlertsQuery }; diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/index.ts b/x-pack/plugins/alerting/server/alerts_client/lib/index.ts index a566c3be9ea7e..7225e87056e4f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/index.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/index.ts @@ -14,5 +14,5 @@ export { getHitsWithCount, getLifecycleAlertsQueries, getContinualAlertsQuery, - expandFlattenedAlert, } from './get_summarized_alerts_query'; +export { expandFlattenedAlert } from './format_alert'; diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/test_fixtures.ts b/x-pack/plugins/alerting/server/alerts_client/lib/test_fixtures.ts new file mode 100644 index 0000000000000..1a8e2be1e16a4 --- /dev/null +++ b/x-pack/plugins/alerting/server/alerts_client/lib/test_fixtures.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ALERT_RULE_CATEGORY, + ALERT_RULE_CONSUMER, + ALERT_RULE_EXECUTION_UUID, + ALERT_RULE_NAME, + ALERT_RULE_PARAMETERS, + ALERT_RULE_PRODUCER, + ALERT_RULE_REVISION, + ALERT_RULE_TYPE_ID, + ALERT_RULE_TAGS, + ALERT_RULE_UUID, + SPACE_IDS, + ALERT_ACTION_GROUP, + ALERT_FLAPPING, + ALERT_FLAPPING_HISTORY, + ALERT_INSTANCE_ID, + ALERT_MAINTENANCE_WINDOW_IDS, + ALERT_STATUS, + ALERT_UUID, + ALERT_WORKFLOW_STATUS, + EVENT_ACTION, + EVENT_KIND, + TAGS, + TIMESTAMP, + VERSION, + ALERT_DURATION, + ALERT_START, + ALERT_TIME_RANGE, + ALERT_END, +} from '@kbn/rule-data-utils'; +import { AlertRule } from '../types'; +import { expandFlattenedAlert } from './format_alert'; + +export const 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', +}; + +export const alertRule: AlertRule = { + [ALERT_RULE_CATEGORY]: rule.category, + [ALERT_RULE_CONSUMER]: rule.consumer, + [ALERT_RULE_EXECUTION_UUID]: rule.execution.uuid, + [ALERT_RULE_NAME]: rule.name, + [ALERT_RULE_PARAMETERS]: rule.parameters, + [ALERT_RULE_PRODUCER]: rule.producer, + [ALERT_RULE_REVISION]: rule.revision, + [ALERT_RULE_TYPE_ID]: rule.rule_type_id, + [ALERT_RULE_TAGS]: rule.tags, + [ALERT_RULE_UUID]: rule.uuid, + [SPACE_IDS]: ['default'], +}; + +export const existingFlattenedNewAlert = { + ...alertRule, + [TIMESTAMP]: '2023-03-28T12:27:28.159Z', + [EVENT_ACTION]: 'open', + [EVENT_KIND]: 'signal', + [ALERT_ACTION_GROUP]: 'error', + [ALERT_DURATION]: '0', + [ALERT_FLAPPING]: false, + [ALERT_FLAPPING_HISTORY]: [true], + [ALERT_INSTANCE_ID]: 'alert-A', + [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_STATUS]: 'active', + [ALERT_START]: '2023-03-28T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, + [ALERT_UUID]: 'abcdefg', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['default'], + [VERSION]: '8.8.1', + [TAGS]: ['rule-', '-tags'], +}; + +export const existingFlattenedActiveAlert = { + ...existingFlattenedNewAlert, + [TIMESTAMP]: '2023-03-28T12:28:28.159Z', + [EVENT_ACTION]: 'active', + [ALERT_ACTION_GROUP]: 'default', + [ALERT_DURATION]: '3600', + [ALERT_FLAPPING_HISTORY]: [true, false], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-x'], +}; + +export const existingFlattenedRecoveredAlert = { + ...existingFlattenedActiveAlert, + [TIMESTAMP]: '2023-03-28T12:29:28.159Z', + [EVENT_ACTION]: 'close', + [ALERT_ACTION_GROUP]: 'recovered', + [ALERT_DURATION]: '36000000', + [ALERT_END]: '2023-03-30T12:27:28.159Z', + [ALERT_TIME_RANGE]: { gte: '2023-03-27T12:27:28.159Z', lte: '2023-03-30T12:27:28.159Z' }, + [ALERT_FLAPPING_HISTORY]: [true, false, false, true], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['maint-x'], + [ALERT_STATUS]: 'recovered', +}; + +export const existingExpandedNewAlert = expandFlattenedAlert(existingFlattenedNewAlert); +export const existingExpandedActiveAlert = expandFlattenedAlert(existingFlattenedActiveAlert); +export const existingExpandedRecoveredAlert = expandFlattenedAlert(existingFlattenedRecoveredAlert);