Skip to content

Commit

Permalink
[Response Ops][Alerting] Adding ability to run actions for backfill r…
Browse files Browse the repository at this point in the history
…ule runs (#200784)

Resolves elastic/response-ops-team#251


## Note

This PR includes some saved object schema changes that I will pull out
into their own separate PR in order to perform an intermediate release.
I wanted to make sure all the schema changes made sense in the overall
context of the PR before opening those separate PRs.

Update: PR for intermediate release here:
#203184 (Merged)

## Summary

Adds ability to run actions for backfill rule runs.

- Updates schedule backfill API to accept `run_actions` parameter to
specify whether to run actions for backfill.
- Schedule API accepts any action where `frequency.notifyWhen ===
'onActiveAlert'`. If a rule has multiple actions where some are
`onActiveAlert` and some are `onThrottleInterval`, the invalid actions
will be stripped and a warning returned in the schedule response but
valid actions will be scheduled.
- Connector IDs are extracted and stored as references in the ad hoc run
params saved object
- Any actions that result from a backfill task run are scheduled as low
priority tasks

## To Verify

1. Create a detection rule. Make sure you have some past data that the
rule can run over in order to generate actions. Make sure you add
actions to the rule. For testing, I added some conditional actions so I
could see actions running only on backfill runs using
`kibana.alert.rule.execution.type: "manual"`. Create actions with and
without summaries.
2. Schedule a backfill either directly via the API or using the
detection UI. Verify that actions are run for the backfill runs that
generate alerts.

---------

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
ymao1 and elasticmachine authored Jan 20, 2025
1 parent a39898b commit 075806b
Show file tree
Hide file tree
Showing 50 changed files with 4,251 additions and 421 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
asSavedObjectExecutionSource,
} from './lib/action_execution_source';
import { actionsConfigMock } from './actions_config.mock';
import { TaskPriority } from '@kbn/task-manager-plugin/server';

const mockTaskManager = taskManagerMock.createStart();
const savedObjectsClient = savedObjectsClientMock.create();
Expand Down Expand Up @@ -1189,4 +1190,210 @@ describe('bulkExecute()', () => {
]
`);
});

test('uses priority if specified', 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',
priority: TaskPriority.Low,
},
{
id: '123',
params: { baz: false },
spaceId: 'default',
executionId: '456xyz',
apiKey: null,
source: asHttpRequestExecutionSource(request),
actionTypeId: 'mock-action',
},
])
).toMatchInlineSnapshot(`
Object {
"errors": true,
"items": Array [
Object {
"actionTypeId": "mock-action",
"id": "123",
"response": "success",
"uuid": undefined,
},
Object {
"actionTypeId": "mock-action",
"id": "123",
"response": "queuedActionsLimitError",
"uuid": undefined,
},
],
}
`);
expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1);
expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Array [
Object {
"params": Object {
"actionTaskParamsId": "234",
"spaceId": "default",
},
"priority": 1,
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:mock-action",
},
],
]
`);
});

test('uses apiKeyId if specified', 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',
apiKeyId: '235qgbdbqet',
},
{
id: '123',
params: { baz: false },
spaceId: 'default',
executionId: '456xyz',
apiKey: null,
source: asHttpRequestExecutionSource(request),
actionTypeId: 'mock-action',
apiKeyId: '235qgbdbqet',
},
])
).toMatchInlineSnapshot(`
Object {
"errors": true,
"items": Array [
Object {
"actionTypeId": "mock-action",
"id": "123",
"response": "success",
"uuid": undefined,
},
Object {
"actionTypeId": "mock-action",
"id": "123",
"response": "queuedActionsLimitError",
"uuid": undefined,
},
],
}
`);

expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[
{
attributes: {
actionId: '123',
apiKey: null,
apiKeyId: '235qgbdbqet',
consumer: undefined,
executionId: '123abc',
params: {
baz: false,
},
relatedSavedObjects: undefined,
source: 'HTTP_REQUEST',
},
references: [
{
id: '123',
name: 'actionRef',
type: 'action',
},
],
type: 'action_task_params',
},
],
{ refresh: false }
);
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",
},
],
]
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { SavedObjectsBulkResponse, SavedObjectsClientContract, Logger } from '@kbn/core/server';
import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
import { TaskPriority, TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
import { RawAction, ActionTypeRegistryContract, InMemoryConnector } from './types';
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects';
import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor';
Expand All @@ -28,9 +28,11 @@ export interface ExecuteOptions
id: string;
uuid?: string;
spaceId: string;
apiKeyId?: string;
apiKey: string | null;
executionId: string;
actionTypeId: string;
priority?: TaskPriority;
}

interface ActionTaskParams
Expand Down Expand Up @@ -166,15 +168,17 @@ export function createBulkExecutionEnqueuerFunction({
executionId: actionToExecute.executionId,
consumer: actionToExecute.consumer,
relatedSavedObjects: relatedSavedObjectWithRefs,
apiKeyId: actionToExecute.apiKeyId,
...(actionToExecute.source ? { source: actionToExecute.source.type } : {}),
},
references: taskReferences,
};
});

const actionTaskParamsRecords: SavedObjectsBulkResponse<ActionTaskParams> =
await unsecuredSavedObjectsClient.bulkCreate(actions, { refresh: false });

const taskInstances = actionTaskParamsRecords.saved_objects.map((so) => {
const taskInstances = actionTaskParamsRecords.saved_objects.map((so, index) => {
const actionId = so.attributes.actionId;
return {
taskType: `actions:${actionTypeIds[actionId]}`,
Expand All @@ -184,6 +188,7 @@ export function createBulkExecutionEnqueuerFunction({
},
state: {},
scope: ['actions'],
...(runnableActions[index]?.priority ? { priority: runnableActions[index].priority } : {}),
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ const mockAdHocRunSO: SavedObject<AdHocRunSO> = {
name: fakeRuleName,
tags: ['foo'],
alertTypeId: 'myType',
// @ts-expect-error
params: {},
actions: [],
apiKeyOwner: 'user',
apiKeyCreatedByUser: false,
consumer: 'myApp',
Expand Down
Loading

0 comments on commit 075806b

Please sign in to comment.