Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add muting support for alerts #43712

Merged
merged 33 commits into from
Sep 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
85cd641
Create supporting API
mikecote Aug 21, 2019
aee975f
Merge branch 'master' of github.com:elastic/kibana into alerting/muting
mikecote Aug 26, 2019
aa5677a
Merge with master
mikecote Sep 23, 2019
db605ac
Rename mute terminology to mute instance to allow alert level muting
mikecote Sep 23, 2019
a92a58d
Add alert mute and unmute APIs
mikecote Sep 23, 2019
ddcdf6e
Add logic to handle alert muting
mikecote Sep 23, 2019
e6a22d4
Add integration tests + fix AAD breaking the object
mikecote Sep 23, 2019
b9527d9
Fix failing jest tests
mikecote Sep 23, 2019
0b62a8f
Fix test failures
mikecote Sep 23, 2019
a0266f8
Clear out mutedInstanceIds when muting / unmuting an alert
mikecote Sep 23, 2019
45aebfd
Skip muting / unmuting instances when alert is muted
mikecote Sep 23, 2019
4c63dd2
Rename interface for alert instance
mikecote Sep 23, 2019
68eb6e9
Rename functional tests to alert instance terminology
mikecote Sep 23, 2019
5116398
Add API integration tests for alert muting / unmuting
mikecote Sep 23, 2019
f7efee0
Merge branch 'master' of github.com:elastic/kibana into alerting/muting
mikecote Sep 24, 2019
5c147c1
Apply PR feedback pt1
mikecote Sep 24, 2019
413e67a
Create single index record action
mikecote Sep 24, 2019
491887f
Function to create always firing alerts and function to generate refe…
mikecote Sep 24, 2019
8d00147
Make tests use alert utils
mikecote Sep 24, 2019
2cb11d9
Merge branch 'master' of github.com:elastic/kibana into alerting/muting
mikecote Sep 25, 2019
0508bac
Rename mute / unmute alert routes
mikecote Sep 25, 2019
1a97fc2
Make alerts.ts integration test use alertUtils for both spaces_only a…
mikecote Sep 25, 2019
4e25fcd
Re-use alert utils where possible
mikecote Sep 25, 2019
4466cdf
Merge branch 'master' of github.com:elastic/kibana into alerting/muting
mikecote Sep 27, 2019
67c1bb3
Change muted in mapping to muteAll
mikecote Sep 27, 2019
9de3ccf
Rename alert client methods to muteAll and unmuteAll
mikecote Sep 27, 2019
3ee1ea4
Rename files
mikecote Sep 27, 2019
10f660d
Rename alert utils function muteAll and unmuteAll
mikecote Sep 27, 2019
522eaa9
Rename variable in task runner
mikecote Sep 27, 2019
b97f1ce
Cleanup
mikecote Sep 27, 2019
730acf2
Destructure instead of using existingObject variable
mikecote Sep 30, 2019
62ba157
Merge branch 'master' of github.com:elastic/kibana into alerting/muting
mikecote Sep 30, 2019
5a5fbc4
Fix merge conflicts
mikecote Sep 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
mikecote marked this conversation as resolved.
Show resolved Hide resolved
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