Skip to content

Commit

Permalink
Add muting support for alerts (elastic#43712) (elastic#46951)
Browse files Browse the repository at this point in the history
* Create supporting API

* Rename mute terminology to mute instance to allow alert level muting

* Add alert mute and unmute APIs

* Add logic to handle alert muting

* Add integration tests + fix AAD breaking the object

* Fix failing jest tests

* Fix test failures

* Clear out mutedInstanceIds when muting / unmuting an alert

* Skip muting / unmuting instances when alert is muted

* Rename interface for alert instance

* Rename functional tests to alert instance terminology

* Add API integration tests for alert muting / unmuting

* Apply PR feedback pt1

* Create single index record action

* Function to create always firing alerts and function to generate reference

* Make tests use alert utils

* Rename mute / unmute alert routes

* Make alerts.ts integration test use alertUtils for both spaces_only and security_and_spaces

* Re-use alert utils where possible

* Change muted in mapping to muteAll

* Rename alert client methods to muteAll and unmuteAll

* Rename files

* Rename alert utils function muteAll and unmuteAll

* Rename variable in task runner

* Cleanup

* Destructure instead of using existingObject variable
  • Loading branch information
mikecote authored Sep 30, 2019
1 parent 24edb62 commit d6d01a2
Show file tree
Hide file tree
Showing 45 changed files with 1,880 additions and 616 deletions.
6 changes: 6 additions & 0 deletions x-pack/legacy/plugins/alerting/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
},
"throttle": {
"type": "keyword"
},
"muteAll": {
"type": "boolean"
},
"mutedInstanceIds": {
"type": "keyword"
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const createAlertsClientMock = () => {
enable: jest.fn(),
disable: jest.fn(),
updateApiKey: jest.fn(),
muteAll: jest.fn(),
unmuteAll: jest.fn(),
muteInstance: jest.fn(),
unmuteInstance: jest.fn(),
};
return mocked;
};
Expand Down
750 changes: 486 additions & 264 deletions x-pack/legacy/plugins/alerting/server/alerts_client.test.ts

Large diffs are not rendered by default.

140 changes: 123 additions & 17 deletions x-pack/legacy/plugins/alerting/server/alerts_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ interface FindResult {
}

interface CreateOptions {
data: Pick<Alert, Exclude<keyof Alert, 'createdBy' | 'updatedBy' | 'apiKey' | 'apiKeyOwner'>>;
data: Pick<
Alert,
Exclude<
keyof Alert,
'createdBy' | 'updatedBy' | 'apiKey' | 'apiKeyOwner' | 'muteAll' | 'mutedInstanceIds'
>
>;
options?: {
migrationVersion?: Record<string, string>;
};
Expand All @@ -69,7 +75,6 @@ interface UpdateOptions {
actions: AlertAction[];
alertTypeParams: Record<string, any>;
};
options?: { version?: string };
}

