Skip to content

Commit

Permalink
added support for action subgroups
Browse files Browse the repository at this point in the history
  • Loading branch information
gmmorris committed Dec 2, 2020
1 parent 88580c5 commit 79abcaa
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 30 deletions.
13 changes: 9 additions & 4 deletions x-pack/plugins/alerts/common/alert_instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

Expand Down
128 changes: 128 additions & 0 deletions x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } });
Expand Down
80 changes: 66 additions & 14 deletions x-pack/plugins/alerts/server/alert_instance/alert_instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,32 @@ import {

import { parseDuration } from '../lib';

export type AlertInstances<
interface ScheduledExecutionOptions<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string
> {
actionGroup: ActionGroupIds;
subgroup?: string;
context: Context;
state: State;
}

export type PublicAlertInstance<
State extends AlertInstanceState = AlertInstanceState,
Context extends AlertInstanceContext = AlertInstanceContext,
ActionGroupIds extends string = string
> = Record<string, AlertInstance<State, Context, ActionGroupIds>>;
> = Pick<
AlertInstance<State, Context, ActionGroupIds>,
'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup'
>;

export class AlertInstance<
State extends AlertInstanceState = AlertInstanceState,
Context extends AlertInstanceContext = AlertInstanceContext,
ActionGroupIds extends string = string
> {
private scheduledExecutionOptions?: {
actionGroup: ActionGroupIds;
context: Context;
state: State;
};
private scheduledExecutionOptions?: ScheduledExecutionOptions<State, Context, ActionGroupIds>;
private meta: AlertInstanceMeta;
private state: State;

Expand All @@ -45,17 +56,39 @@ 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;
}
return false;
}

private scheduledActionGroupIsUnchanged(
lastScheduledActions: NonNullable<AlertInstanceMeta['lastScheduledActions']>,
scheduledExecutionOptions: ScheduledExecutionOptions<State, Context, ActionGroupIds>
) {
return lastScheduledActions.group === scheduledExecutionOptions.actionGroup;
}

private scheduledActionSubgroupIsUnchanged(
lastScheduledActions: NonNullable<AlertInstanceMeta['lastScheduledActions']>,
scheduledExecutionOptions: ScheduledExecutionOptions<State, Context, ActionGroupIds>
) {
return lastScheduledActions.subgroup && scheduledExecutionOptions.subgroup
? lastScheduledActions.subgroup === scheduledExecutionOptions.subgroup
: true;
}

getLastScheduledActions() {
return this.meta.lastScheduledActions;
}
Expand All @@ -74,24 +107,43 @@ export class AlertInstance<
}

scheduleActions(actionGroup: ActionGroupIds, context: Context = {} as Context) {
if (this.hasScheduledActions()) {
throw new Error('Alert instance execution has already been scheduled, cannot schedule twice');
}
this.ensureHasNoScheduledActions();
this.scheduledExecutionOptions = {
actionGroup,
context,
state: this.state,
};
return this;
}

scheduleActionsWithSubGroup(
actionGroup: ActionGroupIds,
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() };
}

/**
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/alerts/server/alert_instance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 1 addition & 1 deletion x-pack/plugins/alerts/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface CreateExecutionHandlerOptions {

interface ExecutionHandlerOptions {
actionGroup: string;
actionSubgroup?: string;
alertInstanceId: string;
context: AlertInstanceContext;
state: AlertInstanceState;
Expand All @@ -59,7 +60,13 @@ export function createExecutionHandler({
alertParams,
}: CreateExecutionHandlerOptions) {
const alertTypeActionGroups = new Set(map(alertType.actionGroups, 'id'));
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;
Expand All @@ -76,6 +83,7 @@ export function createExecutionHandler({
tags,
alertInstanceId,
alertActionGroup: actionGroup,
alertActionSubgroup: actionSubgroup,
context,
actionParams: action.params,
state,
Expand Down
11 changes: 8 additions & 3 deletions x-pack/plugins/alerts/server/task_runner/task_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface TransformActionParamsOptions {
tags?: string[];
alertInstanceId: string;
alertActionGroup: string;
alertActionSubgroup?: string;
actionParams: AlertActionParams;
alertParams: AlertTypeParams;
state: AlertInstanceState;
Expand All @@ -33,6 +34,7 @@ export function transformActionParams({
tags,
alertInstanceId,
alertActionGroup,
alertActionSubgroup,
context,
actionParams,
state,
Expand All @@ -51,6 +53,7 @@ export function transformActionParams({
tags,
alertInstanceId,
alertActionGroup,
alertActionSubgroup,
context,
date: new Date().toISOString(),
state,
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/alerts/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -58,7 +58,7 @@ export interface AlertServices<
> extends Services {
alertInstanceFactory: (
id: string
) => AlertInstance<InstanceState, InstanceContext, ActionGroupIds>;
) => PublicAlertInstance<InstanceState, InstanceContext, ActionGroupIds>;
}

export interface AlertExecutorOptions<
Expand Down
Loading

0 comments on commit 79abcaa

Please sign in to comment.