From d3e831f8123bcea1bfe37b9f24e847652c363095 Mon Sep 17 00:00:00 2001 From: Ying Date: Thu, 26 Sep 2024 12:37:21 -0400 Subject: [PATCH 1/2] Breaking up ActionScheduler.run into subclasses --- .../server/create_execute_function.test.ts | 90 +++ .../actions/server/create_execute_function.ts | 4 + .../alerting_event_logger.test.ts | 9 + .../alerting_event_logger.ts | 1 + .../action_scheduler/action_scheduler.test.ts | 66 +- .../action_scheduler/action_scheduler.ts | 501 ++------------ .../lib/build_rule_url.test.ts | 141 ++++ .../action_scheduler/lib/build_rule_url.ts | 65 ++ .../lib/format_action_to_enqueue.test.ts | 222 +++++++ .../lib/format_action_to_enqueue.ts | 48 ++ .../{ => lib}/get_summarized_alerts.test.ts | 6 +- .../{ => lib}/get_summarized_alerts.ts | 4 +- .../task_runner/action_scheduler/lib/index.ts | 20 + .../{ => lib}/rule_action_helper.test.ts | 2 +- .../{ => lib}/rule_action_helper.ts | 2 +- .../lib/should_schedule_action.test.ts | 195 ++++++ .../lib/should_schedule_action.ts | 70 ++ .../per_alert_action_scheduler.test.ts | 610 ++++++++++++------ .../schedulers/per_alert_action_scheduler.ts | 134 +++- .../summary_action_scheduler.test.ts | 319 ++++++--- .../schedulers/summary_action_scheduler.ts | 121 +++- .../system_action_scheduler.test.ts | 297 +++++++-- .../schedulers/system_action_scheduler.ts | 118 +++- .../task_runner/action_scheduler/types.ts | 21 +- .../alerting/server/task_runner/fixtures.ts | 10 +- .../server/task_runner/task_runner.test.ts | 34 +- 26 files changed, 2281 insertions(+), 829 deletions(-) create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts rename x-pack/plugins/alerting/server/task_runner/action_scheduler/{ => lib}/get_summarized_alerts.test.ts (95%) rename x-pack/plugins/alerting/server/task_runner/action_scheduler/{ => lib}/get_summarized_alerts.ts (98%) create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts rename x-pack/plugins/alerting/server/task_runner/action_scheduler/{ => lib}/rule_action_helper.test.ts (99%) rename x-pack/plugins/alerting/server/task_runner/action_scheduler/{ => lib}/rule_action_helper.ts (99%) create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index a1ab85933d9bc..7be187743e634 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -1088,6 +1088,7 @@ describe('bulkExecute()', () => { "actionTypeId": "mock-action", "id": "123", "response": "queuedActionsLimitError", + "uuid": undefined, }, ], } @@ -1099,4 +1100,93 @@ describe('bulkExecute()', () => { ] `); }); + + test('passes through action uuid if provided', async () => { + mockTaskManager.aggregate.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 2, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: {}, + }); + mockActionsConfig.getMaxQueued.mockReturnValueOnce(3); + const executeFn = createBulkExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + isESOCanEncrypt: true, + inMemoryConnectors: [], + configurationUtilities: mockActionsConfig, + logger: mockLogger, + }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { id: '123', type: 'action', attributes: { actionTypeId: 'mock-action' }, references: [] }, + ], + }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { id: '234', type: 'action_task_params', attributes: { actionId: '123' }, references: [] }, + ], + }); + expect( + await executeFn(savedObjectsClient, [ + { + id: '123', + params: { baz: false }, + spaceId: 'default', + executionId: '123abc', + apiKey: null, + source: asHttpRequestExecutionSource(request), + actionTypeId: 'mock-action', + uuid: 'aaa', + }, + { + id: '123', + params: { baz: false }, + spaceId: 'default', + executionId: '456xyz', + apiKey: null, + source: asHttpRequestExecutionSource(request), + actionTypeId: 'mock-action', + uuid: 'bbb', + }, + ]) + ).toMatchInlineSnapshot(` + Object { + "errors": true, + "items": Array [ + Object { + "actionTypeId": "mock-action", + "id": "123", + "response": "success", + "uuid": "aaa", + }, + Object { + "actionTypeId": "mock-action", + "id": "123", + "response": "queuedActionsLimitError", + "uuid": "bbb", + }, + ], + } + `); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:mock-action", + }, + ], + ] + `); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index e8f9c859747ff..a92bff9719559 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -31,6 +31,7 @@ interface CreateExecuteFunctionOptions { export interface ExecuteOptions extends Pick { id: string; + uuid?: string; spaceId: string; apiKey: string | null; executionId: string; @@ -71,6 +72,7 @@ export interface ExecutionResponse { export interface ExecutionResponseItem { id: string; + uuid?: string; actionTypeId: string; response: ExecutionResponseType; } @@ -197,12 +199,14 @@ export function createBulkExecutionEnqueuerFunction({ items: runnableActions .map((a) => ({ id: a.id, + uuid: a.uuid, actionTypeId: a.actionTypeId, response: ExecutionResponseType.SUCCESS, })) .concat( actionsOverLimit.map((a) => ({ id: a.id, + uuid: a.uuid, actionTypeId: a.actionTypeId, response: ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR, })) diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts index 82e8663bd6bf8..082d5ea6381df 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts @@ -807,6 +807,15 @@ describe('AlertingEventLogger', () => { expect(eventLogger.logEvent).toHaveBeenCalledWith(event); }); + + test('should log action event with uuid', () => { + alertingEventLogger.initialize({ context: ruleContext, runDate, ruleData }); + alertingEventLogger.logAction({ ...action, uuid: 'abcdefg' }); + + const event = createActionExecuteRecord(ruleContext, ruleData, [alertSO], action); + + expect(eventLogger.logEvent).toHaveBeenCalledWith(event); + }); }); describe('done()', () => { diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts index f29e1e00473b2..5f9af56754e7a 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts @@ -78,6 +78,7 @@ interface AlertOpts { export interface ActionOpts { id: string; + uuid?: string; typeId: string; alertId?: string; alertGroup?: string; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts index 600f6aedbe039..b6f250b47205e 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts @@ -60,7 +60,9 @@ const defaultSchedulerContext = getDefaultSchedulerContext( const defaultExecutionResponse = { errors: false, - items: [{ actionTypeId: 'test', id: '1', response: ExecutionResponseType.SUCCESS }], + items: [ + { actionTypeId: 'test', id: '1', uuid: '111-111', response: ExecutionResponseType.SUCCESS }, + ], }; let ruleRunMetricsStore: RuleRunMetricsStore; @@ -99,7 +101,7 @@ describe('Action Scheduler', () => { }); afterAll(() => clock.restore()); - test('enqueues execution per selected action', async () => { + test('schedules execution per selected action', async () => { const alerts = generateAlert({ id: 1 }); const actionScheduler = new ActionScheduler(getSchedulerContext()); await actionScheduler.run(alerts); @@ -138,6 +140,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -146,6 +149,7 @@ describe('Action Scheduler', () => { expect(alertingEventLogger.logAction).toHaveBeenCalledTimes(1); expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, { id: '1', + uuid: '111-111', typeId: 'test', alertId: '1', alertGroup: 'default', @@ -368,6 +372,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -409,6 +414,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -437,11 +443,13 @@ describe('Action Scheduler', () => { { actionTypeId: 'test2', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'test2', id: '2', + uuid: '222-222', response: ExecutionResponseType.SUCCESS, }, ], @@ -508,20 +516,23 @@ describe('Action Scheduler', () => { actionsClient.bulkEnqueueExecution.mockResolvedValueOnce({ errors: false, items: [ - { actionTypeId: 'test', id: '1', response: ExecutionResponseType.SUCCESS }, + { actionTypeId: 'test', id: '1', uuid: '222-222', response: ExecutionResponseType.SUCCESS }, { actionTypeId: 'test-action-type-id', id: '2', + uuid: '222-222', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'another-action-type-id', id: '4', + uuid: '444-444', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'another-action-type-id', id: '5', + uuid: '555-555', response: ExecutionResponseType.SUCCESS, }, ], @@ -537,6 +548,7 @@ describe('Action Scheduler', () => { contextVal: 'My other {{context.value}} goes here', stateVal: 'My other {{state.value}} goes here', }, + uuid: '222-222', }, { id: '3', @@ -547,6 +559,7 @@ describe('Action Scheduler', () => { contextVal: '{{context.value}} goes here', stateVal: '{{state.value}} goes here', }, + uuid: '333-333', }, { id: '4', @@ -557,6 +570,7 @@ describe('Action Scheduler', () => { contextVal: '{{context.value}} goes here', stateVal: '{{state.value}} goes here', }, + uuid: '444-444', }, { id: '5', @@ -567,6 +581,7 @@ describe('Action Scheduler', () => { contextVal: '{{context.value}} goes here', stateVal: '{{state.value}} goes here', }, + uuid: '555-555', }, ]; const actionScheduler = new ActionScheduler( @@ -612,16 +627,19 @@ describe('Action Scheduler', () => { { actionTypeId: 'test', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'test', id: '2', + uuid: '222-222', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'test', id: '3', + uuid: '333-333', response: ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR, }, ], @@ -636,6 +654,7 @@ describe('Action Scheduler', () => { contextVal: 'My other {{context.value}} goes here', stateVal: 'My other {{state.value}} goes here', }, + uuid: '111-111', }, { id: '2', @@ -646,6 +665,7 @@ describe('Action Scheduler', () => { contextVal: 'My other {{context.value}} goes here', stateVal: 'My other {{state.value}} goes here', }, + uuid: '222-222', }, { id: '3', @@ -656,6 +676,7 @@ describe('Action Scheduler', () => { contextVal: '{{context.value}} goes here', stateVal: '{{state.value}} goes here', }, + uuid: '333-333', }, ]; const actionScheduler = new ActionScheduler( @@ -679,7 +700,7 @@ describe('Action Scheduler', () => { test('schedules alerts with recovered actions', async () => { const actions = [ { - id: '1', + id: 'action-2', group: 'recovered', actionTypeId: 'test', params: { @@ -689,6 +710,7 @@ describe('Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, + uuid: '222-222', }, ]; const actionScheduler = new ActionScheduler( @@ -711,7 +733,7 @@ describe('Action Scheduler', () => { "apiKey": "MTIzOmFiYw==", "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - "id": "1", + "id": "action-2", "params": Object { "alertVal": "My 1 name-of-alert test1 tag-A,tag-B 1 goes here", "contextVal": "My goes here", @@ -734,6 +756,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "222-222", }, ], ] @@ -883,6 +906,7 @@ describe('Action Scheduler', () => { { actionTypeId: 'testActionTypeId', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, ], @@ -914,6 +938,7 @@ describe('Action Scheduler', () => { message: 'New: {{alerts.new.count}} Ongoing: {{alerts.ongoing.count}} Recovered: {{alerts.recovered.count}}', }, + uuid: '111-111', }, ], }, @@ -957,6 +982,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -964,6 +990,7 @@ describe('Action Scheduler', () => { expect(alertingEventLogger.logAction).toBeCalledWith({ alertSummary: { new: 1, ongoing: 0, recovered: 0 }, id: '1', + uuid: '111-111', typeId: 'testActionTypeId', }); }); @@ -1012,6 +1039,7 @@ describe('Action Scheduler', () => { { actionTypeId: 'testActionTypeId', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, ], @@ -1095,6 +1123,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -1102,6 +1131,7 @@ describe('Action Scheduler', () => { expect(alertingEventLogger.logAction).toBeCalledWith({ alertSummary: { new: 1, ongoing: 0, recovered: 0 }, id: '1', + uuid: '111-111', typeId: 'testActionTypeId', }); }); @@ -1256,10 +1286,11 @@ describe('Action Scheduler', () => { actionsClient.bulkEnqueueExecution.mockResolvedValueOnce({ errors: false, items: [ - { actionTypeId: 'test', id: '1', response: ExecutionResponseType.SUCCESS }, + { actionTypeId: 'test', id: '1', uuid: '111-111', response: ExecutionResponseType.SUCCESS }, { actionTypeId: 'test', id: '2', + uuid: '222-222', response: ExecutionResponseType.SUCCESS, }, ], @@ -1276,6 +1307,7 @@ describe('Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, + uuid: '111-111', }, { id: '2', @@ -1288,6 +1320,7 @@ describe('Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, + uuid: '222-222', }, ]; const actionScheduler = new ActionScheduler( @@ -1333,6 +1366,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, Object { "actionTypeId": "test", @@ -1362,6 +1396,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "222-222", }, ], ] @@ -1448,6 +1483,7 @@ describe('Action Scheduler', () => { { actionTypeId: 'testActionTypeId', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, ], @@ -1518,6 +1554,7 @@ describe('Action Scheduler', () => { { actionTypeId: 'testActionTypeId', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, ], @@ -1541,7 +1578,7 @@ describe('Action Scheduler', () => { actions: [ { id: '1', - uuid: '111', + uuid: '111-111', group: 'default', actionTypeId: 'testActionTypeId', frequency: { @@ -1587,17 +1624,19 @@ describe('Action Scheduler', () => { ], source: { source: { id: '1', type: RULE_SAVED_OBJECT_TYPE }, type: 'SAVED_OBJECT' }, spaceId: 'test1', + uuid: '111-111', }, ]); expect(alertingEventLogger.logAction).toHaveBeenCalledWith({ alertGroup: 'default', alertId: '1', id: '1', + uuid: '111-111', typeId: 'testActionTypeId', }); expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1); expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith( - '(2) alerts have been filtered out for: testActionTypeId:111' + '(2) alerts have been filtered out for: testActionTypeId:111-111' ); }); @@ -1840,6 +1879,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, Object { "actionTypeId": "test", @@ -1869,6 +1909,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, Object { "actionTypeId": "test", @@ -1898,6 +1939,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -2261,12 +2303,13 @@ describe('Action Scheduler', () => { const executorParams = getSchedulerContext({ rule: { ...defaultSchedulerContext.rule, + actions: [], systemActions: [ { id: '1', actionTypeId: '.test-system-action', params: actionsParams, - uui: 'test', + uuid: 'test', }, ], }, @@ -2360,6 +2403,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "test", }, ], ] @@ -2368,6 +2412,7 @@ describe('Action Scheduler', () => { expect(alertingEventLogger.logAction).toBeCalledWith({ alertSummary: { new: 1, ongoing: 0, recovered: 0 }, id: '1', + uuid: 'test', typeId: '.test-system-action', }); }); @@ -2387,6 +2432,7 @@ describe('Action Scheduler', () => { const executorParams = getSchedulerContext({ rule: { ...defaultSchedulerContext.rule, + actions: [], systemActions: [ { id: 'action-id', @@ -2443,6 +2489,7 @@ describe('Action Scheduler', () => { }, rule: { ...defaultSchedulerContext.rule, + actions: [], systemActions: [ { id: 'action-id', @@ -2477,6 +2524,7 @@ describe('Action Scheduler', () => { const executorParams = getSchedulerContext({ rule: { ...defaultSchedulerContext.rule, + actions: [], systemActions: [ { id: '1', diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts index 3b804ce3da413..2f29e4f265a33 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils'; -import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; import { createTaskRunError, isEphemeralTaskRejectedDueToCapacityError, @@ -19,77 +17,21 @@ import { } from '@kbn/actions-plugin/server/create_execute_function'; import { ActionsCompletion } from '@kbn/alerting-state-types'; import { chunk } from 'lodash'; -import { CombinedSummarizedAlerts, ThrottledActions } from '../../types'; -import { injectActionParams } from '../inject_action_params'; -import { ActionSchedulerOptions, IActionScheduler, RuleUrl } from './types'; -import { - transformActionParams, - TransformActionParamsOptions, - transformSummaryActionParams, -} from '../transform_action_params'; +import { ThrottledActions } from '../../types'; +import { ActionSchedulerOptions, ActionsToSchedule, IActionScheduler } from './types'; import { Alert } from '../../alert'; import { AlertInstanceContext, AlertInstanceState, - RuleAction, RuleTypeParams, RuleTypeState, - SanitizedRule, RuleAlertData, - RuleSystemAction, } from '../../../common'; -import { - generateActionHash, - getSummaryActionsFromTaskState, - getSummaryActionTimeBounds, - isActionOnInterval, -} from './rule_action_helper'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { ConnectorAdapter } from '../../connector_adapters/types'; +import { getSummaryActionsFromTaskState } from './lib'; import { withAlertingSpan } from '../lib'; import * as schedulers from './schedulers'; -interface LogAction { - id: string; - typeId: string; - alertId?: string; - alertGroup?: string; - alertSummary?: { - new: number; - ongoing: number; - recovered: number; - }; -} - -interface RunSummarizedActionArgs { - action: RuleAction; - summarizedAlerts: CombinedSummarizedAlerts; - spaceId: string; - bulkActions: EnqueueExecutionOptions[]; -} - -interface RunSystemActionArgs { - action: RuleSystemAction; - connectorAdapter: ConnectorAdapter; - summarizedAlerts: CombinedSummarizedAlerts; - rule: SanitizedRule; - ruleProducer: string; - spaceId: string; - bulkActions: EnqueueExecutionOptions[]; -} - -interface RunActionArgs< - State extends AlertInstanceState, - Context extends AlertInstanceContext, - ActionGroupIds extends string, - RecoveryActionGroupId extends string -> { - action: RuleAction; - alert: Alert; - ruleId: string; - spaceId: string; - bulkActions: EnqueueExecutionOptions[]; -} +const BULK_SCHEDULE_CHUNK_SIZE = 1000; export interface RunResult { throttledSummaryActions: ThrottledActions; @@ -110,9 +52,6 @@ export class ActionScheduler< > = []; private ephemeralActionsToSchedule: number; - private CHUNK_SIZE = 1000; - private ruleTypeActionGroups?: Map; - private previousStartedAt: Date | null; constructor( private readonly context: ActionSchedulerOptions< @@ -127,11 +66,6 @@ export class ActionScheduler< > ) { this.ephemeralActionsToSchedule = context.taskRunnerContext.maxEphemeralActionsPerRule; - this.ruleTypeActionGroups = new Map( - context.ruleType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name]) - ); - this.previousStartedAt = context.previousStartedAt; - for (const [_, scheduler] of Object.entries(schedulers)) { this.schedulers.push(new scheduler(context)); } @@ -148,148 +82,30 @@ export class ActionScheduler< summaryActions: this.context.taskInstance.state?.summaryActions, }); - const executables = []; + const allActionsToScheduleResult: ActionsToSchedule[] = []; for (const scheduler of this.schedulers) { - executables.push( - ...(await scheduler.generateExecutables({ alerts, throttledSummaryActions })) + allActionsToScheduleResult.push( + ...(await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions })) ); } - if (executables.length === 0) { + if (allActionsToScheduleResult.length === 0) { return { throttledSummaryActions }; } - const { - CHUNK_SIZE, - context: { - logger, - alertingEventLogger, - ruleRunMetricsStore, - taskRunnerContext: { actionsConfigMap }, - taskInstance: { - params: { spaceId, alertId: ruleId }, - }, - }, - } = this; - - const logActions: Record = {}; - const bulkActions: EnqueueExecutionOptions[] = []; - let bulkActionsResponse: ExecutionResponseItem[] = []; + const bulkScheduleRequest: EnqueueExecutionOptions[] = []; - this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); - - for (const { action, alert, summarizedAlerts } of executables) { - const { actionTypeId } = action; - - ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(actionTypeId); - if (ruleRunMetricsStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) { - ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ - actionTypeId, - status: ActionsCompletion.PARTIAL, - }); - logger.debug( - `Rule "${this.context.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.` - ); - break; - } - - if ( - ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({ - actionTypeId, - actionsConfigMap, - }) - ) { - if (!ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(actionTypeId)) { - logger.debug( - `Rule "${this.context.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${actionTypeId} has been reached.` - ); - } - ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ - actionTypeId, - status: ActionsCompletion.PARTIAL, - }); - continue; - } - - if (!this.isExecutableAction(action)) { - this.context.logger.warn( - `Rule "${this.context.taskInstance.params.alertId}" skipped scheduling action "${action.id}" because it is disabled` - ); - continue; - } - - ruleRunMetricsStore.incrementNumberOfTriggeredActions(); - ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId); - - if (!this.isSystemAction(action) && summarizedAlerts) { - const defaultAction = action as RuleAction; - if (isActionOnInterval(action)) { - throttledSummaryActions[defaultAction.uuid!] = { date: new Date().toISOString() }; - } - - logActions[defaultAction.id] = await this.runSummarizedAction({ - action, - summarizedAlerts, - spaceId, - bulkActions, - }); - } else if (summarizedAlerts && this.isSystemAction(action)) { - const hasConnectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.has( - action.actionTypeId - ); - /** - * System actions without an adapter - * cannot be executed - * - */ - if (!hasConnectorAdapter) { - this.context.logger.warn( - `Rule "${this.context.taskInstance.params.alertId}" skipped scheduling system action "${action.id}" because no connector adapter is configured` - ); - - continue; - } - - const connectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.get( - action.actionTypeId - ); - logActions[action.id] = await this.runSystemAction({ - action, - connectorAdapter, - summarizedAlerts, - rule: this.context.rule, - ruleProducer: this.context.ruleType.producer, - spaceId, - bulkActions, - }); - } else if (!this.isSystemAction(action) && alert) { - const defaultAction = action as RuleAction; - logActions[defaultAction.id] = await this.runAction({ - action, - spaceId, - alert, - ruleId, - bulkActions, - }); - - const actionGroup = defaultAction.group; - if (!this.isRecoveredAlert(actionGroup)) { - if (isActionOnInterval(action)) { - alert.updateLastScheduledActions( - defaultAction.group as ActionGroupIds, - generateActionHash(action), - defaultAction.uuid - ); - } else { - alert.updateLastScheduledActions(defaultAction.group as ActionGroupIds); - } - alert.unscheduleActions(); - } - } + for (const result of allActionsToScheduleResult) { + await this.runActionAsEphemeralOrAddToBulkScheduleRequest({ + enqueueOptions: result.actionToEnqueue, + bulkScheduleRequest, + }); } - if (!!bulkActions.length) { - for (const c of chunk(bulkActions, CHUNK_SIZE)) { + let bulkScheduleResponse: ExecutionResponseItem[] = []; + + if (!!bulkScheduleRequest.length) { + for (const c of chunk(bulkScheduleRequest, BULK_SCHEDULE_CHUNK_SIZE)) { let enqueueResponse; try { enqueueResponse = await withAlertingSpan('alerting:bulk-enqueue-actions', () => @@ -302,7 +118,7 @@ export class ActionScheduler< throw createTaskRunError(e, TaskErrorSource.FRAMEWORK); } if (enqueueResponse.errors) { - bulkActionsResponse = bulkActionsResponse.concat( + bulkScheduleResponse = bulkScheduleResponse.concat( enqueueResponse.items.filter( (i) => i.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR ) @@ -311,280 +127,51 @@ export class ActionScheduler< } } - if (!!bulkActionsResponse.length) { - for (const r of bulkActionsResponse) { + const actionsToNotLog: string[] = []; + if (!!bulkScheduleResponse.length) { + for (const r of bulkScheduleResponse) { if (r.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR) { - ruleRunMetricsStore.setHasReachedQueuedActionsLimit(true); - ruleRunMetricsStore.decrementNumberOfTriggeredActions(); - ruleRunMetricsStore.decrementNumberOfTriggeredActionsByConnectorType(r.actionTypeId); - ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + this.context.ruleRunMetricsStore.setHasReachedQueuedActionsLimit(true); + this.context.ruleRunMetricsStore.decrementNumberOfTriggeredActions(); + this.context.ruleRunMetricsStore.decrementNumberOfTriggeredActionsByConnectorType( + r.actionTypeId + ); + this.context.ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ actionTypeId: r.actionTypeId, status: ActionsCompletion.PARTIAL, }); - logger.debug( + this.context.logger.debug( `Rule "${this.context.rule.id}" skipped scheduling action "${r.id}" because the maximum number of queued actions has been reached.` ); - delete logActions[r.id]; + const uuid = r.uuid; + if (uuid) { + actionsToNotLog.push(uuid); + } } } } - const logActionsValues = Object.values(logActions); - if (!!logActionsValues.length) { - for (const action of logActionsValues) { - alertingEventLogger.logAction(action); - } - } - - return { throttledSummaryActions }; - } - - private async runSummarizedAction({ - action, - summarizedAlerts, - spaceId, - bulkActions, - }: RunSummarizedActionArgs): Promise { - const { start, end } = getSummaryActionTimeBounds( - action, - this.context.rule.schedule, - this.previousStartedAt + const actionsToLog = allActionsToScheduleResult.filter( + (result) => result.actionToLog.uuid && !actionsToNotLog.includes(result.actionToLog.uuid) ); - const ruleUrl = this.buildRuleUrl(spaceId, start, end); - const actionToRun = { - ...action, - params: injectActionParams({ - actionTypeId: action.actionTypeId, - ruleUrl, - ruleName: this.context.rule.name, - actionParams: transformSummaryActionParams({ - alerts: summarizedAlerts, - rule: this.context.rule, - ruleTypeId: this.context.ruleType.id, - actionId: action.id, - actionParams: action.params, - spaceId, - actionsPlugin: this.context.taskRunnerContext.actionsPlugin, - actionTypeId: action.actionTypeId, - kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, - ruleUrl: ruleUrl?.absoluteUrl, - }), - }), - }; - await this.actionRunOrAddToBulk({ - enqueueOptions: this.getEnqueueOptions(actionToRun), - bulkActions, - }); - - return { - id: action.id, - typeId: action.actionTypeId, - alertSummary: { - new: summarizedAlerts.new.count, - ongoing: summarizedAlerts.ongoing.count, - recovered: summarizedAlerts.recovered.count, - }, - }; - } - - private async runSystemAction({ - action, - spaceId, - connectorAdapter, - summarizedAlerts, - rule, - ruleProducer, - bulkActions, - }: RunSystemActionArgs): Promise { - const ruleUrl = this.buildRuleUrl(spaceId); - - const connectorAdapterActionParams = connectorAdapter.buildActionParams({ - alerts: summarizedAlerts, - rule: { - id: rule.id, - tags: rule.tags, - name: rule.name, - consumer: rule.consumer, - producer: ruleProducer, - }, - ruleUrl: ruleUrl?.absoluteUrl, - spaceId, - params: action.params, - }); - - const actionToRun = Object.assign(action, { params: connectorAdapterActionParams }); - - await this.actionRunOrAddToBulk({ - enqueueOptions: this.getEnqueueOptions(actionToRun), - bulkActions, - }); - - return { - id: action.id, - typeId: action.actionTypeId, - alertSummary: { - new: summarizedAlerts.new.count, - ongoing: summarizedAlerts.ongoing.count, - recovered: summarizedAlerts.recovered.count, - }, - }; - } - - private async runAction({ - action, - spaceId, - alert, - ruleId, - bulkActions, - }: RunActionArgs): Promise { - const ruleUrl = this.buildRuleUrl(spaceId); - const executableAlert = alert!; - const actionGroup = action.group as ActionGroupIds; - const transformActionParamsOptions: TransformActionParamsOptions = { - actionsPlugin: this.context.taskRunnerContext.actionsPlugin, - alertId: ruleId, - alertType: this.context.ruleType.id, - actionTypeId: action.actionTypeId, - alertName: this.context.rule.name, - spaceId, - tags: this.context.rule.tags, - alertInstanceId: executableAlert.getId(), - alertUuid: executableAlert.getUuid(), - alertActionGroup: actionGroup, - alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!, - context: executableAlert.getContext(), - actionId: action.id, - state: executableAlert.getState(), - kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, - alertParams: this.context.rule.params, - actionParams: action.params, - flapping: executableAlert.getFlapping(), - ruleUrl: ruleUrl?.absoluteUrl, - }; - - if (executableAlert.isAlertAsData()) { - transformActionParamsOptions.aadAlert = executableAlert.getAlertAsData(); - } - const actionToRun = { - ...action, - params: injectActionParams({ - actionTypeId: action.actionTypeId, - ruleUrl, - ruleName: this.context.rule.name, - actionParams: transformActionParams(transformActionParamsOptions), - }), - }; - - await this.actionRunOrAddToBulk({ - enqueueOptions: this.getEnqueueOptions(actionToRun), - bulkActions, - }); - - return { - id: action.id, - typeId: action.actionTypeId, - alertId: alert.getId(), - alertGroup: action.group, - }; - } - - private isExecutableAction(action: RuleAction | RuleSystemAction) { - return this.context.taskRunnerContext.actionsPlugin.isActionExecutable( - action.id, - action.actionTypeId, - { - notifyUsage: true, + if (!!actionsToLog.length) { + for (const action of actionsToLog) { + this.context.alertingEventLogger.logAction(action.actionToLog); } - ); - } - - private isSystemAction(action?: RuleAction | RuleSystemAction): action is RuleSystemAction { - return this.context.taskRunnerContext.actionsPlugin.isSystemActionConnector(action?.id ?? ''); - } - - private isRecoveredAlert(actionGroup: string) { - return actionGroup === this.context.ruleType.recoveryActionGroup.id; - } - - private buildRuleUrl(spaceId: string, start?: number, end?: number): RuleUrl | undefined { - if (!this.context.taskRunnerContext.kibanaBaseUrl) { - return; } - const relativePath = this.context.ruleType.getViewInAppRelativeUrl - ? this.context.ruleType.getViewInAppRelativeUrl({ rule: this.context.rule, start, end }) - : `${triggersActionsRoute}${getRuleDetailsRoute(this.context.rule.id)}`; - - try { - const basePathname = new URL(this.context.taskRunnerContext.kibanaBaseUrl).pathname; - const basePathnamePrefix = basePathname !== '/' ? `${basePathname}` : ''; - const spaceIdSegment = spaceId !== 'default' ? `/s/${spaceId}` : ''; - - const ruleUrl = new URL( - [basePathnamePrefix, spaceIdSegment, relativePath].join(''), - this.context.taskRunnerContext.kibanaBaseUrl - ); - - return { - absoluteUrl: ruleUrl.toString(), - kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, - basePathname: basePathnamePrefix, - spaceIdSegment, - relativePath, - }; - } catch (error) { - this.context.logger.debug( - `Rule "${this.context.rule.id}" encountered an error while constructing the rule.url variable: ${error.message}` - ); - return; - } - } - - private getEnqueueOptions(action: RuleAction | RuleSystemAction): EnqueueExecutionOptions { - const { - context: { - apiKey, - ruleConsumer, - executionId, - taskInstance: { - params: { spaceId, alertId: ruleId }, - }, - }, - } = this; - - const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; - return { - id: action.id, - params: action.params, - spaceId, - apiKey: apiKey ?? null, - consumer: ruleConsumer, - source: asSavedObjectExecutionSource({ - id: ruleId, - type: RULE_SAVED_OBJECT_TYPE, - }), - executionId, - relatedSavedObjects: [ - { - id: ruleId, - type: RULE_SAVED_OBJECT_TYPE, - namespace: namespace.namespace, - typeId: this.context.ruleType.id, - }, - ], - actionTypeId: action.actionTypeId, - }; + return { throttledSummaryActions }; } - private async actionRunOrAddToBulk({ + private async runActionAsEphemeralOrAddToBulkScheduleRequest({ enqueueOptions, - bulkActions, + bulkScheduleRequest, }: { enqueueOptions: EnqueueExecutionOptions; - bulkActions: EnqueueExecutionOptions[]; + bulkScheduleRequest: EnqueueExecutionOptions[]; }) { if ( this.context.taskRunnerContext.supportsEphemeralTasks && @@ -595,11 +182,11 @@ export class ActionScheduler< await this.context.actionsClient!.ephemeralEnqueuedExecution(enqueueOptions); } catch (err) { if (isEphemeralTaskRejectedDueToCapacityError(err)) { - bulkActions.push(enqueueOptions); + bulkScheduleRequest.push(enqueueOptions); } } } else { - bulkActions.push(enqueueOptions); + bulkScheduleRequest.push(enqueueOptions); } } } diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts new file mode 100644 index 0000000000000..cb1f3c60fd992 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts @@ -0,0 +1,141 @@ +/* + * 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 { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { buildRuleUrl } from './build_rule_url'; +import { getRule } from '../test_fixtures'; + +const logger = loggingSystemMock.create().get(); +const rule = getRule(); + +describe('buildRuleUrl', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should return undefined if kibanaBaseUrl is not provided', () => { + expect( + buildRuleUrl({ + kibanaBaseUrl: undefined, + logger, + rule, + spaceId: 'default', + }) + ).toBeUndefined(); + }); + + test('should return the expected URL', () => { + expect( + buildRuleUrl({ + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + spaceId: 'default', + }) + ).toEqual({ + absoluteUrl: + 'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1', + spaceIdSegment: '', + }); + }); + + test('should return the expected URL for custom space', () => { + expect( + buildRuleUrl({ + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + spaceId: 'my-special-space', + }) + ).toEqual({ + absoluteUrl: + 'http://localhost:5601/s/my-special-space/app/management/insightsAndAlerting/triggersActions/rule/1', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1', + spaceIdSegment: '/s/my-special-space', + }); + }); + + test('should return the expected URL when getViewInAppRelativeUrl is defined', () => { + expect( + buildRuleUrl({ + getViewInAppRelativeUrl: ({ rule: r }) => `/app/test/my-custom-rule-page/${r.id}`, + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + spaceId: 'default', + }) + ).toEqual({ + absoluteUrl: 'http://localhost:5601/app/test/my-custom-rule-page/1', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/test/my-custom-rule-page/1', + spaceIdSegment: '', + }); + }); + + test('should return the expected URL when start, end and getViewInAppRelativeUrl is defined', () => { + expect( + buildRuleUrl({ + end: 987654321, + getViewInAppRelativeUrl: ({ rule: r, start: s, end: e }) => + `/app/test/my-custom-rule-page/${r.id}?start=${s}&end=${e}`, + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + start: 123456789, + spaceId: 'default', + }) + ).toEqual({ + absoluteUrl: + 'http://localhost:5601/app/test/my-custom-rule-page/1?start=123456789&end=987654321', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/test/my-custom-rule-page/1?start=123456789&end=987654321', + spaceIdSegment: '', + }); + }); + + test('should return the expected URL when start and end are defined but getViewInAppRelativeUrl is undefined', () => { + expect( + buildRuleUrl({ + end: 987654321, + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + start: 123456789, + spaceId: 'default', + }) + ).toEqual({ + absoluteUrl: + 'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1', + spaceIdSegment: '', + }); + }); + + test('should return undefined if base url is invalid', () => { + expect( + buildRuleUrl({ + kibanaBaseUrl: 'foo-url', + logger, + rule, + spaceId: 'default', + }) + ).toBeUndefined(); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "1" encountered an error while constructing the rule.url variable: Invalid URL: foo-url` + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts new file mode 100644 index 0000000000000..3df27a512c7f9 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts @@ -0,0 +1,65 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-types'; +import { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils'; +import { GetViewInAppRelativeUrlFn } from '../../../types'; + +interface BuildRuleUrlOpts { + end?: number; + getViewInAppRelativeUrl?: GetViewInAppRelativeUrlFn; + kibanaBaseUrl: string | undefined; + logger: Logger; + rule: SanitizedRule; + spaceId: string; + start?: number; +} + +interface BuildRuleUrlResult { + absoluteUrl: string; + basePathname: string; + kibanaBaseUrl: string; + relativePath: string; + spaceIdSegment: string; +} + +export const buildRuleUrl = ( + opts: BuildRuleUrlOpts +): BuildRuleUrlResult | undefined => { + if (!opts.kibanaBaseUrl) { + return; + } + + const relativePath = opts.getViewInAppRelativeUrl + ? opts.getViewInAppRelativeUrl({ rule: opts.rule, start: opts.start, end: opts.end }) + : `${triggersActionsRoute}${getRuleDetailsRoute(opts.rule.id)}`; + + try { + const basePathname = new URL(opts.kibanaBaseUrl).pathname; + const basePathnamePrefix = basePathname !== '/' ? `${basePathname}` : ''; + const spaceIdSegment = opts.spaceId !== 'default' ? `/s/${opts.spaceId}` : ''; + + const ruleUrl = new URL( + [basePathnamePrefix, spaceIdSegment, relativePath].join(''), + opts.kibanaBaseUrl + ); + + return { + absoluteUrl: ruleUrl.toString(), + kibanaBaseUrl: opts.kibanaBaseUrl, + basePathname: basePathnamePrefix, + spaceIdSegment, + relativePath, + }; + } catch (error) { + opts.logger.debug( + `Rule "${opts.rule.id}" encountered an error while constructing the rule.url variable: ${error.message}` + ); + return; + } +}; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts new file mode 100644 index 0000000000000..02ff513c5b639 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts @@ -0,0 +1,222 @@ +/* + * 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 { RULE_SAVED_OBJECT_TYPE } from '../../..'; +import { formatActionToEnqueue } from './format_action_to_enqueue'; + +describe('formatActionToEnqueue', () => { + test('should format a rule action as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + apiKey: 'MTIzOmFiYw==', + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'default', + }) + ).toEqual({ + id: '1', + uuid: '111-111', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + spaceId: 'default', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: undefined, + typeId: 'security-rule', + }, + ], + actionTypeId: 'test', + }); + }); + + test('should format a rule action with null apiKey as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + apiKey: null, + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'default', + }) + ).toEqual({ + id: '1', + uuid: '111-111', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + spaceId: 'default', + apiKey: null, + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: undefined, + typeId: 'security-rule', + }, + ], + actionTypeId: 'test', + }); + }); + + test('should format a rule action in a custom space as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + apiKey: 'MTIzOmFiYw==', + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'my-special-space', + }) + ).toEqual({ + id: '1', + uuid: '111-111', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + spaceId: 'my-special-space', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: 'my-special-space', + typeId: 'security-rule', + }, + ], + actionTypeId: 'test', + }); + }); + + test('should format a system action as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + actionTypeId: '.test-system-action', + params: { myParams: 'test' }, + uuid: 'xxxyyyyzzzz', + }, + apiKey: 'MTIzOmFiYw==', + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'default', + }) + ).toEqual({ + id: '1', + uuid: 'xxxyyyyzzzz', + params: { myParams: 'test' }, + spaceId: 'default', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: undefined, + typeId: 'security-rule', + }, + ], + actionTypeId: '.test-system-action', + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts new file mode 100644 index 0000000000000..af560a19ab9be --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts @@ -0,0 +1,48 @@ +/* + * 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 { RuleAction, RuleSystemAction } from '@kbn/alerting-types'; +import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; +import { RULE_SAVED_OBJECT_TYPE } from '../../..'; + +interface FormatActionToEnqueueOpts { + action: RuleAction | RuleSystemAction; + apiKey: string | null; + executionId: string; + ruleConsumer: string; + ruleId: string; + ruleTypeId: string; + spaceId: string; +} + +export const formatActionToEnqueue = (opts: FormatActionToEnqueueOpts) => { + const { action, apiKey, executionId, ruleConsumer, ruleId, ruleTypeId, spaceId } = opts; + + const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; + return { + id: action.id, + uuid: action.uuid, + params: action.params, + spaceId, + apiKey: apiKey ?? null, + consumer: ruleConsumer, + source: asSavedObjectExecutionSource({ + id: ruleId, + type: RULE_SAVED_OBJECT_TYPE, + }), + executionId, + relatedSavedObjects: [ + { + id: ruleId, + type: RULE_SAVED_OBJECT_TYPE, + namespace: namespace.namespace, + typeId: ruleTypeId, + }, + ], + actionTypeId: action.actionTypeId, + }; +}; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.test.ts index 9afd0647094eb..036c49c51d1be 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.test.ts @@ -6,10 +6,10 @@ */ import { getSummarizedAlerts } from './get_summarized_alerts'; -import { alertsClientMock } from '../../alerts_client/alerts_client.mock'; -import { mockAAD } from '../fixtures'; +import { alertsClientMock } from '../../../alerts_client/alerts_client.mock'; +import { mockAAD } from '../../fixtures'; import { ALERT_UUID } from '@kbn/rule-data-utils'; -import { generateAlert } from './test_fixtures'; +import { generateAlert } from '../test_fixtures'; import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running'; const alertsClient = alertsClientMock.create(); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.ts similarity index 98% rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.ts index df667a3e20775..00e155856d946 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.ts @@ -7,13 +7,13 @@ import { ALERT_UUID } from '@kbn/rule-data-utils'; import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server'; -import { GetSummarizedAlertsParams, IAlertsClient } from '../../alerts_client/types'; +import { GetSummarizedAlertsParams, IAlertsClient } from '../../../alerts_client/types'; import { AlertInstanceContext, AlertInstanceState, CombinedSummarizedAlerts, RuleAlertData, -} from '../../types'; +} from '../../../types'; interface GetSummarizedAlertsOpts< State extends AlertInstanceState, diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts new file mode 100644 index 0000000000000..1bd78f302d00c --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { buildRuleUrl } from './build_rule_url'; +export { formatActionToEnqueue } from './format_action_to_enqueue'; +export { getSummarizedAlerts } from './get_summarized_alerts'; +export { + isSummaryAction, + isActionOnInterval, + isSummaryActionThrottled, + generateActionHash, + getSummaryActionsFromTaskState, + getSummaryActionTimeBounds, + logNumberOfFilteredAlerts, +} from './rule_action_helper'; +export { shouldScheduleAction } from './should_schedule_action'; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.test.ts index cc8a0a1b0cde5..1adb68a951351 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.test.ts @@ -7,7 +7,7 @@ import { Logger } from '@kbn/logging'; import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { RuleAction } from '../../types'; +import { RuleAction } from '../../../types'; import { generateActionHash, getSummaryActionsFromTaskState, diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.ts similarity index 99% rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.ts index 67223b0728689..c3ef79b3086d8 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.ts @@ -12,7 +12,7 @@ import { RuleAction, RuleNotifyWhenTypeValues, ThrottledActions, -} from '../../../common'; +} from '../../../../common'; export const isSummaryAction = (action?: RuleAction) => { return action?.frequency?.summary ?? false; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts new file mode 100644 index 0000000000000..7ebd65fab005d --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts @@ -0,0 +1,195 @@ +/* + * 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 { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { shouldScheduleAction } from './should_schedule_action'; +import { ruleRunMetricsStoreMock } from '../../../lib/rule_run_metrics_store.mock'; +import { ActionsCompletion } from '@kbn/alerting-state-types'; + +const logger = loggingSystemMock.create().get(); +const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); + +describe('shouldScheduleAction', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should return false if the the limit of executable actions has been reached', () => { + ruleRunMetricsStore.hasReachedTheExecutableActionsLimit.mockReturnValueOnce(true); + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => true, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(false); + + expect(ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType).toHaveBeenCalledWith({ + actionTypeId: 'test-action-type-id', + status: ActionsCompletion.PARTIAL, + }); + expect(logger.debug).toHaveBeenCalledWith( + `Rule "1" skipped scheduling action "1" because the maximum number of allowed actions has been reached.` + ); + }); + + test('should return false if the the limit of executable actions for this action type has been reached', () => { + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType.mockReturnValueOnce( + true + ); + ruleRunMetricsStore.hasConnectorTypeReachedTheLimit.mockReturnValueOnce(true); + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => true, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(false); + + expect(ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType).toHaveBeenCalledWith({ + actionTypeId: 'test-action-type-id', + status: ActionsCompletion.PARTIAL, + }); + expect(logger.debug).not.toHaveBeenCalled(); + }); + + test('should return false and log if the the limit of executable actions for this action type has been reached', () => { + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType.mockReturnValueOnce( + true + ); + ruleRunMetricsStore.hasConnectorTypeReachedTheLimit.mockReturnValueOnce(false); + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => true, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(false); + + expect(ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType).toHaveBeenCalledWith({ + actionTypeId: 'test-action-type-id', + status: ActionsCompletion.PARTIAL, + }); + expect(logger.debug).toHaveBeenCalledWith( + `Rule "1" skipped scheduling action "1" because the maximum number of allowed actions for connector type test-action-type-id has been reached.` + ); + }); + + test('should return false the action is not executable', () => { + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => false, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(false); + + expect(logger.warn).toHaveBeenCalledWith( + `Rule "1" skipped scheduling action "1" because it is disabled` + ); + }); + + test('should return true if the action is executable and no limits have been reached', () => { + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => true, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(true); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts new file mode 100644 index 0000000000000..99fa3c42ad3df --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts @@ -0,0 +1,70 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import { ActionsCompletion } from '@kbn/alerting-state-types'; +import { RuleAction, RuleSystemAction } from '@kbn/alerting-types'; +import { RuleRunMetricsStore } from '../../../lib/rule_run_metrics_store'; +import { ActionsConfigMap } from '../../../lib/get_actions_config_map'; + +interface ShouldScheduleActionOpts { + action: RuleAction | RuleSystemAction; + actionsConfigMap: ActionsConfigMap; + isActionExecutable( + actionId: string, + actionTypeId: string, + options?: { notifyUsage: boolean } + ): boolean; + logger: Logger; + ruleId: string; + ruleRunMetricsStore: RuleRunMetricsStore; +} + +export const shouldScheduleAction = (opts: ShouldScheduleActionOpts): boolean => { + const { actionsConfigMap, action, logger, ruleRunMetricsStore } = opts; + + // keep track of how many actions we want to schedule by connector type + ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(action.actionTypeId); + + if (ruleRunMetricsStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) { + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId: action.actionTypeId, + status: ActionsCompletion.PARTIAL, + }); + logger.debug( + `Rule "${opts.ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.` + ); + return false; + } + + if ( + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({ + actionTypeId: action.actionTypeId, + actionsConfigMap, + }) + ) { + if (!ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(action.actionTypeId)) { + logger.debug( + `Rule "${opts.ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${action.actionTypeId} has been reached.` + ); + } + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId: action.actionTypeId, + status: ActionsCompletion.PARTIAL, + }); + return false; + } + + if (!opts.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true })) { + logger.warn( + `Rule "${opts.ruleId}" skipped scheduling action "${action.id}" because it is disabled` + ); + return false; + } + + return true; +}; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts index 53e75245d94d0..99a693133a2a6 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts @@ -16,6 +16,12 @@ import { PerAlertActionScheduler } from './per_alert_action_scheduler'; import { getRule, getRuleType, getDefaultSchedulerContext, generateAlert } from '../test_fixtures'; import { SanitizedRuleAction } from '@kbn/alerting-types'; import { ALERT_UUID } from '@kbn/rule-data-utils'; +import { Alert } from '../../../alert'; +import { + ActionsCompletion, + AlertInstanceContext, + AlertInstanceState, +} from '@kbn/alerting-state-types'; const alertingEventLogger = alertingEventLoggerMock.create(); const actionsClient = actionsClientMock.create(); @@ -25,9 +31,10 @@ const logger = loggingSystemMock.create().get(); let ruleRunMetricsStore: RuleRunMetricsStore; const rule = getRule({ + id: 'rule-id-1', actions: [ { - id: '1', + id: 'action-1', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -41,7 +48,7 @@ const rule = getRule({ uuid: '111-111', }, { - id: '2', + id: 'action-2', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -55,7 +62,7 @@ const rule = getRule({ uuid: '222-222', }, { - id: '3', + id: 'action-3', group: 'default', actionTypeId: 'test', frequency: { summary: true, notifyWhen: 'onActiveAlert' }, @@ -84,6 +91,21 @@ const getSchedulerContext = (params = {}) => { return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; }; +const getResult = (actionId: string, alertId: string, actionUuid: string) => ({ + actionToEnqueue: { + actionTypeId: 'test', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: actionId, + uuid: actionUuid, + relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }], + source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' }, + spaceId: 'test1', + }, + actionToLog: { alertGroup: 'default', alertId, id: actionId, uuid: actionUuid, typeId: 'test' }, +}); + let clock: sinon.SinonFakeTimers; describe('Per-Alert Action Scheduler', () => { @@ -93,6 +115,7 @@ describe('Per-Alert Action Scheduler', () => { beforeEach(() => { jest.resetAllMocks(); + jest.clearAllMocks(); mockActionsPlugin.isActionTypeEnabled.mockReturnValue(true); mockActionsPlugin.isActionExecutable.mockReturnValue(true); mockActionsPlugin.getActionsClientWithRequest.mockResolvedValue(actionsClient); @@ -163,67 +186,93 @@ describe('Per-Alert Action Scheduler', () => { expect(scheduler.actions).toEqual([actions[0]]); expect(logger.error).toHaveBeenCalledTimes(1); expect(logger.error).toHaveBeenCalledWith( - `Skipping action \"2\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.` + `Skipping action \"2\" for rule \"rule-id-1\" because the rule type \"Test\" does not support alert-as-data.` ); }); - describe('generateExecutables', () => { - const newAlert1 = generateAlert({ id: 1 }); - const newAlert2 = generateAlert({ id: 2 }); - const alerts = { ...newAlert1, ...newAlert2 }; + describe('getActionsToSchedule', () => { + let newAlert1: Record< + string, + Alert + >; + let newAlert2: Record< + string, + Alert + >; + let alerts: Record< + string, + Alert + >; + + beforeEach(() => { + newAlert1 = generateAlert({ id: 1 }); + newAlert2 = generateAlert({ id: 2 }); + alerts = { ...newAlert1, ...newAlert2 }; + }); - test('should generate executable for each alert and each action', async () => { + test('should create action to schedule for each alert and each action', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule const scheduler = new PerAlertActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: rule.actions[1], alert: alerts['1'] }, - { action: rule.actions[1], alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-2', '1', '222-222'), + getResult('action-2', '2', '222-222'), ]); }); - test('should skip generating executable when alert has maintenance window', async () => { + test('should skip creating actions to schedule when alert has maintenance window', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 1 has maintenance window, so only actions for alert 2 should be scheduled const scheduler = new PerAlertActionScheduler(getSchedulerContext()); const newAlertWithMaintenanceWindow = generateAlert({ id: 1, maintenanceWindowIds: ['mw-1'], }); const alertsWithMaintenanceWindow = { ...newAlertWithMaintenanceWindow, ...newAlert2 }; - const executables = await scheduler.generateExecutables({ - alerts: alertsWithMaintenanceWindow, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithMaintenanceWindow }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(2); expect(logger.debug).toHaveBeenNthCalledWith( 1, - `no scheduling of summary actions \"1\" for rule \"1\": has active maintenance windows mw-1.` + `no scheduling of summary actions \"action-1\" for rule \"rule-id-1\": has active maintenance windows mw-1.` ); expect(logger.debug).toHaveBeenNthCalledWith( 2, - `no scheduling of summary actions \"2\" for rule \"1\": has active maintenance windows mw-1.` + `no scheduling of summary actions \"action-2\" for rule \"rule-id-1\": has active maintenance windows mw-1.` ); - expect(executables).toHaveLength(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['2'] }, - { action: rule.actions[1], alert: alerts['2'] }, + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '2', '111-111'), + getResult('action-2', '2', '222-222'), ]); }); - test('should skip generating executable when alert has invalid action group', async () => { + test('should skip creating actions to schedule when alert has invalid action group', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 1 has invalid action group, so only actions for alert 2 should be scheduled const scheduler = new PerAlertActionScheduler(getSchedulerContext()); const newAlertInvalidActionGroup = generateAlert({ id: 1, @@ -231,9 +280,8 @@ describe('Per-Alert Action Scheduler', () => { group: 'invalid', }); const alertsWithInvalidActionGroup = { ...newAlertInvalidActionGroup, ...newAlert2 }; - const executables = await scheduler.generateExecutables({ + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithInvalidActionGroup, - throttledSummaryActions: {}, }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); @@ -247,15 +295,23 @@ describe('Per-Alert Action Scheduler', () => { `Invalid action group \"invalid\" for rule \"test\".` ); - expect(executables).toHaveLength(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['2'] }, - { action: rule.actions[1], alert: alerts['2'] }, + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '2', '111-111'), + getResult('action-2', '2', '222-222'), ]); }); - test('should skip generating executable when alert has pending recovered count greater than 0 and notifyWhen is onActiveAlert', async () => { + test('should skip creating actions to schedule when alert has pending recovered count greater than 0 and notifyWhen is onActiveAlert', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 1 has a pending recovered count > 0 & notifyWhen is onActiveAlert, so only actions for alert 2 should be scheduled const scheduler = new PerAlertActionScheduler(getSchedulerContext()); const newAlertWithPendingRecoveredCount = generateAlert({ id: 1, @@ -265,23 +321,31 @@ describe('Per-Alert Action Scheduler', () => { ...newAlertWithPendingRecoveredCount, ...newAlert2, }; - const executables = await scheduler.generateExecutables({ + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithPendingRecoveredCount, - throttledSummaryActions: {}, }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); - expect(executables).toHaveLength(2); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['2'] }, - { action: rule.actions[1], alert: alerts['2'] }, + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '2', '111-111'), + getResult('action-2', '2', '222-222'), ]); }); - test('should skip generating executable when alert has pending recovered count greater than 0 and notifyWhen is onThrottleInterval', async () => { + test('should skip creating actions to schedule when alert has pending recovered count greater than 0 and notifyWhen is onThrottleInterval', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 1 has a pending recovered count > 0 & notifyWhen is onThrottleInterval, so only actions for alert 2 should be scheduled const onThrottleIntervalAction: SanitizedRuleAction = { - id: '2', + id: 'action-4', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, @@ -292,43 +356,45 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '444-444', }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] }, }); - const newAlertWithPendingRecoveredCount = generateAlert({ - id: 1, - pendingRecoveredCount: 3, - }); + const newAlertWithPendingRecoveredCount = generateAlert({ id: 1, pendingRecoveredCount: 3 }); const alertsWithPendingRecoveredCount = { ...newAlertWithPendingRecoveredCount, ...newAlert2, }; - const executables = await scheduler.generateExecutables({ + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithPendingRecoveredCount, - throttledSummaryActions: {}, }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); - expect(executables).toHaveLength(2); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['2'] }, - { action: onThrottleIntervalAction, alert: alerts['2'] }, + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '2', '111-111'), + getResult('action-4', '2', '444-444'), ]); }); - test('should skip generating executable when alert is muted', async () => { + test('should skip creating actions to schedule when alert is muted', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 2 is muted, so only actions for alert 1 should be scheduled const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, mutedInstanceIds: ['2'] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(1); @@ -336,20 +402,27 @@ describe('Per-Alert Action Scheduler', () => { 1, `skipping scheduling of actions for '2' in rule rule-label: rule is muted` ); - expect(executables).toHaveLength(2); - // @ts-expect-error private variable - expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'muted' } }); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[1], alert: alerts['1'] }, + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-2', '1', '222-222'), ]); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'muted' } }); }); - test('should skip generating executable when alert action group has not changed and notifyWhen is onActionGroupChange', async () => { + test('should skip creating actions to schedule when alert action group has not changed and notifyWhen is onActionGroupChange', async () => { const onActionGroupChangeAction: SanitizedRuleAction = { - id: '2', + id: 'action-4', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActionGroupChange', throttle: null }, @@ -360,7 +433,7 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '444-444', }; const activeAlert1 = generateAlert({ @@ -380,10 +453,7 @@ describe('Per-Alert Action Scheduler', () => { rule: { ...rule, actions: [rule.actions[0], onActionGroupChangeAction] }, }); - const executables = await scheduler.generateExecutables({ - alerts: alertsWithOngoingAlert, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithOngoingAlert }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(1); @@ -391,21 +461,28 @@ describe('Per-Alert Action Scheduler', () => { 1, `skipping scheduling of actions for '2' in rule rule-label: alert is active but action group has not changed` ); - expect(executables).toHaveLength(3); - // @ts-expect-error private variable - expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'actionGroupHasNotChanged' } }); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(3); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 3, + numberOfTriggeredActions: 3, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] }, - { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] }, - { action: onActionGroupChangeAction, alert: alertsWithOngoingAlert['1'] }, + expect(results).toHaveLength(3); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-4', '1', '444-444'), ]); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'actionGroupHasNotChanged' } }); }); - test('should skip generating executable when throttle interval has not passed and notifyWhen is onThrottleInterval', async () => { + test('should skip creating actions to schedule when throttle interval has not passed and notifyWhen is onThrottleInterval', async () => { const onThrottleIntervalAction: SanitizedRuleAction = { - id: '2', + id: 'action-5', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, @@ -416,13 +493,13 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '555-555', }; const activeAlert2 = generateAlert({ id: 2, lastScheduledActionsGroup: 'default', - throttledActions: { '222-222': { date: '1969-12-31T23:10:00.000Z' } }, + throttledActions: { '555-555': { date: '1969-12-31T23:10:00.000Z' } }, }); const alertsWithOngoingAlert = { ...newAlert1, ...activeAlert2 }; @@ -431,10 +508,7 @@ describe('Per-Alert Action Scheduler', () => { rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] }, }); - const executables = await scheduler.generateExecutables({ - alerts: alertsWithOngoingAlert, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithOngoingAlert }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(1); @@ -442,21 +516,28 @@ describe('Per-Alert Action Scheduler', () => { 1, `skipping scheduling of actions for '2' in rule rule-label: rule is throttled` ); - expect(executables).toHaveLength(3); - // @ts-expect-error private variable - expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'throttled' } }); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(3); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 3, + numberOfTriggeredActions: 3, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] }, - { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] }, - { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['1'] }, + expect(results).toHaveLength(3); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-5', '1', '555-555'), ]); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'throttled' } }); }); - test('should not skip generating executable when throttle interval has passed and notifyWhen is onThrottleInterval', async () => { + test('should not skip creating actions to schedule when throttle interval has passed and notifyWhen is onThrottleInterval', async () => { const onThrottleIntervalAction: SanitizedRuleAction = { - id: '2', + id: 'action-5', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, @@ -467,7 +548,7 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '555-555', }; const activeAlert2 = generateAlert({ @@ -482,24 +563,28 @@ describe('Per-Alert Action Scheduler', () => { rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] }, }); - const executables = await scheduler.generateExecutables({ - alerts: alertsWithOngoingAlert, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithOngoingAlert }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(4); - // @ts-expect-error private variable - expect(scheduler.skippedAlerts).toEqual({}); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] }, - { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] }, - { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['1'] }, - { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-5', '1', '555-555'), + getResult('action-5', '2', '555-555'), ]); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({}); }); test('should query for summarized alerts if useAlertDataForTemplate is true', async () => { @@ -517,7 +602,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithUseAlertDataForTemplate: SanitizedRuleAction = { - id: '1', + id: 'action-6', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -528,33 +613,36 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '666-666', useAlertDataForTemplate: true, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithUseAlertDataForTemplate, alert: alerts['1'] }, - { action: actionWithUseAlertDataForTemplate, alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-6', '1', '666-666'), + getResult('action-6', '2', '666-666'), ]); }); @@ -573,7 +661,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithUseAlertDataForTemplate: SanitizedRuleAction = { - id: '1', + id: 'action-6', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, @@ -584,34 +672,37 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '666-666', useAlertDataForTemplate: true, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', start: new Date('1969-12-31T23:00:00.000Z'), end: new Date('1970-01-01T00:00:00.000Z'), }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithUseAlertDataForTemplate, alert: alerts['1'] }, - { action: actionWithUseAlertDataForTemplate, alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-6', '1', '666-666'), + getResult('action-6', '2', '666-666'), ]); }); @@ -630,7 +721,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithAlertsFilter: SanitizedRuleAction = { - id: '1', + id: 'action-7', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -641,34 +732,37 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '777-777', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithAlertsFilter, alert: alerts['1'] }, - { action: actionWithAlertsFilter, alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-7', '1', '777-777'), + getResult('action-7', '2', '777-777'), ]); }); @@ -687,7 +781,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithAlertsFilter: SanitizedRuleAction = { - id: '1', + id: 'action-7', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '6h' }, @@ -698,39 +792,42 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '777-777', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, start: new Date('1969-12-31T18:00:00.000Z'), end: new Date('1970-01-01T00:00:00.000Z'), }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithAlertsFilter, alert: alerts['1'] }, - { action: actionWithAlertsFilter, alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-7', '1', '777-777'), + getResult('action-7', '2', '777-777'), ]); }); - test('should skip generating executable if alert does not match any alerts in summarized alerts', async () => { + test('should skip creating actions to schedule if alert does not match any alerts in summarized alerts', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { @@ -745,7 +842,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithAlertsFilter: SanitizedRuleAction = { - id: '1', + id: 'action-8', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -756,33 +853,36 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '888-888', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }); - expect(executables).toHaveLength(3); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(3); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 3, + numberOfTriggeredActions: 3, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithAlertsFilter, alert: alerts['1'] }, + expect(results).toHaveLength(3); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-8', '1', '888-888'), ]); }); @@ -801,7 +901,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithAlertsFilter: SanitizedRuleAction = { - id: '1', + id: 'action-9', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -812,38 +912,168 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '999-999', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); + + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-9', '1', '999-999'), + getResult('action-9', '2', '999-999'), + ]); expect(alerts['1'].getAlertAsData()).not.toBeUndefined(); expect(alerts['2'].getAlertAsData()).not.toBeUndefined(); + }); + + test('should skip creating actions to schedule if overall max actions limit exceeded', async () => { + const defaultContext = getSchedulerContext(); + const scheduler = new PerAlertActionScheduler({ + ...defaultContext, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 3 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithAlertsFilter, alert: alerts['1'] }, - { action: actionWithAlertsFilter, alert: alerts['2'] }, + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 3, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "action-2" because the maximum number of allowed actions has been reached.` + ); + + expect(results).toHaveLength(3); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-2', '1', '222-222'), ]); }); + + test('should skip creating actions to schedule if connector type max actions limit exceeded', async () => { + const defaultContext = getSchedulerContext(); + const scheduler = new PerAlertActionScheduler({ + ...defaultContext, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1000 }, + test: { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "action-1" because the maximum number of allowed actions for connector type test has been reached.` + ); + + expect(results).toHaveLength(1); + expect(results).toEqual([getResult('action-1', '1', '111-111')]); + }); + + test('should correctly update last scheduled actions for alert when action is "onActiveAlert"', async () => { + const alert = new Alert('1', { + state: { test: true }, + meta: {}, + }); + alert.scheduleActions('default'); + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0]] }, + }); + + expect(alert.getLastScheduledActions()).toBeUndefined(); + expect(alert.hasScheduledActions()).toBe(true); + await scheduler.getActionsToSchedule({ alerts: { '1': alert } }); + + expect(alert.getLastScheduledActions()).toEqual({ + date: '1970-01-01T00:00:00.000Z', + group: 'default', + }); + expect(alert.hasScheduledActions()).toBe(false); + }); + + test('should correctly update last scheduled actions for alert', async () => { + const alert = new Alert('1', { + state: { test: true }, + meta: {}, + }); + alert.scheduleActions('default'); + const onThrottleIntervalAction: SanitizedRuleAction = { + id: 'action-4', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }; + + expect(alert.getLastScheduledActions()).toBeUndefined(); + expect(alert.hasScheduledActions()).toBe(true); + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [onThrottleIntervalAction] }, + }); + + await scheduler.getActionsToSchedule({ alerts: { '1': alert } }); + + expect(alert.getLastScheduledActions()).toEqual({ + date: '1970-01-01T00:00:00.000Z', + group: 'default', + actions: { '222-222': { date: '1970-01-01T00:00:00.000Z' } }, + }); + expect(alert.hasScheduledActions()).toBe(false); + }); }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts index 602d3c31688c1..70e6992b4a69a 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts @@ -12,19 +12,24 @@ import { RuleTypeState, RuleAlertData, parseDuration } from '../../../../common' import { GetSummarizedAlertsParams } from '../../../alerts_client/types'; import { AlertHit } from '../../../types'; import { Alert } from '../../../alert'; -import { getSummarizedAlerts } from '../get_summarized_alerts'; import { + buildRuleUrl, + formatActionToEnqueue, generateActionHash, + getSummarizedAlerts, isActionOnInterval, isSummaryAction, logNumberOfFilteredAlerts, -} from '../rule_action_helper'; + shouldScheduleAction, +} from '../lib'; import { ActionSchedulerOptions, - Executable, - GenerateExecutablesOpts, + ActionsToSchedule, + GetActionsToScheduleOpts, IActionScheduler, } from '../types'; +import { TransformActionParamsOptions, transformActionParams } from '../../transform_action_params'; +import { injectActionParams } from '../../inject_action_params'; enum Reasons { MUTED = 'muted', @@ -90,12 +95,16 @@ export class PerAlertActionScheduler< return 2; } - public async generateExecutables({ + public async getActionsToSchedule({ alerts, - }: GenerateExecutablesOpts): Promise< - Array> + }: GetActionsToScheduleOpts): Promise< + ActionsToSchedule[] > { - const executables = []; + const executables: Array<{ + action: RuleAction; + alert: Alert; + }> = []; + const results: ActionsToSchedule[] = []; const alertsArray = Object.entries(alerts); for (const action of this.actions) { @@ -104,7 +113,7 @@ export class PerAlertActionScheduler< if (action.useAlertDataForTemplate || action.alertsFilter) { const optionsBase = { spaceId: this.context.taskInstance.params.spaceId, - ruleId: this.context.taskInstance.params.alertId, + ruleId: this.context.rule.id, excludedAlertInstanceIds: this.context.rule.mutedInstanceIds, alertsFilter: action.alertsFilter, }; @@ -135,7 +144,7 @@ export class PerAlertActionScheduler< if (alertMaintenanceWindowIds.length !== 0) { this.context.logger.debug( `no scheduling of summary actions "${action.id}" for rule "${ - this.context.taskInstance.params.alertId + this.context.rule.id }": has active maintenance windows ${alertMaintenanceWindowIds.join(', ')}.` ); continue; @@ -185,7 +194,110 @@ export class PerAlertActionScheduler< } } - return executables; + if (executables.length === 0) return []; + + this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); + + const ruleUrl = buildRuleUrl({ + getViewInAppRelativeUrl: this.context.ruleType.getViewInAppRelativeUrl, + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + logger: this.context.logger, + rule: this.context.rule, + spaceId: this.context.taskInstance.params.spaceId, + }); + + for (const { action, alert } of executables) { + const { actionTypeId } = action; + + if ( + !shouldScheduleAction({ + action, + actionsConfigMap: this.context.taskRunnerContext.actionsConfigMap, + isActionExecutable: this.context.taskRunnerContext.actionsPlugin.isActionExecutable, + logger: this.context.logger, + ruleId: this.context.rule.id, + ruleRunMetricsStore: this.context.ruleRunMetricsStore, + }) + ) { + continue; + } + + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActions(); + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType( + actionTypeId + ); + + const actionGroup = action.group as ActionGroupIds; + const transformActionParamsOptions: TransformActionParamsOptions = { + actionsPlugin: this.context.taskRunnerContext.actionsPlugin, + alertId: this.context.rule.id, + alertType: this.context.ruleType.id, + actionTypeId: action.actionTypeId, + alertName: this.context.rule.name, + spaceId: this.context.taskInstance.params.spaceId, + tags: this.context.rule.tags, + alertInstanceId: alert.getId(), + alertUuid: alert.getUuid(), + alertActionGroup: actionGroup, + alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!, + context: alert.getContext(), + actionId: action.id, + state: alert.getState(), + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + alertParams: this.context.rule.params, + actionParams: action.params, + flapping: alert.getFlapping(), + ruleUrl: ruleUrl?.absoluteUrl, + }; + + if (alert.isAlertAsData()) { + transformActionParamsOptions.aadAlert = alert.getAlertAsData(); + } + + const actionToRun = { + ...action, + params: injectActionParams({ + actionTypeId: action.actionTypeId, + ruleUrl, + ruleName: this.context.rule.name, + actionParams: transformActionParams(transformActionParamsOptions), + }), + }; + + results.push({ + actionToEnqueue: formatActionToEnqueue({ + action: actionToRun, + apiKey: this.context.apiKey, + executionId: this.context.executionId, + ruleConsumer: this.context.ruleConsumer, + ruleId: this.context.rule.id, + ruleTypeId: this.context.ruleType.id, + spaceId: this.context.taskInstance.params.spaceId, + }), + actionToLog: { + id: action.id, + uuid: action.uuid, + typeId: action.actionTypeId, + alertId: alert.getId(), + alertGroup: action.group, + }, + }); + + if (!this.isRecoveredAlert(actionGroup)) { + if (isActionOnInterval(action)) { + alert.updateLastScheduledActions( + action.group as ActionGroupIds, + generateActionHash(action), + action.uuid + ); + } else { + alert.updateLastScheduledActions(action.group as ActionGroupIds); + } + alert.unscheduleActions(); + } + } + + return results; } private isAlertMuted(alertId: string) { diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts index 600dd0e1951d5..fc810fc4ef34c 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts @@ -20,6 +20,8 @@ import { getErrorSource, TaskErrorSource, } from '@kbn/task-manager-plugin/server/task_running/errors'; +import { CombinedSummarizedAlerts } from '../../../types'; +import { ActionsCompletion } from '@kbn/alerting-state-types'; const alertingEventLogger = alertingEventLoggerMock.create(); const actionsClient = actionsClientMock.create(); @@ -29,9 +31,10 @@ const logger = loggingSystemMock.create().get(); let ruleRunMetricsStore: RuleRunMetricsStore; const rule = getRule({ + id: 'rule-id-1', actions: [ { - id: '1', + id: 'action-1', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -45,7 +48,7 @@ const rule = getRule({ uuid: '111-111', }, { - id: '2', + id: 'action-2', group: 'default', actionTypeId: 'test', frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, @@ -59,7 +62,7 @@ const rule = getRule({ uuid: '222-222', }, { - id: '3', + id: 'action-3', group: 'default', actionTypeId: 'test', frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, @@ -88,6 +91,30 @@ const getSchedulerContext = (params = {}) => { return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; }; +const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({ + actionToEnqueue: { + actionTypeId: 'test', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: actionId, + uuid: actionUuid, + relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }], + source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' }, + spaceId: 'test1', + }, + actionToLog: { + alertSummary: { + new: summary.new.count, + ongoing: summary.ongoing.count, + recovered: summary.recovered.count, + }, + id: actionId, + uuid: actionUuid, + typeId: 'test', + }, +}); + let clock: sinon.SinonFakeTimers; describe('Summary Action Scheduler', () => { @@ -127,21 +154,21 @@ describe('Summary Action Scheduler', () => { expect(logger.error).toHaveBeenCalledTimes(2); expect(logger.error).toHaveBeenNthCalledWith( 1, - `Skipping action \"2\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.` + `Skipping action \"action-2\" for rule \"rule-id-1\" because the rule type \"Test\" does not support alert-as-data.` ); expect(logger.error).toHaveBeenNthCalledWith( 2, - `Skipping action \"3\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.` + `Skipping action \"action-3\" for rule \"rule-id-1\" because the rule type \"Test\" does not support alert-as-data.` ); }); - describe('generateExecutables', () => { + describe('getActionsToSchedule', () => { const newAlert1 = generateAlert({ id: 1 }); const newAlert2 = generateAlert({ id: 2 }); const alerts = { ...newAlert1, ...newAlert2 }; const summaryActionWithAlertFilter: RuleAction = { - id: '2', + id: 'action-3', group: 'default', actionTypeId: 'test', frequency: { @@ -157,11 +184,11 @@ describe('Summary Action Scheduler', () => { 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } }, - uuid: '222-222', + uuid: '333-333', }; const summaryActionWithThrottle: RuleAction = { - id: '2', + id: 'action-4', group: 'default', actionTypeId: 'test', frequency: { @@ -176,10 +203,10 @@ describe('Summary Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '444-444', }; - test('should generate executable for summary action when summary action is per rule run', async () => { + test('should create action to schedule for summary action when summary action is per rule run', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 2, data: [mockAAD, mockAAD] }, @@ -188,37 +215,43 @@ describe('Summary Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const throttledSummaryActions = {}; const scheduler = new SummaryActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; - expect(executables).toEqual([ - { action: rule.actions[1], summarizedAlerts: finalSummary }, - { action: rule.actions[2], summarizedAlerts: finalSummary }, + expect(results).toEqual([ + getResult('action-2', '222-222', finalSummary), + getResult('action-3', '333-333', finalSummary), ]); }); - test('should generate executable for summary action when summary action has alertsFilter', async () => { + test('should create actions to schedule for summary action when summary action has alertsFilter', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 2, data: [mockAAD, mockAAD] }, @@ -232,30 +265,34 @@ describe('Summary Action Scheduler', () => { rule: { ...rule, actions: [summaryActionWithAlertFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } }, }); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; - expect(executables).toEqual([ - { action: summaryActionWithAlertFilter, summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('action-3', '333-333', finalSummary)]); }); - test('should generate executable for summary action when summary action is throttled with no throttle history', async () => { + test('should create actions to schedule for summary action when summary action is throttled with no throttle history', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 2, data: [mockAAD, mockAAD] }, @@ -269,48 +306,52 @@ describe('Summary Action Scheduler', () => { rule: { ...rule, actions: [summaryActionWithThrottle] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({ '444-444': { date: '1970-01-01T00:00:00.000Z' } }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', start: new Date('1969-12-31T00:00:00.000Z'), end: new Date(), }); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; - expect(executables).toEqual([ - { action: summaryActionWithThrottle, summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('action-4', '444-444', finalSummary)]); }); - test('should skip generating executable for summary action when summary action is throttled', async () => { + test('should skip creating actions to schedule for summary action when summary action is throttled', async () => { const scheduler = new SummaryActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [summaryActionWithThrottle] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: { - '222-222': { date: '1969-12-31T13:00:00.000Z' }, - }, - }); + const throttledSummaryActions = { '444-444': { date: '1969-12-31T13:00:00.000Z' } }; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({ '444-444': { date: '1969-12-31T13:00:00.000Z' } }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith( - `skipping scheduling the action 'test:2', summary action is still being throttled` + `skipping scheduling the action 'test:action-4', summary action is still being throttled` ); - expect(executables).toHaveLength(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(0); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0); + + expect(results).toHaveLength(0); }); test('should remove new alerts from summary if suppressed by maintenance window', async () => { @@ -332,22 +373,21 @@ describe('Summary Action Scheduler', () => { alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const scheduler = new SummaryActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); expect(logger.debug).toHaveBeenCalledTimes(2); @@ -360,7 +400,14 @@ describe('Summary Action Scheduler', () => { `(1) alert has been filtered out for: test:333-333` ); - expect(executables).toHaveLength(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); const finalSummary = { all: { count: 1, data: [newAADAlerts[1]] }, @@ -368,13 +415,13 @@ describe('Summary Action Scheduler', () => { ongoing: { count: 0, data: [] }, recovered: { count: 0, data: [] }, }; - expect(executables).toEqual([ - { action: rule.actions[1], summarizedAlerts: finalSummary }, - { action: rule.actions[2], summarizedAlerts: finalSummary }, + expect(results).toEqual([ + getResult('action-2', '222-222', finalSummary), + getResult('action-3', '333-333', finalSummary), ]); }); - test('should generate executable for summary action and log when alerts have been filtered out by action condition', async () => { + test('should create alerts to schedule for summary action and log when alerts have been filtered out by action condition', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 1, data: [mockAAD] }, @@ -388,33 +435,37 @@ describe('Summary Action Scheduler', () => { rule: { ...rule, actions: [summaryActionWithAlertFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } }, }); expect(logger.debug).toHaveBeenCalledTimes(1); expect(logger.debug).toHaveBeenCalledWith( - `(1) alert has been filtered out for: test:222-222` + `(1) alert has been filtered out for: test:333-333` ); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { ...summarizedAlerts, all: { count: 1, data: [mockAAD] } }; - expect(executables).toEqual([ - { action: summaryActionWithAlertFilter, summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('action-3', '333-333', finalSummary)]); }); - test('should skip generating executable for summary action when no alerts found', async () => { + test('should skip creating actions to schedule for summary action when no alerts found', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 0, data: [] }, @@ -428,22 +479,23 @@ describe('Summary Action Scheduler', () => { rule: { ...rule, actions: [summaryActionWithThrottle] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', start: new Date('1969-12-31T00:00:00.000Z'), end: new Date(), }); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(0); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0); + expect(results).toHaveLength(0); }); test('should throw framework error if getSummarizedAlerts throws error', async () => { @@ -455,14 +507,117 @@ describe('Summary Action Scheduler', () => { const scheduler = new SummaryActionScheduler(getSchedulerContext()); try { - await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions: {} }); } catch (err) { expect(err.message).toEqual(`no alerts for you`); expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK); } }); + + test('should skip creating actions to schedule if overall max actions limit exceeded', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SummaryActionScheduler({ + ...defaultContext, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions: {} }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "action-3" because the maximum number of allowed actions has been reached.` + ); + + expect(results).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([getResult('action-2', '222-222', finalSummary)]); + }); + + test('should skip creating actions to schedule if connector type max actions limit exceeded', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SummaryActionScheduler({ + ...defaultContext, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1000 }, + test: { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions: {} }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "action-3" because the maximum number of allowed actions for connector type test has been reached.` + ); + + expect(results).toHaveLength(1); + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([getResult('action-2', '222-222', finalSummary)]); + }); }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts index 9b67c37e6216e..050eea352f0d5 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts @@ -8,21 +8,28 @@ import { AlertInstanceState, AlertInstanceContext } from '@kbn/alerting-state-types'; import { RuleAction, RuleTypeParams } from '@kbn/alerting-types'; import { compact } from 'lodash'; +import { CombinedSummarizedAlerts } from '../../../types'; import { RuleTypeState, RuleAlertData, parseDuration } from '../../../../common'; import { GetSummarizedAlertsParams } from '../../../alerts_client/types'; -import { getSummarizedAlerts } from '../get_summarized_alerts'; import { + buildRuleUrl, + formatActionToEnqueue, + getSummarizedAlerts, + getSummaryActionTimeBounds, isActionOnInterval, isSummaryAction, isSummaryActionThrottled, logNumberOfFilteredAlerts, -} from '../rule_action_helper'; + shouldScheduleAction, +} from '../lib'; import { ActionSchedulerOptions, - Executable, - GenerateExecutablesOpts, + ActionsToSchedule, + GetActionsToScheduleOpts, IActionScheduler, } from '../types'; +import { injectActionParams } from '../../inject_action_params'; +import { transformSummaryActionParams } from '../../transform_action_params'; export class SummaryActionScheduler< Params extends RuleTypeParams, @@ -73,13 +80,18 @@ export class SummaryActionScheduler< return 0; } - public async generateExecutables({ + public async getActionsToSchedule({ alerts, throttledSummaryActions, - }: GenerateExecutablesOpts): Promise< - Array> + }: GetActionsToScheduleOpts): Promise< + ActionsToSchedule[] > { - const executables = []; + const executables: Array<{ + action: RuleAction; + summarizedAlerts: CombinedSummarizedAlerts; + }> = []; + const results: ActionsToSchedule[] = []; + for (const action of this.actions) { if ( // if summary action is throttled, we won't send any notifications @@ -88,7 +100,7 @@ export class SummaryActionScheduler< const actionHasThrottleInterval = isActionOnInterval(action); const optionsBase = { spaceId: this.context.taskInstance.params.spaceId, - ruleId: this.context.taskInstance.params.alertId, + ruleId: this.context.rule.id, excludedAlertInstanceIds: this.context.rule.mutedInstanceIds, alertsFilter: action.alertsFilter, }; @@ -122,6 +134,95 @@ export class SummaryActionScheduler< } } - return executables; + if (executables.length === 0) return []; + + this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); + + for (const { action, summarizedAlerts } of executables) { + const { actionTypeId } = action; + + if ( + !shouldScheduleAction({ + action, + actionsConfigMap: this.context.taskRunnerContext.actionsConfigMap, + isActionExecutable: this.context.taskRunnerContext.actionsPlugin.isActionExecutable, + logger: this.context.logger, + ruleId: this.context.rule.id, + ruleRunMetricsStore: this.context.ruleRunMetricsStore, + }) + ) { + continue; + } + + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActions(); + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType( + actionTypeId + ); + + if (isActionOnInterval(action) && throttledSummaryActions) { + throttledSummaryActions[action.uuid!] = { date: new Date().toISOString() }; + } + + const { start, end } = getSummaryActionTimeBounds( + action, + this.context.rule.schedule, + this.context.previousStartedAt + ); + + const ruleUrl = buildRuleUrl({ + end, + getViewInAppRelativeUrl: this.context.ruleType.getViewInAppRelativeUrl, + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + logger: this.context.logger, + rule: this.context.rule, + spaceId: this.context.taskInstance.params.spaceId, + start, + }); + + const actionToRun = { + ...action, + params: injectActionParams({ + actionTypeId: action.actionTypeId, + ruleUrl, + ruleName: this.context.rule.name, + actionParams: transformSummaryActionParams({ + alerts: summarizedAlerts, + rule: this.context.rule, + ruleTypeId: this.context.ruleType.id, + actionId: action.id, + actionParams: action.params, + spaceId: this.context.taskInstance.params.spaceId, + actionsPlugin: this.context.taskRunnerContext.actionsPlugin, + actionTypeId: action.actionTypeId, + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + ruleUrl: ruleUrl?.absoluteUrl, + }), + }), + }; + + results.push({ + actionToEnqueue: formatActionToEnqueue({ + action: actionToRun, + apiKey: this.context.apiKey, + executionId: this.context.executionId, + ruleConsumer: this.context.ruleConsumer, + ruleId: this.context.rule.id, + ruleTypeId: this.context.ruleType.id, + spaceId: this.context.taskInstance.params.spaceId, + }), + actionToLog: { + id: action.id, + uuid: action.uuid, + typeId: action.actionTypeId, + alertSummary: { + new: summarizedAlerts.new.count, + ongoing: summarizedAlerts.ongoing.count, + recovered: summarizedAlerts.recovered.count, + }, + }, + }); + } + + return results; } } diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts index fd4db6ce34678..28bf58a30c689 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts @@ -12,6 +12,12 @@ import { alertsClientMock } from '../../../alerts_client/alerts_client.mock'; import { alertingEventLoggerMock } from '../../../lib/alerting_event_logger/alerting_event_logger.mock'; import { RuleRunMetricsStore } from '../../../lib/rule_run_metrics_store'; import { mockAAD } from '../../fixtures'; +import { Alert } from '../../../alert'; +import { + ActionsCompletion, + AlertInstanceContext, + AlertInstanceState, +} from '@kbn/alerting-state-types'; import { getRule, getRuleType, getDefaultSchedulerContext, generateAlert } from '../test_fixtures'; import { SystemActionScheduler } from './system_action_scheduler'; import { ALERT_UUID } from '@kbn/rule-data-utils'; @@ -19,6 +25,8 @@ import { getErrorSource, TaskErrorSource, } from '@kbn/task-manager-plugin/server/task_running/errors'; +import { CombinedSummarizedAlerts } from '../../../types'; +import { schema } from '@kbn/config-schema'; const alertingEventLogger = alertingEventLoggerMock.create(); const actionsClient = actionsClientMock.create(); @@ -28,12 +36,13 @@ const logger = loggingSystemMock.create().get(); let ruleRunMetricsStore: RuleRunMetricsStore; const rule = getRule({ + id: 'rule-id-1', systemActions: [ { - id: '1', + id: 'system-action-1', actionTypeId: '.test-system-action', params: { myParams: 'test' }, - uui: 'test', + uuid: 'xxx-xxx', }, ], }); @@ -46,11 +55,43 @@ const defaultSchedulerContext = getDefaultSchedulerContext( alertsClient ); +const actionsParams = { myParams: 'test' }; +const buildActionParams = jest.fn().mockReturnValue({ ...actionsParams, foo: 'bar' }); +defaultSchedulerContext.taskRunnerContext.connectorAdapterRegistry.register({ + connectorTypeId: '.test-system-action', + ruleActionParamsSchema: schema.object({}), + buildActionParams, +}); + // @ts-ignore const getSchedulerContext = (params = {}) => { return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; }; +const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({ + actionToEnqueue: { + actionTypeId: '.test-system-action', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: actionId, + uuid: actionUuid, + relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }], + source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' }, + spaceId: 'test1', + }, + actionToLog: { + alertSummary: { + new: summary.new.count, + ongoing: summary.ongoing.count, + recovered: summary.recovered.count, + }, + id: actionId, + uuid: actionUuid, + typeId: '.test-system-action', + }, +}); + let clock: sinon.SinonFakeTimers; describe('System Action Scheduler', () => { @@ -88,13 +129,29 @@ describe('System Action Scheduler', () => { expect(scheduler.actions).toHaveLength(0); }); - describe('generateExecutables', () => { - const newAlert1 = generateAlert({ id: 1 }); - const newAlert2 = generateAlert({ id: 2 }); - const alerts = { ...newAlert1, ...newAlert2 }; + describe('getActionsToSchedule', () => { + let newAlert1: Record< + string, + Alert + >; + let newAlert2: Record< + string, + Alert + >; + let alerts: Record< + string, + Alert + >; - test('should generate executable for each system action', async () => { + beforeEach(() => { + newAlert1 = generateAlert({ id: 1 }); + newAlert2 = generateAlert({ id: 2 }); + alerts = { ...newAlert1, ...newAlert2 }; + }); + + test('should create actions to schedule for each system action', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { new: { count: 2, data: [mockAAD, mockAAD] }, ongoing: { count: 0, data: [] }, @@ -103,25 +160,27 @@ describe('System Action Scheduler', () => { alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const scheduler = new SystemActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; - expect(executables).toEqual([ - { action: rule.systemActions?.[0], summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]); }); test('should remove new alerts from summary if suppressed by maintenance window', async () => { @@ -141,22 +200,26 @@ describe('System Action Scheduler', () => { recovered: { count: 0, data: [] }, }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); - const scheduler = new SystemActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const scheduler = new SystemActionScheduler(getSchedulerContext()); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { all: { count: 1, data: [newAADAlerts[1]] }, @@ -164,12 +227,10 @@ describe('System Action Scheduler', () => { ongoing: { count: 0, data: [] }, recovered: { count: 0, data: [] }, }; - expect(executables).toEqual([ - { action: rule.systemActions?.[0], summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]); }); - test('should skip generating executable for summary action when no alerts found', async () => { + test('should skip creating actions to schedule for summary action when no alerts found', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 0, data: [] }, @@ -179,21 +240,20 @@ describe('System Action Scheduler', () => { alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const scheduler = new SystemActionScheduler(getSchedulerContext()); - - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); - expect(executables).toHaveLength(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(0); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0); + + expect(results).toHaveLength(0); }); test('should throw framework error if getSummarizedAlerts throws error', async () => { @@ -205,14 +265,175 @@ describe('System Action Scheduler', () => { const scheduler = new SystemActionScheduler(getSchedulerContext()); try { - await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + await scheduler.getActionsToSchedule({ alerts }); } catch (err) { expect(err.message).toEqual(`no alerts for you`); expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK); } }); + + test('should skip creating actions to schedule if overall max actions limit exceeded', async () => { + const anotherSystemAction = { + id: 'system-action-1', + actionTypeId: '.test-system-action', + params: { myParams: 'foo' }, + uuid: 'yyy-yyy', + }; + + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SystemActionScheduler({ + ...defaultContext, + rule: { ...rule, systemActions: [rule.systemActions?.[0]!, anotherSystemAction] }, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "system-action-1" because the maximum number of allowed actions has been reached.` + ); + + expect(results).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]); + }); + + test('should skip creating actions to schedule if connector type max actions limit exceeded', async () => { + const anotherSystemAction = { + id: 'system-action-1', + actionTypeId: '.test-system-action', + params: { myParams: 'foo' }, + uuid: 'yyy-yyy', + }; + + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SystemActionScheduler({ + ...defaultContext, + rule: { ...rule, systemActions: [rule.systemActions?.[0]!, anotherSystemAction] }, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1000 }, + '.test-system-action': { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "system-action-1" because the maximum number of allowed actions for connector type .test-system-action has been reached.` + ); + + expect(results).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]); + }); + + test('should skip creating actions to schedule if no connector adapter exists for connector type', async () => { + const differentSystemAction = { + id: 'different-action-1', + actionTypeId: '.test-bad-system-action', + params: { myParams: 'foo' }, + uuid: 'zzz-zzz', + }; + + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SystemActionScheduler({ + ...defaultContext, + rule: { ...rule, systemActions: [differentSystemAction] }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0); + + expect(logger.warn).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling system action "different-action-1" because no connector adapter is configured` + ); + + expect(results).toHaveLength(0); + }); }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts index b923baf8fbf38..0c5cceb0f0a52 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts @@ -7,13 +7,19 @@ import { AlertInstanceState, AlertInstanceContext } from '@kbn/alerting-state-types'; import { RuleSystemAction, RuleTypeParams } from '@kbn/alerting-types'; +import { CombinedSummarizedAlerts } from '../../../types'; import { RuleTypeState, RuleAlertData } from '../../../../common'; import { GetSummarizedAlertsParams } from '../../../alerts_client/types'; -import { getSummarizedAlerts } from '../get_summarized_alerts'; +import { + buildRuleUrl, + formatActionToEnqueue, + getSummarizedAlerts, + shouldScheduleAction, +} from '../lib'; import { ActionSchedulerOptions, - Executable, - GenerateExecutablesOpts, + ActionsToSchedule, + GetActionsToScheduleOpts, IActionScheduler, } from '../types'; @@ -53,14 +59,19 @@ export class SystemActionScheduler< return 1; } - public async generateExecutables( - _: GenerateExecutablesOpts - ): Promise>> { - const executables = []; + public async getActionsToSchedule( + _: GetActionsToScheduleOpts + ): Promise { + const executables: Array<{ + action: RuleSystemAction; + summarizedAlerts: CombinedSummarizedAlerts; + }> = []; + const results: ActionsToSchedule[] = []; + for (const action of this.actions) { const options: GetSummarizedAlertsParams = { spaceId: this.context.taskInstance.params.spaceId, - ruleId: this.context.taskInstance.params.alertId, + ruleId: this.context.rule.id, excludedAlertInstanceIds: this.context.rule.mutedInstanceIds, executionUuid: this.context.executionId, }; @@ -75,6 +86,95 @@ export class SystemActionScheduler< } } - return executables; + if (executables.length === 0) return []; + + this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); + + const ruleUrl = buildRuleUrl({ + getViewInAppRelativeUrl: this.context.ruleType.getViewInAppRelativeUrl, + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + logger: this.context.logger, + rule: this.context.rule, + spaceId: this.context.taskInstance.params.spaceId, + }); + + for (const { action, summarizedAlerts } of executables) { + const { actionTypeId } = action; + + if ( + !shouldScheduleAction({ + action, + actionsConfigMap: this.context.taskRunnerContext.actionsConfigMap, + isActionExecutable: this.context.taskRunnerContext.actionsPlugin.isActionExecutable, + logger: this.context.logger, + ruleId: this.context.rule.id, + ruleRunMetricsStore: this.context.ruleRunMetricsStore, + }) + ) { + continue; + } + + const hasConnectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.has( + action.actionTypeId + ); + + // System actions without an adapter cannot be executed + if (!hasConnectorAdapter) { + this.context.logger.warn( + `Rule "${this.context.rule.id}" skipped scheduling system action "${action.id}" because no connector adapter is configured` + ); + + continue; + } + + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActions(); + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType( + actionTypeId + ); + + const connectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.get( + action.actionTypeId + ); + + const connectorAdapterActionParams = connectorAdapter.buildActionParams({ + alerts: summarizedAlerts, + rule: { + id: this.context.rule.id, + tags: this.context.rule.tags, + name: this.context.rule.name, + consumer: this.context.rule.consumer, + producer: this.context.ruleType.producer, + }, + ruleUrl: ruleUrl?.absoluteUrl, + spaceId: this.context.taskInstance.params.spaceId, + params: action.params, + }); + + const actionToRun = Object.assign(action, { params: connectorAdapterActionParams }); + + results.push({ + actionToEnqueue: formatActionToEnqueue({ + action: actionToRun, + apiKey: this.context.apiKey, + executionId: this.context.executionId, + ruleConsumer: this.context.ruleConsumer, + ruleId: this.context.rule.id, + ruleTypeId: this.context.ruleType.id, + spaceId: this.context.taskInstance.params.spaceId, + }), + actionToLog: { + id: action.id, + uuid: action.uuid, + typeId: action.actionTypeId, + alertSummary: { + new: summarizedAlerts.new.count, + ongoing: summarizedAlerts.ongoing.count, + recovered: summarizedAlerts.recovered.count, + }, + }, + }); + } + + return results; } } diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts index efcb51fcb2698..b90ffb88d541b 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts @@ -8,6 +8,7 @@ import type { Logger } from '@kbn/core/server'; import { PublicMethodsOf } from '@kbn/utility-types'; import { ActionsClient } from '@kbn/actions-plugin/server/actions_client'; +import { ExecuteOptions as EnqueueExecutionOptions } from '@kbn/actions-plugin/server/create_execute_function'; import { IAlertsClient } from '../../alerts_client/types'; import { Alert } from '../../alert'; import { @@ -24,7 +25,10 @@ import { import { NormalizedRuleType } from '../../rule_type_registry'; import { CombinedSummarizedAlerts, RawRule } from '../../types'; import { RuleRunMetricsStore } from '../../lib/rule_run_metrics_store'; -import { AlertingEventLogger } from '../../lib/alerting_event_logger/alerting_event_logger'; +import { + ActionOpts, + AlertingEventLogger, +} from '../../lib/alerting_event_logger/alerting_event_logger'; import { RuleTaskInstance, TaskRunnerContext } from '../types'; export interface ActionSchedulerOptions< @@ -80,14 +84,19 @@ export type Executable< } ); -export interface GenerateExecutablesOpts< +export interface GetActionsToScheduleOpts< State extends AlertInstanceState, Context extends AlertInstanceContext, ActionGroupIds extends string, RecoveryActionGroupId extends string > { alerts: Record>; - throttledSummaryActions: ThrottledActions; + throttledSummaryActions?: ThrottledActions; +} + +export interface ActionsToSchedule { + actionToEnqueue: EnqueueExecutionOptions; + actionToLog: ActionOpts; } export interface IActionScheduler< @@ -97,9 +106,9 @@ export interface IActionScheduler< RecoveryActionGroupId extends string > { get priority(): number; - generateExecutables( - opts: GenerateExecutablesOpts - ): Promise>>; + getActionsToSchedule( + opts: GetActionsToScheduleOpts + ): Promise; } export interface RuleUrl { diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index ae8eccfcb1f86..17fb621f2d404 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -20,7 +20,7 @@ import { import { getDefaultMonitoring } from '../lib/monitoring'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { RawRule } from '../types'; +import { AlertHit, RawRule } from '../types'; import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; interface GeneratorParams { @@ -342,9 +342,10 @@ export const generateAlertOpts = ({ }; }; -export const generateActionOpts = ({ id, alertGroup, alertId }: GeneratorParams = {}) => ({ +export const generateActionOpts = ({ id, alertGroup, alertId, uuid }: GeneratorParams = {}) => ({ id: id ?? '1', typeId: 'action', + uuid: uuid ?? '111-111', alertId: alertId ?? '1', alertGroup: alertGroup ?? 'default', }); @@ -396,11 +397,13 @@ export const generateRunnerResult = ({ export const generateEnqueueFunctionInput = ({ id = '1', + uuid = '111-111', isBulk = false, isResolved, foo, actionTypeId, }: { + uuid?: string; id: string; isBulk?: boolean; isResolved?: boolean; @@ -412,6 +415,7 @@ export const generateEnqueueFunctionInput = ({ apiKey: 'MTIzOmFiYw==', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', id, + uuid, params: { ...(isResolved !== undefined ? { isResolved } : {}), ...(foo !== undefined ? { foo } : {}), @@ -497,4 +501,4 @@ export const mockAAD = { }, }, }, -}; +} as unknown as AlertHit; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 438ffb3685e2a..956f4a26dbd75 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -1419,7 +1419,7 @@ describe('Task Runner', () => { expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, generateActionOpts({})); expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith( 2, - generateActionOpts({ id: '2', alertId: '2', alertGroup: 'recovered' }) + generateActionOpts({ id: '2', alertId: '2', alertGroup: 'recovered', uuid: '222-222' }) ); expect(enqueueFunction).toHaveBeenCalledTimes(isBulk ? 1 : 2); @@ -1427,7 +1427,12 @@ describe('Task Runner', () => { isBulk ? [ generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }), - generateEnqueueFunctionInput({ isBulk: false, id: '2', isResolved: true }), + generateEnqueueFunctionInput({ + isBulk: false, + id: '2', + isResolved: true, + uuid: '222-222', + }), ] : generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }) ); @@ -1644,7 +1649,12 @@ describe('Task Runner', () => { isBulk ? [ generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }), - generateEnqueueFunctionInput({ isBulk: false, id: '2', isResolved: true }), + generateEnqueueFunctionInput({ + isBulk: false, + id: '2', + isResolved: true, + uuid: '222-222', + }), ] : generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }) ); @@ -2899,26 +2909,31 @@ describe('Task Runner', () => { { group: 'default', id: '1', + uuid: '111-111', actionTypeId: 'action', }, { group: 'default', id: '2', + uuid: '222-222', actionTypeId: 'action', }, { group: 'default', id: '3', + uuid: '333-333', actionTypeId: 'action', }, { group: 'default', id: '4', + uuid: '444-444', actionTypeId: 'action', }, { group: 'default', id: '5', + uuid: '555-555', actionTypeId: 'action', }, ]; @@ -2986,7 +3001,7 @@ describe('Task Runner', () => { }) ); - expect(logger.debug).toHaveBeenCalledTimes(7); + expect(logger.debug).toHaveBeenCalledTimes(8); expect(logger.debug).nthCalledWith( 3, @@ -3023,11 +3038,11 @@ describe('Task Runner', () => { expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, generateActionOpts({})); expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith( 2, - generateActionOpts({ id: '2' }) + generateActionOpts({ id: '2', uuid: '222-222' }) ); expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith( 3, - generateActionOpts({ id: '3' }) + generateActionOpts({ id: '3', uuid: '333-333' }) ); }); @@ -3072,26 +3087,31 @@ describe('Task Runner', () => { { group: 'default', id: '1', + uuid: '111-111', actionTypeId: '.server-log', }, { group: 'default', id: '2', + uuid: '222-222', actionTypeId: '.server-log', }, { group: 'default', id: '3', + uuid: '333-333', actionTypeId: '.server-log', }, { group: 'default', id: '4', + uuid: '444-444', actionTypeId: 'any-action', }, { group: 'default', id: '5', + uuid: '555-555', actionTypeId: 'any-action', }, ] as RuleAction[], @@ -3190,7 +3210,7 @@ describe('Task Runner', () => { status: 'warning', errorReason: `maxExecutableActions`, logAlert: 4, - logAction: 3, + logAction: 5, }); }); From 32beee0ea9e4c9275826e8e486fc852515ad4d86 Mon Sep 17 00:00:00 2001 From: Ying Date: Mon, 7 Oct 2024 09:25:11 -0400 Subject: [PATCH 2/2] Adding comment about optional uuid --- .../server/lib/alerting_event_logger/alerting_event_logger.ts | 2 ++ .../server/task_runner/action_scheduler/action_scheduler.ts | 2 ++ .../action_scheduler/schedulers/per_alert_action_scheduler.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts index 5f9af56754e7a..1607f6090b10c 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts @@ -78,6 +78,8 @@ interface AlertOpts { export interface ActionOpts { id: string; + // uuid is typed as optional but in reality it is always + // populated - https://github.com/elastic/kibana/issues/195255 uuid?: string; typeId: string; alertId?: string; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts index 2f29e4f265a33..44822657ba86f 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts @@ -146,6 +146,8 @@ export class ActionScheduler< ); const uuid = r.uuid; + // uuid is typed as optional but in reality it is always + // populated - https://github.com/elastic/kibana/issues/195255 if (uuid) { actionsToNotLog.push(uuid); } diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts index 70e6992b4a69a..b35d86dff0105 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts @@ -276,6 +276,8 @@ export class PerAlertActionScheduler< }), actionToLog: { id: action.id, + // uuid is typed as optional but in reality it is always + // populated - https://github.com/elastic/kibana/issues/195255 uuid: action.uuid, typeId: action.actionTypeId, alertId: alert.getId(),