Skip to content

Commit

Permalink
[Alerting] extend Alert Type with names/descriptions of action variab…
Browse files Browse the repository at this point in the history
…les (elastic#59756)

resolves elastic#58529

This PR extends alertType with an `actionVariables` property, which
describes the properties of the context object passed when scheduling
actions, and the current state.  These property descriptions are used
by the web ui for the alert create and edit forms, to allow the properties
to be added to action parameters as mustache template variables.
  • Loading branch information
pmuellr committed Mar 13, 2020
1 parent 4cca749 commit e139d57
Show file tree
Hide file tree
Showing 18 changed files with 575 additions and 43 deletions.
41 changes: 38 additions & 3 deletions x-pack/plugins/alerting/README.md
Expand Up @@ -86,6 +86,7 @@ The following table describes the properties of the `options` object.
|id|Unique identifier for the alert type. For convention purposes, ids starting with `.` are reserved for built in alert types. We recommend using a convention like `<plugin_id>.mySpecialAlert` for your alert types to avoid conflicting with another plugin.|string|
|name|A user-friendly name for the alert type. These will be displayed in dropdowns when choosing alert types.|string|
|actionGroups|An explicit list of groups the alert type may schedule actions for, each specifying the ActionGroup's unique ID and human readable name. Alert `actions` validation will use this configuartion to ensure groups are valid. We highly encourage using `kbn-i18n` to translate the names of actionGroup when registering the AlertType. |Array<{id:string, name:string}>|
|actionVariables|An explicit list of action variables the alert type makes available via context and state in action parameter templates, and a short human readable description. Alert UI will use this to display prompts for the users for these variables, in action parameter editors. We highly encourage using `kbn-i18n` to translate the descriptions. |{ context: Array<{name:string, description:string}, state: Array<{name:string, description:string}>|
|validate.params|When developing an alert type, you can choose to accept a series of parameters. You may also have the parameters validated before they are passed to the `executor` function or created as an alert saved object. In order to do this, provide a `@kbn/config-schema` schema that we will use to validate the `params` attribute.|@kbn/config-schema|
|executor|This is where the code of the alert type lives. This is a function to be called when executing an alert on an interval basis. For full details, see executor section below.|Function|

Expand All @@ -112,11 +113,25 @@ This is the primary function for an alert type. Whenever the alert needs to exec
|createdBy|The userid that created this alert.|
|updatedBy|The userid that last updated this alert.|

### The `actionVariables` property

This property should contain the **flattened** names of the state and context variables available when an executor calls `alertInstance.scheduleActions(groupName, context)`. These names are meant to be used in prompters in the alerting user interface, are used as text values for display, and can be inserted into to an action parameter text entry field via UI gesture (eg, clicking a menu item from a menu built with these names). They should be flattened, so if a state or context variable is an object with properties, these should be listed with the "parent" property/properties in the name, separated by a `.` (period).

For example, if the `context` has one variable `foo` which is an object that has one property `bar`, and there are no `state` variables, the `actionVariables` value would be in the following shape:

```js
{
context: [
{ name: 'foo.bar', description: 'the ultra-exciting bar property' },
]
}
```

### Example

This example receives server and threshold as parameters. It will read the CPU usage of the server and schedule actions to be executed (asynchronously by the task manager) if the reading is greater than the threshold.

```
```typescript
import { schema } from '@kbn/config-schema';
...
server.newPlatform.setup.plugins.alerting.registerType({
Expand All @@ -128,6 +143,15 @@ server.newPlatform.setup.plugins.alerting.registerType({
threshold: schema.number({ min: 0, max: 1 }),
}),
},
actionVariables: {
context: [
{ name: 'server', description: 'the server' },
{ name: 'hasCpuUsageIncreased', description: 'boolean indicating if the cpu usage has increased' },
],
state: [
{ name: 'cpuUsage', description: 'CPU usage' },
],
},
async executor({
alertId,
startedAt,
Expand All @@ -136,7 +160,8 @@ server.newPlatform.setup.plugins.alerting.registerType({
params,
state,
}: AlertExecutorOptions) {
const { server, threshold } = params; // Let's assume params is { server: 'server_1', threshold: 0.8 }
// Let's assume params is { server: 'server_1', threshold: 0.8 }
const { server, threshold } = params;

// Call a function to get the server's current CPU usage
const currentCpuUsage = await getCpuUsage(server);
Expand Down Expand Up @@ -177,7 +202,7 @@ server.newPlatform.setup.plugins.alerting.registerType({

This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server.

```
```typescript
server.newPlatform.setup.plugins.alerting.registerType({
id: 'my-alert-type',
name: 'My alert type',
Expand All @@ -186,6 +211,15 @@ server.newPlatform.setup.plugins.alerting.registerType({
threshold: schema.number({ min: 0, max: 1 }),
}),
},
actionVariables: {
context: [
{ name: 'server', description: 'the server' },
{ name: 'hasCpuUsageIncreased', description: 'boolean indicating if the cpu usage has increased' },
],
state: [
{ name: 'cpuUsage', description: 'CPU usage' },
],
},
async executor({
alertId,
startedAt,
Expand Down Expand Up @@ -446,3 +480,4 @@ The templating system will take the alert and alert type as described above and
```

There are limitations that we are aware of using only templates, and we are gathering feedback and use cases for these. (for example passing an array of strings to an action).

72 changes: 72 additions & 0 deletions x-pack/plugins/alerting/server/alert_type_registry.test.ts
Expand Up @@ -6,6 +6,7 @@

import { TaskRunnerFactory } from './task_runner';
import { AlertTypeRegistry } from './alert_type_registry';
import { AlertType } from './types';
import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock';

const taskManager = taskManagerMock.setup();
Expand Down Expand Up @@ -126,6 +127,10 @@ describe('get()', () => {
"name": "Default",
},
],
"actionVariables": Object {
"context": Array [],
"state": Array [],
},
"defaultActionGroupId": "default",
"executor": [MockFunction],
"id": "test",
Expand Down Expand Up @@ -173,11 +178,78 @@ describe('list()', () => {
"name": "Test Action Group",
},
],
"actionVariables": Object {
"context": Array [],
"state": Array [],
},
"defaultActionGroupId": "testActionGroup",
"id": "test",
"name": "Test",
},
]
`);
});

test('should return action variables state and empty context', () => {
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
registry.register(alertTypeWithVariables('x', '', 's'));
const alertType = registry.get('x');
expect(alertType.actionVariables).toBeTruthy();

const context = alertType.actionVariables!.context;
const state = alertType.actionVariables!.state;

expect(context).toBeTruthy();
expect(context!.length).toBe(0);

expect(state).toBeTruthy();
expect(state!.length).toBe(1);
expect(state![0]).toEqual({ name: 's', description: 'x state' });
});

test('should return action variables context and empty state', () => {
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
registry.register(alertTypeWithVariables('x', 'c', ''));
const alertType = registry.get('x');
expect(alertType.actionVariables).toBeTruthy();

const context = alertType.actionVariables!.context;
const state = alertType.actionVariables!.state;

expect(state).toBeTruthy();
expect(state!.length).toBe(0);

expect(context).toBeTruthy();
expect(context!.length).toBe(1);
expect(context![0]).toEqual({ name: 'c', description: 'x context' });
});
});

function alertTypeWithVariables(id: string, context: string, state: string): AlertType {
const baseAlert = {
id,
name: `${id}-name`,
actionGroups: [],
defaultActionGroupId: id,
executor: (params: any): any => {},
};

if (!context && !state) {
return baseAlert;
}

const actionVariables = {
context: [{ name: context, description: `${id} context` }],
state: [{ name: state, description: `${id} state` }],
};

if (!context) {
delete actionVariables.context;
}

if (!state) {
delete actionVariables.state;
}

return { ...baseAlert, actionVariables };
}
9 changes: 9 additions & 0 deletions x-pack/plugins/alerting/server/alert_type_registry.ts
Expand Up @@ -40,6 +40,7 @@ export class AlertTypeRegistry {
})
);
}
alertType.actionVariables = normalizedActionVariables(alertType.actionVariables);
this.alertTypes.set(alertType.id, alertType);
this.taskManager.registerTaskDefinitions({
[`alerting:${alertType.id}`]: {
Expand Down Expand Up @@ -71,6 +72,14 @@ export class AlertTypeRegistry {
name: alertType.name,
actionGroups: alertType.actionGroups,
defaultActionGroupId: alertType.defaultActionGroupId,
actionVariables: alertType.actionVariables,
}));
}
}

function normalizedActionVariables(actionVariables: any) {
return {
context: actionVariables?.context ?? [],
state: actionVariables?.state ?? [],
};
}
Expand Up @@ -32,6 +32,9 @@ export function transformActionParams({
const result = cloneDeep(actionParams, (value: any) => {
if (!isString(value)) return;

// when the list of variables we pass in here changes,
// the UI will need to be updated as well; see:
// x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts
const variables = {
alertId,
alertName,
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/alerting/server/types.ts
Expand Up @@ -52,6 +52,11 @@ export interface AlertExecutorOptions {
updatedBy: string | null;
}

export interface ActionVariable {
name: string;
description: string;
}

export interface AlertType {
id: string;
name: string;
Expand All @@ -61,6 +66,10 @@ export interface AlertType {
actionGroups: ActionGroup[];
defaultActionGroupId: ActionGroup['id'];
executor: ({ services, params, state }: AlertExecutorOptions) => Promise<State | void>;
actionVariables?: {
context?: ActionVariable[];
state?: ActionVariable[];
};
}

export interface RawAlertAction extends SavedObjectAttributes {
Expand Down
Expand Up @@ -26,11 +26,8 @@ describe('ActionContext', () => {
thresholdComparator: '>',
threshold: [4],
});
const alertInfo = {
name: '[alert-name]',
};
const context = addMessages(alertInfo, base, params);
expect(context.subject).toMatchInlineSnapshot(
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
`"alert [alert-name] group [group] exceeded threshold"`
);
expect(context.message).toMatchInlineSnapshot(
Expand All @@ -57,11 +54,8 @@ describe('ActionContext', () => {
thresholdComparator: '>',
threshold: [4.2],
});
const alertInfo = {
name: '[alert-name]',
};
const context = addMessages(alertInfo, base, params);
expect(context.subject).toMatchInlineSnapshot(
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
`"alert [alert-name] group [group] exceeded threshold"`
);
expect(context.message).toMatchInlineSnapshot(
Expand All @@ -87,11 +81,8 @@ describe('ActionContext', () => {
thresholdComparator: 'between',
threshold: [4, 5],
});
const alertInfo = {
name: '[alert-name]',
};
const context = addMessages(alertInfo, base, params);
expect(context.subject).toMatchInlineSnapshot(
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
`"alert [alert-name] group [group] exceeded threshold"`
);
expect(context.message).toMatchInlineSnapshot(
Expand Down
Expand Up @@ -13,9 +13,9 @@ import { AlertExecutorOptions } from '../../../../alerting/server';
type AlertInfo = Pick<AlertExecutorOptions, 'name'>;

export interface ActionContext extends BaseActionContext {
// a short generic message which may be used in an action message
subject: string;
// a longer generic message which may be used in an action message
// a short pre-constructed message which may be used in an action field
title: string;
// a longer pre-constructed message which may be used in an action field
message: string;
}

Expand All @@ -34,7 +34,7 @@ export function addMessages(
baseContext: BaseActionContext,
params: Params
): ActionContext {
const subject = i18n.translate(
const title = i18n.translate(
'xpack.alertingBuiltins.indexThreshold.alertTypeContextSubjectTitle',
{
defaultMessage: 'alert {name} group {group} exceeded threshold',
Expand Down Expand Up @@ -65,5 +65,5 @@ export function addMessages(
}
);

return { ...baseContext, subject, message };
return { ...baseContext, title, message };
}
Expand Up @@ -22,6 +22,33 @@ describe('alertType', () => {
expect(alertType.id).toBe('.index-threshold');
expect(alertType.name).toBe('Index Threshold');
expect(alertType.actionGroups).toEqual([{ id: 'threshold met', name: 'Threshold Met' }]);

expect(alertType.actionVariables).toMatchInlineSnapshot(`
Object {
"context": Array [
Object {
"description": "A pre-constructed message for the alert.",
"name": "message",
},
Object {
"description": "A pre-constructed title for the alert.",
"name": "title",
},
Object {
"description": "The group that exceeded the threshold.",
"name": "group",
},
Object {
"description": "The date the alert exceeded the threshold.",
"name": "date",
},
Object {
"description": "The value that exceeded the threshold.",
"name": "value",
},
],
}
`);
});

it('validator succeeds with valid params', async () => {
Expand Down

0 comments on commit e139d57

Please sign in to comment.