export class AlertsClient {
Expand Down Expand Up @@ -117,6 +122,8 @@ export class AlertsClient {
? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64')
: undefined,
alertTypeParams: validatedAlertTypeParams,
muteAll: false,
mutedInstanceIds: [],
});
const createdAlert = await this.savedObjectsClient.create('alert', rawAlert, {
...options,
Expand Down Expand Up @@ -188,10 +195,9 @@ export class AlertsClient {
return removeResult;
}

public async update({ id, data, options = {} }: UpdateOptions) {
const existingObject = await this.savedObjectsClient.get('alert', id);
const { alertTypeId } = existingObject.attributes;
const alertType = this.alertTypeRegistry.get(alertTypeId);
public async update({ id, data }: UpdateOptions) {
const { attributes, version } = await this.savedObjectsClient.get('alert', id);
const alertType = this.alertTypeRegistry.get(attributes.alertTypeId);
const apiKey = await this.createAPIKey();

// Validate
Expand All @@ -204,6 +210,7 @@ export class AlertsClient {
'alert',
id,
{
...attributes,
...data,
alertTypeParams: validatedAlertTypeParams,
actions,
Expand All @@ -214,48 +221,51 @@ export class AlertsClient {
: null,
},
{
...options,
version,
references,
}
);
return this.getAlertFromRaw(id, updatedObject.attributes, updatedObject.references);
}

public async updateApiKey({ id }: { id: string }) {
const { references } = await this.savedObjectsClient.get('alert', id);
const { references, version, attributes } = await this.savedObjectsClient.get('alert', id);

const apiKey = await this.createAPIKey();
const username = await this.getUserName();
await this.savedObjectsClient.update(
'alert',
id,
{
...attributes,
updatedBy: username,
apiKeyOwner: apiKey.created ? username : null,
apiKey: apiKey.created
? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64')
: null,
},
{
version,
references,
}
);
}

public async enable({ id }: { id: string }) {
const existingObject = await this.savedObjectsClient.get('alert', id);
if (existingObject.attributes.enabled === false) {
const { attributes, version, references } = await this.savedObjectsClient.get('alert', id);
if (attributes.enabled === false) {
const apiKey = await this.createAPIKey();
const scheduledTask = await this.scheduleAlert(
id,
existingObject.attributes.alertTypeId,
existingObject.attributes.interval
attributes.alertTypeId,
attributes.interval
);
const username = await this.getUserName();
await this.savedObjectsClient.update(
'alert',
id,
{
...attributes,
enabled: true,
updatedBy: username,
apiKeyOwner: apiKey.created ? username : null,
Expand All @@ -264,27 +274,123 @@ export class AlertsClient {
? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64')
: null,
},
{ references: existingObject.references }
{
version,
references,
}
);
}
}

public async disable({ id }: { id: string }) {
const existingObject = await this.savedObjectsClient.get('alert', id);
if (existingObject.attributes.enabled === true) {
const { attributes, version, references } = await this.savedObjectsClient.get('alert', id);
if (attributes.enabled === true) {
await this.savedObjectsClient.update(
'alert',
id,
{
...attributes,
enabled: false,
scheduledTaskId: null,
apiKey: null,
apiKeyOwner: null,
updatedBy: await this.getUserName(),
},
{ references: existingObject.references }
{
version,
references,
}
);
await this.taskManager.remove(attributes.scheduledTaskId);
}
}

public async muteAll({ id }: { id: string }) {
const {
references,
attributes: { muteAll },
} = await this.savedObjectsClient.get('alert', id);
if (!muteAll) {
await this.savedObjectsClient.update(
'alert',
id,
{
muteAll: true,
mutedInstanceIds: [],
updatedBy: await this.getUserName(),
},
{ references }
);
}
}

public async unmuteAll({ id }: { id: string }) {
const {
references,
attributes: { muteAll },
} = await this.savedObjectsClient.get('alert', id);
if (muteAll) {
await this.savedObjectsClient.update(
'alert',
id,
{
muteAll: false,
mutedInstanceIds: [],
updatedBy: await this.getUserName(),
},
{ references }
);
}
}

public async muteInstance({
alertId,
alertInstanceId,
}: {
alertId: string;
alertInstanceId: string;
}) {
const { attributes, version, references } = await this.savedObjectsClient.get('alert', alertId);
const mutedInstanceIds = attributes.mutedInstanceIds || [];
if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) {
mutedInstanceIds.push(alertInstanceId);
await this.savedObjectsClient.update(
'alert',
alertId,
{
mutedInstanceIds,
updatedBy: await this.getUserName(),
},
{
version,
references,
}
);
}
}

public async unmuteInstance({
alertId,
alertInstanceId,
}: {
alertId: string;
alertInstanceId: string;
}) {
const { attributes, version, references } = await this.savedObjectsClient.get('alert', alertId);
const mutedInstanceIds = attributes.mutedInstanceIds || [];
if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) {
await this.savedObjectsClient.update(
'alert',
alertId,
{
updatedBy: await this.getUserName(),
mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId),
},
{
version,
references,
}
);
await this.taskManager.remove(existingObject.attributes.scheduledTaskId);
}
}

Expand Down
15 changes: 14 additions & 1 deletion x-pack/legacy/plugins/alerting/server/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
enableAlertRoute,
disableAlertRoute,
updateApiKeyRoute,
muteAllAlertRoute,
unmuteAllAlertRoute,
muteAlertInstanceRoute,
unmuteAlertInstanceRoute,
} from './routes';

// Extend PluginProperties to indicate which plugins are guaranteed to exist
Expand Down Expand Up @@ -88,7 +92,12 @@ export function init(server: Server) {
server.plugins.encrypted_saved_objects.registerType({
type: 'alert',
attributesToEncrypt: new Set(['apiKey']),
attributesToExcludeFromAAD: new Set(['scheduledTaskId']),
attributesToExcludeFromAAD: new Set([
'scheduledTaskId',
'muted',
'mutedInstanceIds',
'updatedBy',
]),
});

function getServices(request: any): Services {
Expand Down Expand Up @@ -127,6 +136,10 @@ export function init(server: Server) {
enableAlertRoute(server);
disableAlertRoute(server);
updateApiKeyRoute(server);
muteAllAlertRoute(server);
unmuteAllAlertRoute(server);
muteAlertInstanceRoute(server);
unmuteAlertInstanceRoute(server);

// Expose functions
server.decorate('request', 'getAlertsClient', function() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const mockedAlertTypeSavedObject = {
enabled: true,
alertTypeId: '123',
interval: '10s',
mutedInstanceIds: [],
alertTypeParams: {
bar: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function getCreateTaskRunnerFunction({
const services = getServices(fakeRequest);
// Ensure API key is still valid and user has access
const {
attributes: { alertTypeParams, actions, interval, throttle },
attributes: { alertTypeParams, actions, interval, throttle, muteAll, mutedInstanceIds },
references,
} = await services.savedObjectsClient.get<RawAlert>('alert', alertId);

Expand Down Expand Up @@ -128,6 +128,9 @@ export function getCreateTaskRunnerFunction({
Object.keys(alertInstances).map(alertInstanceId => {
const alertInstance = alertInstances[alertInstanceId];
if (alertInstance.hasScheduledActions(throttle)) {
if (muteAll || mutedInstanceIds.includes(alertInstanceId)) {
return;
}
const { actionGroup, context, state } = alertInstance.getScheduledActionOptions()!;
alertInstance.updateLastScheduledActions(actionGroup);
alertInstance.unscheduleActions();
Expand Down
4 changes: 4 additions & 0 deletions x-pack/legacy/plugins/alerting/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export { updateAlertRoute } from './update';
export { enableAlertRoute } from './enable';
export { disableAlertRoute } from './disable';
export { updateApiKeyRoute } from './update_api_key';
export { muteAlertInstanceRoute } from './mute_instance';
export { unmuteAlertInstanceRoute } from './unmute_instance';
export { muteAllAlertRoute } from './mute_all';
export { unmuteAllAlertRoute } from './unmute_all';
22 changes: 22 additions & 0 deletions x-pack/legacy/plugins/alerting/server/routes/mute_all.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { createMockServer } from './_mock_server';
import { muteAllAlertRoute } from './mute_all';

const { server, alertsClient } = createMockServer();
muteAllAlertRoute(server);

test('mutes an alert', async () => {
const request = {
method: 'POST',
url: '/api/alert/1/_mute_all',
};

const { statusCode } = await server.inject(request);
expect(statusCode).toBe(204);
expect(alertsClient.muteAll).toHaveBeenCalledWith({ id: '1' });
});
31 changes: 31 additions & 0 deletions x-pack/legacy/plugins/alerting/server/routes/mute_all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import Hapi from 'hapi';

interface MuteAllRequest extends Hapi.Request {
params: {
id: string;
};
}

export function muteAllAlertRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert/{id}/_mute_all',
options: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
},
async handler(request: MuteAllRequest, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.muteAll(request.params);
return h.response();
},
});
}
Loading

0 comments on commit d6d01a2

Please sign in to comment.