Skip to content

Commit

Permalink
[Alerting] run alerts whenever an alerts schedule is updated (#53143)
Browse files Browse the repository at this point in the history
When an Alert is updated its interval is stored but isn't applied to the underlying scheduled task.
In this PR we make use of the new runNow api to "refresh" the task whenever the alert's schedule is updated.
  • Loading branch information
gmmorris committed Dec 20, 2019
1 parent 1cd7c66 commit 9513123
Show file tree
Hide file tree
Showing 17 changed files with 466 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ describe('execute()', () => {
id: '123',
params: { baz: false },
spaceId: 'default',
apiKey: null,
});
expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({
getBasePath: expect.anything(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ExecuteOptions {
id: string;
params: Record<string, any>;
spaceId: string;
apiKey?: string;
apiKey: string | null;
}

export function createExecuteFunction({
Expand Down
2 changes: 1 addition & 1 deletion x-pack/legacy/plugins/alerting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ Payload:
|name|A name to reference and search in the future.|string|
|tags|A list of keywords to reference and search in the future.|string[]|
|alertTypeId|The id value of the alert type you want to call when the alert is scheduled to execute.|string|
|schedule|The schedule specifying when this alert should run, using one of the available schedule formats specified under _Schedule Formats_ below|object|
|schedule|The schedule specifying when this alert should be run, using one of the available schedule formats specified under _Schedule Formats_ below|object|
|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object|
|actions|Array of the following:<br> - `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.<br>- `id` (string): The id of the action saved object to execute.<br>- `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array|

Expand Down
231 changes: 228 additions & 3 deletions x-pack/legacy/plugins/alerting/server/alerts_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import uuid from 'uuid';
import { schema } from '@kbn/config-schema';
import { AlertsClient } from './alerts_client';
import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks';
import { taskManagerMock } from '../../task_manager/task_manager.mock';
import { alertTypeRegistryMock } from './alert_type_registry.mock';
import { TaskStatus } from '../../task_manager';
import { IntervalSchedule } from './types';
import { resolvable } from './test_utils';

const taskManager = taskManagerMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();
Expand All @@ -29,6 +31,7 @@ beforeEach(() => {
jest.resetAllMocks();
alertsClientParams.createAPIKey.mockResolvedValue({ created: false });
alertsClientParams.getUserName.mockResolvedValue('elastic');
taskManager.runNow.mockResolvedValue({ id: '' });
});

const mockedDate = new Date('2019-02-12T21:01:22.479Z');
Expand Down Expand Up @@ -183,8 +186,8 @@ describe('create()', () => {
},
],
"alertTypeId": "123",
"apiKey": undefined,
"apiKeyOwner": undefined,
"apiKey": null,
"apiKeyOwner": null,
"consumer": "bar",
"createdBy": "elastic",
"enabled": true,
Expand Down Expand Up @@ -1955,6 +1958,228 @@ describe('update()', () => {
`"params invalid: [param1]: expected value of type [string] but got [undefined]"`
);
});

describe('updating an alert schedule', () => {
function mockApiCalls(
alertId: string,
taskId: string,
currentSchedule: IntervalSchedule,
updatedSchedule: IntervalSchedule
) {
// mock return values from deps
alertTypeRegistry.get.mockReturnValueOnce({
id: '123',
name: 'Test',
actionGroups: ['default'],
async executor() {},
});
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
actionTypeId: 'test',
},
references: [],
},
],
});
savedObjectsClient.get.mockResolvedValueOnce({
id: alertId,
type: 'alert',
attributes: {
enabled: true,
alertTypeId: '123',
schedule: currentSchedule,
scheduledTaskId: 'task-123',
},
references: [],
version: '123',
});

taskManager.schedule.mockResolvedValueOnce({
id: taskId,
taskType: 'alerting:123',
scheduledAt: new Date(),
attempts: 1,
status: TaskStatus.Idle,
runAt: new Date(),
startedAt: null,
retryAt: null,
state: {},
params: {},
ownerId: null,
});
savedObjectsClient.update.mockResolvedValueOnce({
id: alertId,
type: 'alert',
attributes: {
enabled: true,
schedule: updatedSchedule,
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
scheduledTaskId: taskId,
},
references: [
{
name: 'action_0',
type: 'action',
id: alertId,
},
],
});

taskManager.runNow.mockReturnValueOnce(Promise.resolve({ id: alertId }));
}

test('updating the alert schedule should rerun the task immediately', async () => {
const alertId = uuid.v4();
const taskId = uuid.v4();
const alertsClient = new AlertsClient(alertsClientParams);

mockApiCalls(alertId, taskId, { interval: '60m' }, { interval: '10s' });

await alertsClient.update({
id: alertId,
data: {
schedule: { interval: '10s' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
],
},
});

expect(taskManager.runNow).toHaveBeenCalledWith(taskId);
});

test('updating the alert without changing the schedule should not rerun the task', async () => {
const alertId = uuid.v4();
const taskId = uuid.v4();
const alertsClient = new AlertsClient(alertsClientParams);

mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '10s' });

await alertsClient.update({
id: alertId,
data: {
schedule: { interval: '10s' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
],
},
});

expect(taskManager.runNow).not.toHaveBeenCalled();
});

test('updating the alert should not wait for the rerun the task to complete', async done => {
const alertId = uuid.v4();
const taskId = uuid.v4();
const alertsClient = new AlertsClient(alertsClientParams);

mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' });

const resolveAfterAlertUpdatedCompletes = resolvable<{ id: string }>();
resolveAfterAlertUpdatedCompletes.then(() => done());

taskManager.runNow.mockReset();
taskManager.runNow.mockReturnValue(resolveAfterAlertUpdatedCompletes);

await alertsClient.update({
id: alertId,
data: {
schedule: { interval: '10s' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
],
},
});

expect(taskManager.runNow).toHaveBeenCalled();

resolveAfterAlertUpdatedCompletes.resolve({ id: alertId });
});

test('logs when the rerun of an alerts underlying task fails', async () => {
const alertId = uuid.v4();
const taskId = uuid.v4();
const alertsClient = new AlertsClient(alertsClientParams);

mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' });

taskManager.runNow.mockReset();
taskManager.runNow.mockRejectedValue(new Error('Failed to run alert'));

await alertsClient.update({
id: alertId,
data: {
schedule: { interval: '10s' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
],
},
});

expect(taskManager.runNow).toHaveBeenCalled();

expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
`Alert update failed to run its underlying task. TaskManager runNow failed with Error: Failed to run alert`
);
});
});
});

describe('updateApiKey()', () => {
Expand Down
Loading

0 comments on commit 9513123

Please sign in to comment.