mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Alerting] Introduces a ActionSubGroup which allows for more granular action group scheduling (#84751)
This PR introduces a new concept of an _Action Subgroup_ (naming is open for discussion) which can be used by an Alert Type when scheduling actions. An Action Subgroup can be dynamically specified, unlike Action Groups which have to be specified on the AlertType definition. When scheduling actions, and AlertType can specify an _Action Subgroup_ along side the scheduled _Action Group_, which denotes that the alert instance falls into some kind of narrower grouping in the action group.
This commit is contained in:
parent
0b929f340e
commit
015f3c994b
24 changed files with 588 additions and 58 deletions
|
@ -13,14 +13,13 @@ import {
|
|||
AlwaysFiringParams,
|
||||
} from '../../common/constants';
|
||||
|
||||
const ACTION_GROUPS = [
|
||||
{ id: 'small', name: 'Small t-shirt' },
|
||||
{ id: 'medium', name: 'Medium t-shirt' },
|
||||
{ id: 'large', name: 'Large t-shirt' },
|
||||
];
|
||||
const DEFAULT_ACTION_GROUP = 'small';
|
||||
type ActionGroups = 'small' | 'medium' | 'large';
|
||||
const DEFAULT_ACTION_GROUP: ActionGroups = 'small';
|
||||
|
||||
function getTShirtSizeByIdAndThreshold(id: string, thresholds: AlwaysFiringParams['thresholds']) {
|
||||
function getTShirtSizeByIdAndThreshold(
|
||||
id: string,
|
||||
thresholds: AlwaysFiringParams['thresholds']
|
||||
): ActionGroups {
|
||||
const idAsNumber = parseInt(id, 10);
|
||||
if (!isNaN(idAsNumber)) {
|
||||
if (thresholds?.large && thresholds.large < idAsNumber) {
|
||||
|
@ -36,10 +35,19 @@ function getTShirtSizeByIdAndThreshold(id: string, thresholds: AlwaysFiringParam
|
|||
return DEFAULT_ACTION_GROUP;
|
||||
}
|
||||
|
||||
export const alertType: AlertType<AlwaysFiringParams> = {
|
||||
export const alertType: AlertType<
|
||||
AlwaysFiringParams,
|
||||
{ count?: number },
|
||||
{ triggerdOnCycle: number },
|
||||
never
|
||||
> = {
|
||||
id: 'example.always-firing',
|
||||
name: 'Always firing',
|
||||
actionGroups: ACTION_GROUPS,
|
||||
actionGroups: [
|
||||
{ id: 'small', name: 'Small t-shirt' },
|
||||
{ id: 'medium', name: 'Medium t-shirt' },
|
||||
{ id: 'large', name: 'Large t-shirt' },
|
||||
],
|
||||
defaultActionGroupId: DEFAULT_ACTION_GROUP,
|
||||
async executor({
|
||||
services,
|
||||
|
|
|
@ -623,8 +623,23 @@ This factory returns an instance of `AlertInstance`. The alert instance class ha
|
|||
|Method|Description|
|
||||
|---|---|
|
||||
|getState()|Get the current state of the alert instance.|
|
||||
|scheduleActions(actionGroup, context)|Called to schedule the execution of actions. The actionGroup is a string `id` that relates to the group of alert `actions` to execute and the context will be used for templating purposes. This should only be called once per alert instance.|
|
||||
|replaceState(state)|Used to replace the current state of the alert instance. This doesn't work like react, the entire state must be provided. Use this feature as you see fit. The state that is set will persist between alert type executions whenever you re-create an alert instance with the same id. The instance state will be erased when `scheduleActions` isn't called during an execution.|
|
||||
|scheduleActions(actionGroup, context)|Called to schedule the execution of actions. The actionGroup is a string `id` that relates to the group of alert `actions` to execute and the context will be used for templating purposes. `scheduleActions` or `scheduleActionsWithSubGroup` should only be called once per alert instance.|
|
||||
|scheduleActionsWithSubGroup(actionGroup, subgroup, context)|Called to schedule the execution of actions within a subgroup. The actionGroup is a string `id` that relates to the group of alert `actions` to execute, the `subgroup` is a dynamic string that denotes a subgroup within the actionGroup and the context will be used for templating purposes. `scheduleActions` or `scheduleActionsWithSubGroup` should only be called once per alert instance.|
|
||||
|replaceState(state)|Used to replace the current state of the alert instance. This doesn't work like react, the entire state must be provided. Use this feature as you see fit. The state that is set will persist between alert type executions whenever you re-create an alert instance with the same id. The instance state will be erased when `scheduleActions` or `scheduleActionsWithSubGroup` aren't called during an execution.|
|
||||
|
||||
### when should I use `scheduleActions` and `scheduleActionsWithSubGroup`?
|
||||
The `scheduleActions` or `scheduleActionsWithSubGroup` methods are both used to achieve the same thing: schedule actions to be run under a specific action group.
|
||||
It's important to note though, that when an actions are scheduled for an instance, we check whether the instance was already active in this action group after the previous execution. If it was, then we might throttle the actions (adhering to the user's configuration), as we don't consider this a change in the instance.
|
||||
|
||||
What happens though, if the instance _has_ changed, but they just happen to be in the same action group after this change? This is where subgroups come in. By specifying a subgroup (using the `scheduleActionsWithSubGroup` method), the instance becomes active within the action group, but it will also keep track of the subgroup.
|
||||
If the subgroup changes, then the framework will treat the instance as if it had been placed in a new action group. It is important to note though, we only use the subgroup to denote a change if both the current execution and the previous one specified a subgroup.
|
||||
|
||||
You might wonder, why bother using a subgroup if you can just add a new action group?
|
||||
Action Groups are static, and have to be define when the Alert Type is defined.
|
||||
Action Subgroups are dynamic, and can be defined on the fly.
|
||||
|
||||
This approach enables users to specify actions under specific action groups, but they can't specify actions that are specific to subgroups.
|
||||
As subgroups fall under action groups, we will schedule the actions specified for the action group, but the subgroup allows the AlertType implementer to reuse the same action group for multiple different active subgroups.
|
||||
|
||||
## Templating actions
|
||||
|
||||
|
@ -632,7 +647,7 @@ There needs to be a way to map alert context into action parameters. For this, w
|
|||
|
||||
When an alert instance executes, the first argument is the `group` of actions to execute and the second is the context the alert exposes to templates. We iterate through each action params attributes recursively and render templates if they are a string. Templates have access to the following "variables":
|
||||
|
||||
- `context` - provided by second argument of `.scheduleActions(...)` on an alert instance
|
||||
- `context` - provided by context argument of `.scheduleActions(...)` and `.scheduleActionsWithSubGroup(...)` on an alert instance
|
||||
- `state` - the alert instance's `state` provided by the most recent `replaceState` call on an alert instance
|
||||
- `alertId` - the id of the alert
|
||||
- `alertInstanceId` - the alert instance id
|
||||
|
|
|
@ -7,10 +7,15 @@ import * as t from 'io-ts';
|
|||
import { DateFromString } from './date_from_string';
|
||||
|
||||
const metaSchema = t.partial({
|
||||
lastScheduledActions: t.type({
|
||||
group: t.string,
|
||||
date: DateFromString,
|
||||
}),
|
||||
lastScheduledActions: t.intersection([
|
||||
t.partial({
|
||||
subgroup: t.string,
|
||||
}),
|
||||
t.type({
|
||||
group: t.string,
|
||||
date: DateFromString,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
export type AlertInstanceMeta = t.TypeOf<typeof metaSchema>;
|
||||
|
||||
|
|
|
@ -28,5 +28,6 @@ export interface AlertInstanceStatus {
|
|||
status: AlertInstanceStatusValues;
|
||||
muted: boolean;
|
||||
actionGroupId?: string;
|
||||
actionSubgroup?: string;
|
||||
activeStartDate?: string;
|
||||
}
|
||||
|
|
|
@ -174,6 +174,134 @@ describe('scheduleActions()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('scheduleActionsWithSubGroup()', () => {
|
||||
test('makes hasScheduledActions() return true', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
state: { foo: true },
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance
|
||||
.replaceState({ otherField: true })
|
||||
.scheduleActionsWithSubGroup('default', 'subgroup', { field: true });
|
||||
expect(alertInstance.hasScheduledActions()).toEqual(true);
|
||||
});
|
||||
|
||||
test('makes isThrottled() return true when throttled and subgroup is the same', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
state: { foo: true },
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
subgroup: 'subgroup',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance
|
||||
.replaceState({ otherField: true })
|
||||
.scheduleActionsWithSubGroup('default', 'subgroup', { field: true });
|
||||
expect(alertInstance.isThrottled('1m')).toEqual(true);
|
||||
});
|
||||
|
||||
test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
state: { foo: true },
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance
|
||||
.replaceState({ otherField: true })
|
||||
.scheduleActionsWithSubGroup('default', 'subgroup', { field: true });
|
||||
expect(alertInstance.isThrottled('1m')).toEqual(true);
|
||||
});
|
||||
|
||||
test('makes isThrottled() return false when throttled and subgroup is the different', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
state: { foo: true },
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
subgroup: 'prev-subgroup',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance
|
||||
.replaceState({ otherField: true })
|
||||
.scheduleActionsWithSubGroup('default', 'subgroup', { field: true });
|
||||
expect(alertInstance.isThrottled('1m')).toEqual(false);
|
||||
});
|
||||
|
||||
test('make isThrottled() return false when throttled expired', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
state: { foo: true },
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
});
|
||||
clock.tick(120000);
|
||||
alertInstance
|
||||
.replaceState({ otherField: true })
|
||||
.scheduleActionsWithSubGroup('default', 'subgroup', { field: true });
|
||||
expect(alertInstance.isThrottled('1m')).toEqual(false);
|
||||
});
|
||||
|
||||
test('makes getScheduledActionOptions() return given options', () => {
|
||||
const alertInstance = new AlertInstance({ state: { foo: true }, meta: {} });
|
||||
alertInstance
|
||||
.replaceState({ otherField: true })
|
||||
.scheduleActionsWithSubGroup('default', 'subgroup', { field: true });
|
||||
expect(alertInstance.getScheduledActionOptions()).toEqual({
|
||||
actionGroup: 'default',
|
||||
subgroup: 'subgroup',
|
||||
context: { field: true },
|
||||
state: { otherField: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('cannot schdule for execution twice', () => {
|
||||
const alertInstance = new AlertInstance();
|
||||
alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true });
|
||||
expect(() =>
|
||||
alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Alert instance execution has already been scheduled, cannot schedule twice"`
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot schdule for execution twice with different subgroups', () => {
|
||||
const alertInstance = new AlertInstance();
|
||||
alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true });
|
||||
expect(() =>
|
||||
alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Alert instance execution has already been scheduled, cannot schedule twice"`
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot schdule for execution twice whether there are subgroups', () => {
|
||||
const alertInstance = new AlertInstance();
|
||||
alertInstance.scheduleActions('default', { field: true });
|
||||
expect(() =>
|
||||
alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Alert instance execution has already been scheduled, cannot schedule twice"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceState()', () => {
|
||||
test('replaces previous state', () => {
|
||||
const alertInstance = new AlertInstance({ state: { foo: true } });
|
||||
|
|
|
@ -13,16 +13,29 @@ import {
|
|||
|
||||
import { parseDuration } from '../lib';
|
||||
|
||||
export type AlertInstances = Record<string, AlertInstance>;
|
||||
interface ScheduledExecutionOptions<
|
||||
State extends AlertInstanceState,
|
||||
Context extends AlertInstanceContext
|
||||
> {
|
||||
actionGroup: string;
|
||||
subgroup?: string;
|
||||
context: Context;
|
||||
state: State;
|
||||
}
|
||||
|
||||
export type PublicAlertInstance<
|
||||
State extends AlertInstanceState = AlertInstanceState,
|
||||
Context extends AlertInstanceContext = AlertInstanceContext
|
||||
> = Pick<
|
||||
AlertInstance<State, Context>,
|
||||
'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup'
|
||||
>;
|
||||
|
||||
export class AlertInstance<
|
||||
State extends AlertInstanceState = AlertInstanceState,
|
||||
Context extends AlertInstanceContext = AlertInstanceContext
|
||||
> {
|
||||
private scheduledExecutionOptions?: {
|
||||
actionGroup: string;
|
||||
context: Context;
|
||||
state: State;
|
||||
};
|
||||
private scheduledExecutionOptions?: ScheduledExecutionOptions<State, Context>;
|
||||
private meta: AlertInstanceMeta;
|
||||
private state: State;
|
||||
|
||||
|
@ -40,10 +53,16 @@ export class AlertInstance<
|
|||
return false;
|
||||
}
|
||||
const throttleMills = throttle ? parseDuration(throttle) : 0;
|
||||
const actionGroup = this.scheduledExecutionOptions.actionGroup;
|
||||
if (
|
||||
this.meta.lastScheduledActions &&
|
||||
this.meta.lastScheduledActions.group === actionGroup &&
|
||||
this.scheduledActionGroupIsUnchanged(
|
||||
this.meta.lastScheduledActions,
|
||||
this.scheduledExecutionOptions
|
||||
) &&
|
||||
this.scheduledActionSubgroupIsUnchanged(
|
||||
this.meta.lastScheduledActions,
|
||||
this.scheduledExecutionOptions
|
||||
) &&
|
||||
this.meta.lastScheduledActions.date.getTime() + throttleMills > Date.now()
|
||||
) {
|
||||
return true;
|
||||
|
@ -51,6 +70,22 @@ export class AlertInstance<
|
|||
return false;
|
||||
}
|
||||
|
||||
private scheduledActionGroupIsUnchanged(
|
||||
lastScheduledActions: NonNullable<AlertInstanceMeta['lastScheduledActions']>,
|
||||
scheduledExecutionOptions: ScheduledExecutionOptions<State, Context>
|
||||
) {
|
||||
return lastScheduledActions.group === scheduledExecutionOptions.actionGroup;
|
||||
}
|
||||
|
||||
private scheduledActionSubgroupIsUnchanged(
|
||||
lastScheduledActions: NonNullable<AlertInstanceMeta['lastScheduledActions']>,
|
||||
scheduledExecutionOptions: ScheduledExecutionOptions<State, Context>
|
||||
) {
|
||||
return lastScheduledActions.subgroup && scheduledExecutionOptions.subgroup
|
||||
? lastScheduledActions.subgroup === scheduledExecutionOptions.subgroup
|
||||
: true;
|
||||
}
|
||||
|
||||
getLastScheduledActions() {
|
||||
return this.meta.lastScheduledActions;
|
||||
}
|
||||
|
@ -68,25 +103,44 @@ export class AlertInstance<
|
|||
return this.state;
|
||||
}
|
||||
|
||||
scheduleActions(actionGroup: string, context?: Context) {
|
||||
if (this.hasScheduledActions()) {
|
||||
throw new Error('Alert instance execution has already been scheduled, cannot schedule twice');
|
||||
}
|
||||
scheduleActions(actionGroup: string, context: Context = {} as Context) {
|
||||
this.ensureHasNoScheduledActions();
|
||||
this.scheduledExecutionOptions = {
|
||||
actionGroup,
|
||||
context: (context || {}) as Context,
|
||||
context,
|
||||
state: this.state,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
scheduleActionsWithSubGroup(
|
||||
actionGroup: string,
|
||||
subgroup: string,
|
||||
context: Context = {} as Context
|
||||
) {
|
||||
this.ensureHasNoScheduledActions();
|
||||
this.scheduledExecutionOptions = {
|
||||
actionGroup,
|
||||
subgroup,
|
||||
context,
|
||||
state: this.state,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
private ensureHasNoScheduledActions() {
|
||||
if (this.hasScheduledActions()) {
|
||||
throw new Error('Alert instance execution has already been scheduled, cannot schedule twice');
|
||||
}
|
||||
}
|
||||
|
||||
replaceState(state: State) {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
updateLastScheduledActions(group: string) {
|
||||
this.meta.lastScheduledActions = { group, date: new Date() };
|
||||
updateLastScheduledActions(group: string, subgroup?: string) {
|
||||
this.meta.lastScheduledActions = { group, subgroup, date: new Date() };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { AlertInstance } from './alert_instance';
|
||||
export { AlertInstance, PublicAlertInstance } from './alert_instance';
|
||||
export { createAlertInstanceFactory } from './create_alert_instance_factory';
|
||||
|
|
|
@ -145,18 +145,21 @@ describe('getAlertInstanceSummary()', () => {
|
|||
"instances": Object {
|
||||
"instance-currently-active": Object {
|
||||
"actionGroupId": "action group A",
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": "2019-02-12T21:01:22.479Z",
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
},
|
||||
"instance-muted-no-activity": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
},
|
||||
"instance-previously-active": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
|
|
|
@ -28,7 +28,7 @@ export {
|
|||
} from './types';
|
||||
export { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
export { FindResult } from './alerts_client';
|
||||
export { AlertInstance } from './alert_instance';
|
||||
export { PublicAlertInstance as AlertInstance } from './alert_instance';
|
||||
export { parseDuration } from './lib';
|
||||
|
||||
export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext);
|
||||
|
|
|
@ -105,12 +105,14 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
},
|
||||
"instance-2": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
|
@ -205,6 +207,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
|
@ -241,6 +244,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
|
@ -276,6 +280,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
|
@ -312,6 +317,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": "action group A",
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": "2020-06-18T00:00:00.000Z",
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
|
@ -348,6 +354,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": "2020-06-18T00:00:00.000Z",
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
|
@ -384,6 +391,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": "action group B",
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": "2020-06-18T00:00:00.000Z",
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
|
@ -419,6 +427,7 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": "action group A",
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
|
@ -458,12 +467,14 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": "action group A",
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": "2020-06-18T00:00:00.000Z",
|
||||
"muted": true,
|
||||
"status": "Active",
|
||||
},
|
||||
"instance-2": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
|
@ -509,12 +520,14 @@ describe('alertInstanceSummaryFromEventLog', () => {
|
|||
"instances": Object {
|
||||
"instance-1": Object {
|
||||
"actionGroupId": "action group B",
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": "2020-06-18T00:00:00.000Z",
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
},
|
||||
"instance-2": Object {
|
||||
"actionGroupId": undefined,
|
||||
"actionSubgroup": undefined,
|
||||
"activeStartDate": undefined,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
|
|
|
@ -79,12 +79,14 @@ export function alertInstanceSummaryFromEventLog(
|
|||
case EVENT_LOG_ACTIONS.activeInstance:
|
||||
status.status = 'Active';
|
||||
status.actionGroupId = event?.kibana?.alerting?.action_group_id;
|
||||
status.actionSubgroup = event?.kibana?.alerting?.action_subgroup;
|
||||
break;
|
||||
case LEGACY_EVENT_LOG_ACTIONS.resolvedInstance:
|
||||
case EVENT_LOG_ACTIONS.recoveredInstance:
|
||||
status.status = 'OK';
|
||||
status.activeStartDate = undefined;
|
||||
status.actionGroupId = undefined;
|
||||
status.actionSubgroup = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,6 +124,7 @@ function getAlertInstanceStatus(
|
|||
status: 'OK',
|
||||
muted: false,
|
||||
actionGroupId: undefined,
|
||||
actionSubgroup: undefined,
|
||||
activeStartDate: undefined,
|
||||
};
|
||||
instances.set(instanceId, status);
|
||||
|
|
|
@ -118,6 +118,7 @@ test('enqueues execution per selected action', async () => {
|
|||
"kibana": Object {
|
||||
"alerting": Object {
|
||||
"action_group_id": "default",
|
||||
"action_subgroup": undefined,
|
||||
"instance_id": "2",
|
||||
},
|
||||
"saved_objects": Array [
|
||||
|
|
|
@ -38,6 +38,7 @@ interface CreateExecutionHandlerOptions {
|
|||
|
||||
interface ExecutionHandlerOptions {
|
||||
actionGroup: string;
|
||||
actionSubgroup?: string;
|
||||
alertInstanceId: string;
|
||||
context: AlertInstanceContext;
|
||||
state: AlertInstanceState;
|
||||
|
@ -60,7 +61,13 @@ export function createExecutionHandler({
|
|||
const alertTypeActionGroups = new Map(
|
||||
alertType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name])
|
||||
);
|
||||
return async ({ actionGroup, context, state, alertInstanceId }: ExecutionHandlerOptions) => {
|
||||
return async ({
|
||||
actionGroup,
|
||||
actionSubgroup,
|
||||
context,
|
||||
state,
|
||||
alertInstanceId,
|
||||
}: ExecutionHandlerOptions) => {
|
||||
if (!alertTypeActionGroups.has(actionGroup)) {
|
||||
logger.error(`Invalid action group "${actionGroup}" for alert "${alertType.id}".`);
|
||||
return;
|
||||
|
@ -78,6 +85,7 @@ export function createExecutionHandler({
|
|||
alertInstanceId,
|
||||
alertActionGroup: actionGroup,
|
||||
alertActionGroupName: alertTypeActionGroups.get(actionGroup)!,
|
||||
alertActionSubgroup: actionSubgroup,
|
||||
context,
|
||||
actionParams: action.params,
|
||||
state,
|
||||
|
@ -120,6 +128,7 @@ export function createExecutionHandler({
|
|||
alerting: {
|
||||
instance_id: alertInstanceId,
|
||||
action_group_id: actionGroup,
|
||||
action_subgroup: actionSubgroup,
|
||||
},
|
||||
saved_objects: [
|
||||
{ rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', id: alertId, ...namespace },
|
||||
|
@ -128,7 +137,11 @@ export function createExecutionHandler({
|
|||
},
|
||||
};
|
||||
|
||||
event.message = `alert: ${alertLabel} instanceId: '${alertInstanceId}' scheduled actionGroup: '${actionGroup}' action: ${actionLabel}`;
|
||||
event.message = `alert: ${alertLabel} instanceId: '${alertInstanceId}' scheduled ${
|
||||
actionSubgroup
|
||||
? `actionGroup(subgroup): '${actionGroup}(${actionSubgroup})'`
|
||||
: `actionGroup: '${actionGroup}'`
|
||||
} action: ${actionLabel}`;
|
||||
eventLogger.logEvent(event);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -230,7 +230,9 @@ describe('Task Runner', () => {
|
|||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
alertType.executor.mockImplementation(
|
||||
({ services: executorServices }: AlertExecutorOptions) => {
|
||||
executorServices.alertInstanceFactory('1').scheduleActions('default');
|
||||
executorServices
|
||||
.alertInstanceFactory('1')
|
||||
.scheduleActionsWithSubGroup('default', 'subDefault');
|
||||
}
|
||||
);
|
||||
const taskRunner = new TaskRunner(
|
||||
|
@ -290,6 +292,7 @@ describe('Task Runner', () => {
|
|||
kibana: {
|
||||
alerting: {
|
||||
action_group_id: 'default',
|
||||
action_subgroup: 'subDefault',
|
||||
instance_id: '1',
|
||||
},
|
||||
saved_objects: [
|
||||
|
@ -311,6 +314,7 @@ describe('Task Runner', () => {
|
|||
alerting: {
|
||||
instance_id: '1',
|
||||
action_group_id: 'default',
|
||||
action_subgroup: 'subDefault',
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -321,7 +325,8 @@ describe('Task Runner', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
message: "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'",
|
||||
message:
|
||||
"test:1: 'alert-name' active instance: '1' in actionGroup(subgroup): 'default(subDefault)'",
|
||||
});
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, {
|
||||
event: {
|
||||
|
@ -331,6 +336,7 @@ describe('Task Runner', () => {
|
|||
alerting: {
|
||||
instance_id: '1',
|
||||
action_group_id: 'default',
|
||||
action_subgroup: 'subDefault',
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -347,7 +353,7 @@ describe('Task Runner', () => {
|
|||
],
|
||||
},
|
||||
message:
|
||||
"alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1",
|
||||
"alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup(subgroup): 'default(subDefault)' action: action:1",
|
||||
});
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, {
|
||||
'@timestamp': '1970-01-01T00:00:00.000Z',
|
||||
|
@ -647,6 +653,7 @@ describe('Task Runner', () => {
|
|||
"kibana": Object {
|
||||
"alerting": Object {
|
||||
"action_group_id": "default",
|
||||
"action_subgroup": undefined,
|
||||
"instance_id": "1",
|
||||
},
|
||||
"saved_objects": Array [
|
||||
|
@ -733,6 +740,7 @@ describe('Task Runner', () => {
|
|||
"lastScheduledActions": Object {
|
||||
"date": 1970-01-01T00:00:00.000Z,
|
||||
"group": "default",
|
||||
"subgroup": undefined,
|
||||
},
|
||||
},
|
||||
"state": Object {
|
||||
|
@ -852,6 +860,7 @@ describe('Task Runner', () => {
|
|||
"lastScheduledActions": Object {
|
||||
"date": 1970-01-01T00:00:00.000Z,
|
||||
"group": "default",
|
||||
"subgroup": undefined,
|
||||
},
|
||||
},
|
||||
"state": Object {
|
||||
|
@ -929,6 +938,7 @@ describe('Task Runner', () => {
|
|||
"lastScheduledActions": Object {
|
||||
"date": 1970-01-01T00:00:00.000Z,
|
||||
"group": "default",
|
||||
"subgroup": undefined,
|
||||
},
|
||||
},
|
||||
"state": Object {
|
||||
|
|
|
@ -152,10 +152,15 @@ export class TaskRunner {
|
|||
alertInstance: AlertInstance,
|
||||
executionHandler: ReturnType<typeof createExecutionHandler>
|
||||
) {
|
||||
const { actionGroup, context, state } = alertInstance.getScheduledActionOptions()!;
|
||||
alertInstance.updateLastScheduledActions(actionGroup);
|
||||
const {
|
||||
actionGroup,
|
||||
subgroup: actionSubgroup,
|
||||
context,
|
||||
state,
|
||||
} = alertInstance.getScheduledActionOptions()!;
|
||||
alertInstance.updateLastScheduledActions(actionGroup, actionSubgroup);
|
||||
alertInstance.unscheduleActions();
|
||||
return executionHandler({ actionGroup, context, state, alertInstanceId });
|
||||
return executionHandler({ actionGroup, actionSubgroup, context, state, alertInstanceId });
|
||||
}
|
||||
|
||||
async executeAlertInstances(
|
||||
|
@ -487,28 +492,40 @@ function generateNewAndRecoveredInstanceEvents(
|
|||
const originalAlertInstanceIds = Object.keys(originalAlertInstances);
|
||||
const currentAlertInstanceIds = Object.keys(currentAlertInstances);
|
||||
const recoveredAlertInstanceIds = Object.keys(recoveredAlertInstances);
|
||||
|
||||
const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds);
|
||||
|
||||
for (const id of recoveredAlertInstanceIds) {
|
||||
const actionGroup = recoveredAlertInstances[id].getLastScheduledActions()?.group;
|
||||
const { group: actionGroup, subgroup: actionSubgroup } =
|
||||
recoveredAlertInstances[id].getLastScheduledActions() ?? {};
|
||||
const message = `${params.alertLabel} instance '${id}' has recovered`;
|
||||
logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup);
|
||||
logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup, actionSubgroup);
|
||||
}
|
||||
|
||||
for (const id of newIds) {
|
||||
const actionGroup = currentAlertInstances[id].getScheduledActionOptions()?.actionGroup;
|
||||
const { actionGroup, subgroup: actionSubgroup } =
|
||||
currentAlertInstances[id].getScheduledActionOptions() ?? {};
|
||||
const message = `${params.alertLabel} created new instance: '${id}'`;
|
||||
logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message, actionGroup);
|
||||
logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message, actionGroup, actionSubgroup);
|
||||
}
|
||||
|
||||
for (const id of currentAlertInstanceIds) {
|
||||
const actionGroup = currentAlertInstances[id].getScheduledActionOptions()?.actionGroup;
|
||||
const message = `${params.alertLabel} active instance: '${id}' in actionGroup: '${actionGroup}'`;
|
||||
logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message, actionGroup);
|
||||
const { actionGroup, subgroup: actionSubgroup } =
|
||||
currentAlertInstances[id].getScheduledActionOptions() ?? {};
|
||||
const message = `${params.alertLabel} active instance: '${id}' in ${
|
||||
actionSubgroup
|
||||
? `actionGroup(subgroup): '${actionGroup}(${actionSubgroup})'`
|
||||
: `actionGroup: '${actionGroup}'`
|
||||
}`;
|
||||
logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message, actionGroup, actionSubgroup);
|
||||
}
|
||||
|
||||
function logInstanceEvent(instanceId: string, action: string, message: string, group?: string) {
|
||||
function logInstanceEvent(
|
||||
instanceId: string,
|
||||
action: string,
|
||||
message: string,
|
||||
group?: string,
|
||||
subgroup?: string
|
||||
) {
|
||||
const event: IEvent = {
|
||||
event: {
|
||||
action,
|
||||
|
@ -517,6 +534,7 @@ function generateNewAndRecoveredInstanceEvents(
|
|||
alerting: {
|
||||
instance_id: instanceId,
|
||||
...(group ? { action_group_id: group } : {}),
|
||||
...(subgroup ? { action_subgroup: subgroup } : {}),
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
|
|
|
@ -21,6 +21,7 @@ interface TransformActionParamsOptions {
|
|||
alertInstanceId: string;
|
||||
alertActionGroup: string;
|
||||
alertActionGroupName: string;
|
||||
alertActionSubgroup?: string;
|
||||
actionParams: AlertActionParams;
|
||||
alertParams: AlertTypeParams;
|
||||
state: AlertInstanceState;
|
||||
|
@ -34,6 +35,7 @@ export function transformActionParams({
|
|||
tags,
|
||||
alertInstanceId,
|
||||
alertActionGroup,
|
||||
alertActionSubgroup,
|
||||
alertActionGroupName,
|
||||
context,
|
||||
actionParams,
|
||||
|
@ -54,6 +56,7 @@ export function transformActionParams({
|
|||
alertInstanceId,
|
||||
alertActionGroup,
|
||||
alertActionGroupName,
|
||||
alertActionSubgroup,
|
||||
context,
|
||||
date: new Date().toISOString(),
|
||||
state,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { AlertInstance } from './alert_instance';
|
||||
import { PublicAlertInstance } from './alert_instance';
|
||||
import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry';
|
||||
import { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
import { AlertsClient } from './alerts_client';
|
||||
|
@ -55,7 +55,7 @@ export interface AlertServices<
|
|||
InstanceState extends AlertInstanceState = AlertInstanceState,
|
||||
InstanceContext extends AlertInstanceContext = AlertInstanceContext
|
||||
> extends Services {
|
||||
alertInstanceFactory: (id: string) => AlertInstance<InstanceState, InstanceContext>;
|
||||
alertInstanceFactory: (id: string) => PublicAlertInstance<InstanceState, InstanceContext>;
|
||||
}
|
||||
|
||||
export interface AlertExecutorOptions<
|
||||
|
|
|
@ -2,3 +2,10 @@ The files in this directory were generated by manually running the script
|
|||
../scripts/create-schemas.js from the root directory of the repository.
|
||||
|
||||
These files should not be edited by hand.
|
||||
|
||||
Please follow the following steps:
|
||||
1. clone the [ECS](https://github.com/elastic/ecs) repo locally so that it resides along side your kibana repo, and checkout the ECS version you wish to support (for example, the `1.6` branch, for version 1.6)
|
||||
2. In the `x-pack/plugins/event_log/scripts/mappings.js` file you'll want to make th efollowing changes:
|
||||
1. Update `EcsKibanaExtensionsMappings` to include the mapping of the fields you wish to add.
|
||||
2. Update `EcsEventLogProperties` to include the fields in the generated mappings.json.
|
||||
3. cd to the `kibana` root folder and run: `node ./x-pack/plugins/event_log/scripts/create_schemas.js`
|
||||
|
|
|
@ -90,6 +90,10 @@
|
|||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"action_subgroup": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"status": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
|
|
|
@ -62,6 +62,7 @@ export const EventSchema = schema.maybe(
|
|||
schema.object({
|
||||
instance_id: ecsString(),
|
||||
action_group_id: ecsString(),
|
||||
action_subgroup: ecsString(),
|
||||
status: ecsString(),
|
||||
})
|
||||
),
|
||||
|
|
|
@ -22,6 +22,10 @@ exports.EcsKibanaExtensionsMappings = {
|
|||
type: 'keyword',
|
||||
ignore_above: 1024,
|
||||
},
|
||||
action_subgroup: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024,
|
||||
},
|
||||
status: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024,
|
||||
|
@ -73,6 +77,7 @@ exports.EcsEventLogProperties = [
|
|||
'kibana.server_uuid',
|
||||
'kibana.alerting.instance_id',
|
||||
'kibana.alerting.action_group_id',
|
||||
'kibana.alerting.action_subgroup',
|
||||
'kibana.alerting.status',
|
||||
'kibana.saved_objects.rel',
|
||||
'kibana.saved_objects.namespace',
|
||||
|
|
|
@ -62,21 +62,35 @@ function getAlwaysFiringAlertType() {
|
|||
updatedBy,
|
||||
} = alertExecutorOptions;
|
||||
let group: string | null = 'default';
|
||||
let subgroup: string | null = null;
|
||||
const alertInfo = { alertId, spaceId, namespace, name, tags, createdBy, updatedBy };
|
||||
|
||||
if (params.groupsToScheduleActionsInSeries) {
|
||||
const index = state.groupInSeriesIndex || 0;
|
||||
group = params.groupsToScheduleActionsInSeries[index];
|
||||
const [scheduledGroup, scheduledSubgroup] = (
|
||||
params.groupsToScheduleActionsInSeries[index] ?? ''
|
||||
).split(':');
|
||||
|
||||
group = scheduledGroup;
|
||||
subgroup = scheduledSubgroup;
|
||||
}
|
||||
|
||||
if (group) {
|
||||
services
|
||||
const instance = services
|
||||
.alertInstanceFactory('1')
|
||||
.replaceState({ instanceStateValue: true })
|
||||
.scheduleActions(group, {
|
||||
.replaceState({ instanceStateValue: true });
|
||||
|
||||
if (subgroup) {
|
||||
instance.scheduleActionsWithSubGroup(group, subgroup, {
|
||||
instanceContextValue: true,
|
||||
});
|
||||
} else {
|
||||
instance.scheduleActions(group, {
|
||||
instanceContextValue: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await services.scopedClusterClient.index({
|
||||
index: params.index,
|
||||
refresh: 'wait_for',
|
||||
|
@ -330,7 +344,10 @@ function getValidationAlertType() {
|
|||
|
||||
function getPatternFiringAlertType() {
|
||||
const paramsSchema = schema.object({
|
||||
pattern: schema.recordOf(schema.string(), schema.arrayOf(schema.boolean())),
|
||||
pattern: schema.recordOf(
|
||||
schema.string(),
|
||||
schema.arrayOf(schema.oneOf([schema.boolean(), schema.string()]))
|
||||
),
|
||||
reference: schema.maybe(schema.string()),
|
||||
});
|
||||
type ParamsType = TypeOf<typeof paramsSchema>;
|
||||
|
@ -375,8 +392,13 @@ function getPatternFiringAlertType() {
|
|||
|
||||
// fire if pattern says to
|
||||
for (const [instanceId, instancePattern] of Object.entries(pattern)) {
|
||||
if (instancePattern[patternIndex]) {
|
||||
const scheduleByPattern = instancePattern[patternIndex];
|
||||
if (scheduleByPattern === true) {
|
||||
services.alertInstanceFactory(instanceId).scheduleActions('default');
|
||||
} else if (typeof scheduleByPattern === 'string') {
|
||||
services
|
||||
.alertInstanceFactory(instanceId)
|
||||
.scheduleActionsWithSubGroup('default', scheduleByPattern);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -841,6 +841,80 @@ instanceStateValue: true
|
|||
}
|
||||
});
|
||||
|
||||
it('should not throttle when changing subgroups', async () => {
|
||||
const testStart = new Date();
|
||||
const reference = alertUtils.generateReference();
|
||||
const response = await alertUtils.createAlwaysFiringAction({
|
||||
reference,
|
||||
overwrites: {
|
||||
schedule: { interval: '1s' },
|
||||
params: {
|
||||
index: ES_TEST_INDEX_NAME,
|
||||
reference,
|
||||
groupsToScheduleActionsInSeries: ['default:prev', 'default:next'],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: indexRecordActionId,
|
||||
params: {
|
||||
index: ES_TEST_INDEX_NAME,
|
||||
reference,
|
||||
message: 'from:{{alertActionGroup}}:{{alertActionSubgroup}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: getConsumerUnauthorizedErrorMessage(
|
||||
'create',
|
||||
'test.always-firing',
|
||||
'alertsFixture'
|
||||
),
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: `Unauthorized to get actions`,
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'superuser at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
// Wait for actions to execute twice before disabling the alert and waiting for tasks to finish
|
||||
await esTestIndexTool.waitForDocs('action:test.index-record', reference, 2);
|
||||
await alertUtils.disable(response.body.id);
|
||||
await taskManagerUtils.waitForEmpty(testStart);
|
||||
|
||||
// Ensure only 2 actions with proper params exists
|
||||
const searchResult = await esTestIndexTool.search(
|
||||
'action:test.index-record',
|
||||
reference
|
||||
);
|
||||
expect(searchResult.hits.total.value).to.eql(2);
|
||||
const messages: string[] = searchResult.hits.hits.map(
|
||||
(hit: { _source: { params: { message: string } } }) => hit._source.params.message
|
||||
);
|
||||
expect(messages.sort()).to.eql(['from:default:next', 'from:default:prev']);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should reset throttle window when not firing', async () => {
|
||||
const testStart = new Date();
|
||||
const reference = alertUtils.generateReference();
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import uuid from 'uuid';
|
||||
import { Spaces } from '../../scenarios';
|
||||
import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
@ -153,6 +154,147 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
|
|||
}
|
||||
});
|
||||
|
||||
it('should generate expected events for normal operation with subgroups', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'MY action',
|
||||
actionTypeId: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// pattern of when the alert should fire
|
||||
const [firstSubgroup, secondSubgroup] = [uuid.v4(), uuid.v4()];
|
||||
const pattern = {
|
||||
instance: [false, firstSubgroup, secondSubgroup],
|
||||
};
|
||||
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
alertTypeId: 'test.patternFiring',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
params: {
|
||||
pattern,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
const alertId = response.body.id;
|
||||
objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts');
|
||||
|
||||
// get the events we're expecting
|
||||
const events = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: Spaces.space1.id,
|
||||
type: 'alert',
|
||||
id: alertId,
|
||||
provider: 'alerting',
|
||||
actions: new Map([
|
||||
// make sure the counts of the # of events per type are as expected
|
||||
['execute', { gte: 4 }],
|
||||
['execute-action', { equal: 2 }],
|
||||
['new-instance', { equal: 1 }],
|
||||
['active-instance', { gte: 2 }],
|
||||
['recovered-instance', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvents = getEventsByAction(events, 'execute');
|
||||
const executeActionEvents = getEventsByAction(events, 'execute-action');
|
||||
const newInstanceEvents = getEventsByAction(events, 'new-instance');
|
||||
const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance');
|
||||
|
||||
// make sure the events are in the right temporal order
|
||||
const executeTimes = getTimestamps(executeEvents);
|
||||
const executeActionTimes = getTimestamps(executeActionEvents);
|
||||
const newInstanceTimes = getTimestamps(newInstanceEvents);
|
||||
const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents);
|
||||
|
||||
expect(executeTimes[0] < newInstanceTimes[0]).to.be(true);
|
||||
expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true);
|
||||
expect(executeTimes[2] > newInstanceTimes[0]).to.be(true);
|
||||
expect(executeTimes[1] <= executeActionTimes[0]).to.be(true);
|
||||
expect(executeTimes[2] > executeActionTimes[0]).to.be(true);
|
||||
expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true);
|
||||
|
||||
// validate each event
|
||||
let executeCount = 0;
|
||||
const executeStatuses = ['ok', 'active', 'active'];
|
||||
for (const event of events) {
|
||||
switch (event?.event?.action) {
|
||||
case 'execute':
|
||||
validateEvent(event, {
|
||||
spaceId: Spaces.space1.id,
|
||||
savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }],
|
||||
outcome: 'success',
|
||||
message: `alert executed: test.patternFiring:${alertId}: 'abc'`,
|
||||
status: executeStatuses[executeCount++],
|
||||
});
|
||||
break;
|
||||
case 'execute-action':
|
||||
expect(
|
||||
[firstSubgroup, secondSubgroup].includes(event?.kibana?.alerting?.action_subgroup!)
|
||||
).to.be(true);
|
||||
validateEvent(event, {
|
||||
spaceId: Spaces.space1.id,
|
||||
savedObjects: [
|
||||
{ type: 'alert', id: alertId, rel: 'primary' },
|
||||
{ type: 'action', id: createdAction.id },
|
||||
],
|
||||
message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})' action: test.noop:${createdAction.id}`,
|
||||
instanceId: 'instance',
|
||||
actionGroupId: 'default',
|
||||
});
|
||||
break;
|
||||
case 'new-instance':
|
||||
validateInstanceEvent(event, `created new instance: 'instance'`);
|
||||
break;
|
||||
case 'recovered-instance':
|
||||
validateInstanceEvent(event, `instance 'instance' has recovered`);
|
||||
break;
|
||||
case 'active-instance':
|
||||
expect(
|
||||
[firstSubgroup, secondSubgroup].includes(event?.kibana?.alerting?.action_subgroup!)
|
||||
).to.be(true);
|
||||
validateInstanceEvent(
|
||||
event,
|
||||
`active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'`
|
||||
);
|
||||
break;
|
||||
// this will get triggered as we add new event actions
|
||||
default:
|
||||
throw new Error(`unexpected event action "${event?.event?.action}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateInstanceEvent(event: IValidatedEvent, subMessage: string) {
|
||||
validateEvent(event, {
|
||||
spaceId: Spaces.space1.id,
|
||||
savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }],
|
||||
message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`,
|
||||
instanceId: 'instance',
|
||||
actionGroupId: 'default',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should generate events for execution errors', async () => {
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue