mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Add circuit breaker for max number of actions by connector type (#128319)
* connectorTypeOverrides key in kibana.yml can create a connector type specific action config. * Update docs and docker allowed keys
This commit is contained in:
parent
3ad6452166
commit
f44e198784
31 changed files with 827 additions and 362 deletions
|
@ -211,13 +211,26 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `5m`.
|
|||
`xpack.alerting.rules.run.ruleTypeOverrides`::
|
||||
Overrides the configs under `xpack.alerting.rules.run` for the rule type with the given ID. List the rule identifier and its settings in an array of objects.
|
||||
+
|
||||
--
|
||||
For example:
|
||||
```
|
||||
[source,yaml]
|
||||
--
|
||||
xpack.alerting.rules.run:
|
||||
timeout: '5m'
|
||||
ruleTypeOverrides:
|
||||
- id: '.index-threshold'
|
||||
timeout: '15m'
|
||||
```
|
||||
--
|
||||
--
|
||||
|
||||
`xpack.alerting.rules.run.actions.connectorTypeOverrides`::
|
||||
Overrides the configs under `xpack.alerting.rules.run.actions` for the connector type with the given ID. List the connector type identifier and its settings in an array of objects.
|
||||
+
|
||||
For example:
|
||||
[source,yaml]
|
||||
--
|
||||
xpack.alerting.rules.run:
|
||||
actions:
|
||||
max: 10
|
||||
connectorTypeOverrides:
|
||||
- id: '.server-log'
|
||||
max: 5
|
||||
--
|
||||
|
|
|
@ -211,6 +211,7 @@ kibana_vars=(
|
|||
xpack.alerting.rules.minimumScheduleInterval.value
|
||||
xpack.alerting.rules.minimumScheduleInterval.enforce
|
||||
xpack.alerting.rules.run.actions.max
|
||||
xpack.alerting.rules.run.actions.connectorTypeOverrides
|
||||
xpack.alerts.healthCheck.interval
|
||||
xpack.alerts.invalidateApiKeysTask.interval
|
||||
xpack.alerts.invalidateApiKeysTask.removalDelay
|
||||
|
|
|
@ -22,7 +22,7 @@ const ruleExecutionMetricsSchema = t.partial({
|
|||
esSearchDurationMs: t.number,
|
||||
});
|
||||
|
||||
const alertExecutionStore = t.partial({
|
||||
const alertExecutionMetrics = t.partial({
|
||||
numberOfTriggeredActions: t.number,
|
||||
numberOfGeneratedActions: t.number,
|
||||
triggeredActionsStatus: t.string,
|
||||
|
@ -32,7 +32,7 @@ export type RuleExecutionMetrics = t.TypeOf<typeof ruleExecutionMetricsSchema>;
|
|||
export type RuleTaskState = t.TypeOf<typeof ruleStateSchema>;
|
||||
export type RuleExecutionState = RuleTaskState & {
|
||||
metrics: RuleExecutionMetrics;
|
||||
alertExecutionStore: t.TypeOf<typeof alertExecutionStore>;
|
||||
alertExecutionMetrics: t.TypeOf<typeof alertExecutionMetrics>;
|
||||
};
|
||||
|
||||
export const ruleParamsSchema = t.intersection([
|
||||
|
|
|
@ -14,6 +14,11 @@ const ruleTypeSchema = schema.object({
|
|||
timeout: schema.maybe(schema.string({ validate: validateDurationSchema })),
|
||||
});
|
||||
|
||||
const connectorTypeSchema = schema.object({
|
||||
id: schema.string(),
|
||||
max: schema.maybe(schema.number({ max: 100000 })),
|
||||
});
|
||||
|
||||
const rulesSchema = schema.object({
|
||||
minimumScheduleInterval: schema.object({
|
||||
value: schema.string({
|
||||
|
@ -36,6 +41,7 @@ const rulesSchema = schema.object({
|
|||
timeout: schema.maybe(schema.string({ validate: validateDurationSchema })),
|
||||
actions: schema.object({
|
||||
max: schema.number({ defaultValue: 100000, max: 100000 }),
|
||||
connectorTypeOverrides: schema.maybe(schema.arrayOf(connectorTypeSchema)),
|
||||
}),
|
||||
ruleTypeOverrides: schema.maybe(schema.arrayOf(ruleTypeSchema)),
|
||||
}),
|
||||
|
@ -59,5 +65,6 @@ export const configSchema = schema.object({
|
|||
|
||||
export type AlertingConfig = TypeOf<typeof configSchema>;
|
||||
export type RulesConfig = TypeOf<typeof rulesSchema>;
|
||||
export type RuleTypeConfig = Omit<RulesConfig, 'ruleTypeOverrides' | 'minimumScheduleInterval'>;
|
||||
export type AlertingRulesConfig = Pick<AlertingConfig['rules'], 'minimumScheduleInterval'>;
|
||||
export type ActionsConfig = RulesConfig['run']['actions'];
|
||||
export type ActionTypeConfig = Omit<ActionsConfig, 'connectorTypeOverrides'>;
|
||||
|
|
115
x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts
Normal file
115
x-pack/plugins/alerting/server/lib/alert_execution_store.test.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertExecutionStore } from './alert_execution_store';
|
||||
import { ActionsCompletion } from '../task_runner/types';
|
||||
|
||||
describe('AlertExecutionStore', () => {
|
||||
const alertExecutionStore = new AlertExecutionStore();
|
||||
const testConnectorId = 'test-connector-id';
|
||||
|
||||
// Getter Setter
|
||||
test('returns the default values if there is no change', () => {
|
||||
expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE);
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(0);
|
||||
expect(alertExecutionStore.getStatusByConnectorType('any')).toBe(undefined);
|
||||
});
|
||||
|
||||
test('sets and returns numberOfTriggeredActions', () => {
|
||||
alertExecutionStore.setNumberOfTriggeredActions(5);
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(5);
|
||||
});
|
||||
|
||||
test('sets and returns numberOfGeneratedActions', () => {
|
||||
alertExecutionStore.setNumberOfGeneratedActions(15);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(15);
|
||||
});
|
||||
|
||||
test('sets and returns triggeredActionsStatusByConnectorType', () => {
|
||||
alertExecutionStore.setTriggeredActionsStatusByConnectorType({
|
||||
actionTypeId: testConnectorId,
|
||||
status: ActionsCompletion.PARTIAL,
|
||||
});
|
||||
expect(
|
||||
alertExecutionStore.getStatusByConnectorType(testConnectorId).triggeredActionsStatus
|
||||
).toBe(ActionsCompletion.PARTIAL);
|
||||
expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL);
|
||||
});
|
||||
|
||||
// increment
|
||||
test('increments numberOfTriggeredActions by 1', () => {
|
||||
alertExecutionStore.incrementNumberOfTriggeredActions();
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(6);
|
||||
});
|
||||
|
||||
test('increments incrementNumberOfGeneratedActions by x', () => {
|
||||
alertExecutionStore.incrementNumberOfGeneratedActions(2);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(17);
|
||||
});
|
||||
|
||||
test('increments numberOfTriggeredActionsByConnectorType by 1', () => {
|
||||
alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId);
|
||||
expect(
|
||||
alertExecutionStore.getStatusByConnectorType(testConnectorId).numberOfTriggeredActions
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('increments NumberOfGeneratedActionsByConnectorType by 1', () => {
|
||||
alertExecutionStore.incrementNumberOfGeneratedActionsByConnectorType(testConnectorId);
|
||||
expect(
|
||||
alertExecutionStore.getStatusByConnectorType(testConnectorId).numberOfGeneratedActions
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
// Checker
|
||||
test('checks if it has reached the executable actions limit', () => {
|
||||
expect(alertExecutionStore.hasReachedTheExecutableActionsLimit({ default: { max: 10 } })).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
expect(alertExecutionStore.hasReachedTheExecutableActionsLimit({ default: { max: 5 } })).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('checks if it has reached the executable actions limit by connector type', () => {
|
||||
alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId);
|
||||
alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId);
|
||||
alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId);
|
||||
alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId);
|
||||
alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(testConnectorId);
|
||||
|
||||
expect(
|
||||
alertExecutionStore.hasReachedTheExecutableActionsLimitByConnectorType({
|
||||
actionsConfigMap: {
|
||||
default: { max: 20 },
|
||||
[testConnectorId]: {
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
actionTypeId: testConnectorId,
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
alertExecutionStore.hasReachedTheExecutableActionsLimitByConnectorType({
|
||||
actionsConfigMap: {
|
||||
default: { max: 20 },
|
||||
[testConnectorId]: {
|
||||
max: 8,
|
||||
},
|
||||
},
|
||||
actionTypeId: testConnectorId,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('checks if a connector type it has already reached the executable actions limit', () => {
|
||||
expect(alertExecutionStore.hasConnectorTypeReachedTheLimit(testConnectorId)).toBe(true);
|
||||
});
|
||||
});
|
106
x-pack/plugins/alerting/server/lib/alert_execution_store.ts
Normal file
106
x-pack/plugins/alerting/server/lib/alert_execution_store.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { set } from 'lodash';
|
||||
import { ActionsConfigMap } from './get_actions_config_map';
|
||||
import { ActionsCompletion } from '../task_runner/types';
|
||||
|
||||
interface State {
|
||||
numberOfTriggeredActions: number;
|
||||
numberOfGeneratedActions: number;
|
||||
connectorTypes: {
|
||||
[key: string]: {
|
||||
triggeredActionsStatus: ActionsCompletion;
|
||||
numberOfTriggeredActions: number;
|
||||
numberOfGeneratedActions: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class AlertExecutionStore {
|
||||
private state: State = {
|
||||
numberOfTriggeredActions: 0,
|
||||
numberOfGeneratedActions: 0,
|
||||
connectorTypes: {},
|
||||
};
|
||||
|
||||
// Getters
|
||||
public getTriggeredActionsStatus = () => {
|
||||
const hasPartial = Object.values(this.state.connectorTypes).some(
|
||||
(connectorType) => connectorType?.triggeredActionsStatus === ActionsCompletion.PARTIAL
|
||||
);
|
||||
return hasPartial ? ActionsCompletion.PARTIAL : ActionsCompletion.COMPLETE;
|
||||
};
|
||||
public getNumberOfTriggeredActions = () => {
|
||||
return this.state.numberOfTriggeredActions;
|
||||
};
|
||||
public getNumberOfGeneratedActions = () => {
|
||||
return this.state.numberOfGeneratedActions;
|
||||
};
|
||||
public getStatusByConnectorType = (actionTypeId: string) => {
|
||||
return this.state.connectorTypes[actionTypeId];
|
||||
};
|
||||
|
||||
// Setters
|
||||
public setNumberOfTriggeredActions = (numberOfTriggeredActions: number) => {
|
||||
this.state.numberOfTriggeredActions = numberOfTriggeredActions;
|
||||
};
|
||||
|
||||
public setNumberOfGeneratedActions = (numberOfGeneratedActions: number) => {
|
||||
this.state.numberOfGeneratedActions = numberOfGeneratedActions;
|
||||
};
|
||||
|
||||
public setTriggeredActionsStatusByConnectorType = ({
|
||||
actionTypeId,
|
||||
status,
|
||||
}: {
|
||||
actionTypeId: string;
|
||||
status: ActionsCompletion;
|
||||
}) => {
|
||||
set(this.state, `connectorTypes["${actionTypeId}"].triggeredActionsStatus`, status);
|
||||
};
|
||||
|
||||
// Checkers
|
||||
public hasReachedTheExecutableActionsLimit = (actionsConfigMap: ActionsConfigMap): boolean =>
|
||||
this.state.numberOfTriggeredActions >= actionsConfigMap.default.max;
|
||||
|
||||
public hasReachedTheExecutableActionsLimitByConnectorType = ({
|
||||
actionsConfigMap,
|
||||
actionTypeId,
|
||||
}: {
|
||||
actionsConfigMap: ActionsConfigMap;
|
||||
actionTypeId: string;
|
||||
}): boolean => {
|
||||
const numberOfTriggeredActionsByConnectorType =
|
||||
this.state.connectorTypes[actionTypeId]?.numberOfTriggeredActions || 0;
|
||||
const executableActionsLimitByConnectorType =
|
||||
actionsConfigMap[actionTypeId]?.max || actionsConfigMap.default.max;
|
||||
|
||||
return numberOfTriggeredActionsByConnectorType >= executableActionsLimitByConnectorType;
|
||||
};
|
||||
|
||||
public hasConnectorTypeReachedTheLimit = (actionTypeId: string) =>
|
||||
this.state.connectorTypes[actionTypeId]?.triggeredActionsStatus === ActionsCompletion.PARTIAL;
|
||||
|
||||
// Incrementer
|
||||
public incrementNumberOfTriggeredActions = () => {
|
||||
this.state.numberOfTriggeredActions++;
|
||||
};
|
||||
|
||||
public incrementNumberOfGeneratedActions = (incrementBy: number) => {
|
||||
this.state.numberOfGeneratedActions += incrementBy;
|
||||
};
|
||||
|
||||
public incrementNumberOfTriggeredActionsByConnectorType = (actionTypeId: string) => {
|
||||
const currentVal = this.state.connectorTypes[actionTypeId]?.numberOfTriggeredActions || 0;
|
||||
set(this.state, `connectorTypes["${actionTypeId}"].numberOfTriggeredActions`, currentVal + 1);
|
||||
};
|
||||
public incrementNumberOfGeneratedActionsByConnectorType = (actionTypeId: string) => {
|
||||
const currentVal = this.state.connectorTypes[actionTypeId]?.numberOfGeneratedActions || 0;
|
||||
set(this.state, `connectorTypes["${actionTypeId}"].numberOfGeneratedActions`, currentVal + 1);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getActionsConfigMap } from './get_actions_config_map';
|
||||
|
||||
const connectorTypeId = 'test-connector-type-id';
|
||||
const actionsConfig = {
|
||||
max: 1000,
|
||||
};
|
||||
|
||||
const actionsConfigWithConnectorType = {
|
||||
...actionsConfig,
|
||||
connectorTypeOverrides: [
|
||||
{
|
||||
id: connectorTypeId,
|
||||
max: 20,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('get actions config map', () => {
|
||||
test('returns the default actions config', () => {
|
||||
expect(getActionsConfigMap(actionsConfig)).toEqual({
|
||||
default: {
|
||||
max: 1000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('applies the connector type specific config', () => {
|
||||
expect(getActionsConfigMap(actionsConfigWithConnectorType)).toEqual({
|
||||
default: {
|
||||
max: 1000,
|
||||
},
|
||||
[connectorTypeId]: {
|
||||
max: 20,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
27
x-pack/plugins/alerting/server/lib/get_actions_config_map.ts
Normal file
27
x-pack/plugins/alerting/server/lib/get_actions_config_map.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { ActionsConfig, ActionTypeConfig } from '../config';
|
||||
|
||||
export interface ActionsConfigMap {
|
||||
default: ActionTypeConfig;
|
||||
[key: string]: ActionTypeConfig;
|
||||
}
|
||||
|
||||
export const getActionsConfigMap = (actionsConfig: ActionsConfig): ActionsConfigMap => {
|
||||
const configsByConnectorType = actionsConfig.connectorTypeOverrides?.reduce(
|
||||
(config, configByConnectorType) => {
|
||||
return { ...config, [configByConnectorType.id]: omit(configByConnectorType, 'id') };
|
||||
},
|
||||
{}
|
||||
);
|
||||
return {
|
||||
default: omit(actionsConfig, 'connectorTypeOverrides'),
|
||||
...configsByConnectorType,
|
||||
};
|
||||
};
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getExecutionConfigForRuleType } from './get_rules_config';
|
||||
import { RulesConfig } from '../config';
|
||||
|
||||
const ruleTypeId = 'test-rule-type-id';
|
||||
const config = {
|
||||
minimumScheduleInterval: {
|
||||
value: '2m',
|
||||
enforce: false,
|
||||
},
|
||||
run: {
|
||||
timeout: '1m',
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
} as RulesConfig;
|
||||
|
||||
const configWithRuleType = {
|
||||
...config,
|
||||
run: {
|
||||
...config.run,
|
||||
ruleTypeOverrides: [
|
||||
{
|
||||
id: ruleTypeId,
|
||||
actions: { max: 20 },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('get rules config', () => {
|
||||
test('returns the rule type specific config and keeps the default values that are not overwritten', () => {
|
||||
expect(getExecutionConfigForRuleType({ config: configWithRuleType, ruleTypeId })).toEqual({
|
||||
run: {
|
||||
id: ruleTypeId,
|
||||
timeout: '1m',
|
||||
actions: { max: 20 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('returns the default config when there is no rule type specific config', () => {
|
||||
expect(getExecutionConfigForRuleType({ config, ruleTypeId })).toEqual({
|
||||
run: config.run,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { RulesConfig, RuleTypeConfig } from '../config';
|
||||
|
||||
export const getExecutionConfigForRuleType = ({
|
||||
config,
|
||||
ruleTypeId,
|
||||
}: {
|
||||
config: RulesConfig;
|
||||
ruleTypeId: string;
|
||||
}): RuleTypeConfig => {
|
||||
const ruleTypeExecutionConfig = config.run.ruleTypeOverrides?.find(
|
||||
(ruleType) => ruleType.id === ruleTypeId
|
||||
);
|
||||
|
||||
return {
|
||||
run: {
|
||||
...omit(config.run, 'ruleTypeOverrides'),
|
||||
...ruleTypeExecutionConfig,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -32,7 +32,7 @@ describe('RuleExecutionStatus', () => {
|
|||
describe('executionStatusFromState()', () => {
|
||||
test('empty task state', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertExecutionStore: {
|
||||
alertExecutionMetrics: {
|
||||
numberOfTriggeredActions: 0,
|
||||
numberOfGeneratedActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
|
@ -49,7 +49,7 @@ describe('RuleExecutionStatus', () => {
|
|||
test('task state with no instances', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertInstances: {},
|
||||
alertExecutionStore: {
|
||||
alertExecutionMetrics: {
|
||||
numberOfTriggeredActions: 0,
|
||||
numberOfGeneratedActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
|
@ -68,7 +68,7 @@ describe('RuleExecutionStatus', () => {
|
|||
test('task state with one instance', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertInstances: { a: {} },
|
||||
alertExecutionStore: {
|
||||
alertExecutionMetrics: {
|
||||
numberOfTriggeredActions: 0,
|
||||
numberOfGeneratedActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
|
@ -86,7 +86,7 @@ describe('RuleExecutionStatus', () => {
|
|||
|
||||
test('task state with numberOfTriggeredActions', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertExecutionStore: {
|
||||
alertExecutionMetrics: {
|
||||
numberOfTriggeredActions: 1,
|
||||
numberOfGeneratedActions: 2,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
|
@ -106,7 +106,7 @@ describe('RuleExecutionStatus', () => {
|
|||
test('task state with warning', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertInstances: { a: {} },
|
||||
alertExecutionStore: {
|
||||
alertExecutionMetrics: {
|
||||
numberOfTriggeredActions: 3,
|
||||
triggeredActionsStatus: ActionsCompletion.PARTIAL,
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ export function executionStatusFromState(state: RuleExecutionState): RuleExecuti
|
|||
const alertIds = Object.keys(state.alertInstances ?? {});
|
||||
|
||||
const hasIncompleteAlertExecution =
|
||||
state.alertExecutionStore.triggeredActionsStatus === ActionsCompletion.PARTIAL;
|
||||
state.alertExecutionMetrics.triggeredActionsStatus === ActionsCompletion.PARTIAL;
|
||||
|
||||
let status: RuleExecutionStatuses =
|
||||
alertIds.length === 0 ? RuleExecutionStatusValues[0] : RuleExecutionStatusValues[1];
|
||||
|
@ -34,8 +34,8 @@ export function executionStatusFromState(state: RuleExecutionState): RuleExecuti
|
|||
|
||||
return {
|
||||
metrics: state.metrics,
|
||||
numberOfTriggeredActions: state.alertExecutionStore.numberOfTriggeredActions,
|
||||
numberOfGeneratedActions: state.alertExecutionStore.numberOfGeneratedActions,
|
||||
numberOfTriggeredActions: state.alertExecutionMetrics.numberOfTriggeredActions,
|
||||
numberOfGeneratedActions: state.alertExecutionMetrics.numberOfGeneratedActions,
|
||||
lastExecutionDate: new Date(),
|
||||
status,
|
||||
...(hasIncompleteAlertExecution && {
|
||||
|
|
|
@ -50,13 +50,6 @@ const sampleRuleType: RuleType<never, never, never, never, never, 'default'> = {
|
|||
actionGroups: [],
|
||||
defaultActionGroupId: 'default',
|
||||
producer: 'test',
|
||||
config: {
|
||||
run: {
|
||||
actions: {
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
async executor() {},
|
||||
};
|
||||
|
||||
|
@ -122,61 +115,6 @@ describe('Alerting Plugin', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`applies the default config if there is no rule type specific config `, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
...generateAlertingConfig(),
|
||||
rules: {
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
run: {
|
||||
actions: {
|
||||
max: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
plugin = new AlertingPlugin(context);
|
||||
|
||||
const setupContract = await plugin.setup(setupMocks, mockPlugins);
|
||||
|
||||
const ruleType = { ...sampleRuleType };
|
||||
setupContract.registerType(ruleType);
|
||||
|
||||
expect(ruleType.config).toEqual({
|
||||
run: {
|
||||
actions: { max: 123 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`applies rule type specific config if defined in config`, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
...generateAlertingConfig(),
|
||||
rules: {
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
run: {
|
||||
actions: { max: 123 },
|
||||
ruleTypeOverrides: [{ id: sampleRuleType.id, timeout: '1d' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
plugin = new AlertingPlugin(context);
|
||||
|
||||
const setupContract = await plugin.setup(setupMocks, mockPlugins);
|
||||
|
||||
const ruleType = { ...sampleRuleType };
|
||||
setupContract.registerType(ruleType);
|
||||
|
||||
expect(ruleType.config).toEqual({
|
||||
run: {
|
||||
id: sampleRuleType.id,
|
||||
actions: {
|
||||
max: 123,
|
||||
},
|
||||
timeout: '1d',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerType()', () => {
|
||||
let setup: PluginSetupContract;
|
||||
beforeEach(async () => {
|
||||
|
|
|
@ -77,8 +77,8 @@ import { AlertingAuthorizationClientFactory } from './alerting_authorization_cli
|
|||
import { AlertingAuthorization } from './authorization';
|
||||
import { getSecurityHealth, SecurityHealth } from './lib/get_security_health';
|
||||
import { registerNodeCollector, registerClusterCollector, InMemoryMetrics } from './monitoring';
|
||||
import { getExecutionConfigForRuleType } from './lib/get_rules_config';
|
||||
import { getRuleTaskTimeout } from './lib/get_rule_task_timeout';
|
||||
import { getActionsConfigMap } from './lib/get_actions_config_map';
|
||||
|
||||
export const EVENT_LOG_PROVIDER = 'alerting';
|
||||
export const EVENT_LOG_ACTIONS = {
|
||||
|
@ -319,10 +319,6 @@ export class AlertingPlugin {
|
|||
if (!(ruleType.minimumLicenseRequired in LICENSE_TYPE)) {
|
||||
throw new Error(`"${ruleType.minimumLicenseRequired}" is not a valid license type`);
|
||||
}
|
||||
ruleType.config = getExecutionConfigForRuleType({
|
||||
config: this.config.rules,
|
||||
ruleTypeId: ruleType.id,
|
||||
});
|
||||
ruleType.ruleTaskTimeout = getRuleTaskTimeout({
|
||||
config: this.config.rules,
|
||||
ruleTaskTimeout: ruleType.ruleTaskTimeout,
|
||||
|
@ -437,6 +433,7 @@ export class AlertingPlugin {
|
|||
supportsEphemeralTasks: plugins.taskManager.supportsEphemeralTasks(),
|
||||
maxEphemeralActionsPerRule: this.config.maxEphemeralActionsPerAlert,
|
||||
cancelAlertsOnRuleTimeout: this.config.cancelAlertsOnRuleTimeout,
|
||||
actionsConfigMap: getActionsConfigMap(this.config.rules.run.actions),
|
||||
usageCounter: this.usageCounter,
|
||||
});
|
||||
|
||||
|
|
|
@ -60,11 +60,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(registry.has('foo')).toEqual(true);
|
||||
});
|
||||
|
@ -86,11 +81,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
|
||||
|
@ -124,11 +114,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
|
||||
|
@ -153,11 +138,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
|
||||
|
@ -185,11 +165,6 @@ describe('Create Lifecycle', () => {
|
|||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
defaultScheduleInterval: 'foobar',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
|
||||
|
@ -278,11 +253,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
|
||||
|
@ -312,11 +282,6 @@ describe('Create Lifecycle', () => {
|
|||
producer: 'alerts',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
registry.register(ruleType);
|
||||
|
@ -350,11 +315,6 @@ describe('Create Lifecycle', () => {
|
|||
producer: 'alerts',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
registry.register(ruleType);
|
||||
|
@ -392,11 +352,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
|
||||
|
@ -423,11 +378,6 @@ describe('Create Lifecycle', () => {
|
|||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
ruleTaskTimeout: '20m',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
registry.register(ruleType);
|
||||
|
@ -460,11 +410,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
registry.register(ruleType);
|
||||
|
@ -488,11 +433,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(() =>
|
||||
registry.register({
|
||||
|
@ -509,11 +449,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Rule type \\"test\\" is already registered."`);
|
||||
});
|
||||
|
@ -536,11 +471,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
const ruleType = registry.get('test');
|
||||
expect(ruleType).toMatchInlineSnapshot(`
|
||||
|
@ -560,13 +490,6 @@ describe('Create Lifecycle', () => {
|
|||
"params": Array [],
|
||||
"state": Array [],
|
||||
},
|
||||
"config": Object {
|
||||
"run": Object {
|
||||
"actions": Object {
|
||||
"max": 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
"defaultActionGroupId": "default",
|
||||
"executor": [MockFunction],
|
||||
"id": "test",
|
||||
|
@ -615,11 +538,6 @@ describe('Create Lifecycle', () => {
|
|||
minimumLicenseRequired: 'basic',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = registry.list();
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
|
@ -714,11 +632,6 @@ describe('Create Lifecycle', () => {
|
|||
isExportable: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -752,11 +665,6 @@ function ruleTypeWithVariables<ActionGroupIds extends string>(
|
|||
minimumLicenseRequired: 'basic',
|
||||
async executor() {},
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!context && !state) return baseAlert;
|
||||
|
|
|
@ -1166,11 +1166,6 @@ describe('create()', () => {
|
|||
extractReferences: extractReferencesFn,
|
||||
injectReferences: injectReferencesFn,
|
||||
},
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
const data = getMockData({
|
||||
params: ruleParams,
|
||||
|
@ -1339,11 +1334,6 @@ describe('create()', () => {
|
|||
extractReferences: extractReferencesFn,
|
||||
injectReferences: injectReferencesFn,
|
||||
},
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
const data = getMockData({
|
||||
params: ruleParams,
|
||||
|
@ -2098,11 +2088,6 @@ describe('create()', () => {
|
|||
isExportable: true,
|
||||
async executor() {},
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"params invalid: [param1]: expected value of type [string] but got [undefined]"`
|
||||
|
@ -2628,11 +2613,6 @@ describe('create()', () => {
|
|||
extractReferences: jest.fn(),
|
||||
injectReferences: jest.fn(),
|
||||
},
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const data = getMockData({ schedule: { interval: '1s' } });
|
||||
|
|
|
@ -91,11 +91,6 @@ export function getBeforeSetup(
|
|||
isExportable: true,
|
||||
async executor() {},
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
rulesClientParams.getEventLogClient.mockResolvedValue(
|
||||
eventLogClient ?? eventLogClientMock.create()
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { createExecutionHandler } from './create_execution_handler';
|
||||
import { ActionsCompletion, AlertExecutionStore, CreateExecutionHandlerOptions } from './types';
|
||||
import { ActionsCompletion, CreateExecutionHandlerOptions } from './types';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import {
|
||||
actionsClientMock,
|
||||
|
@ -19,6 +19,7 @@ import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server';
|
|||
import { InjectActionParamsOpts } from './inject_action_params';
|
||||
import { NormalizedRuleType } from '../rule_type_registry';
|
||||
import { AlertInstanceContext, AlertInstanceState, RuleTypeParams, RuleTypeState } from '../types';
|
||||
import { AlertExecutionStore } from '../lib/alert_execution_store';
|
||||
|
||||
jest.mock('./inject_action_params', () => ({
|
||||
injectActionParams: jest.fn(),
|
||||
|
@ -48,11 +49,6 @@ const ruleType: NormalizedRuleType<
|
|||
},
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
|
@ -103,6 +99,11 @@ const createExecutionHandlerParams: jest.Mocked<
|
|||
},
|
||||
supportsEphemeralTasks: false,
|
||||
maxEphemeralActionsPerRule: 10,
|
||||
actionsConfigMap: {
|
||||
default: {
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
};
|
||||
let alertExecutionStore: AlertExecutionStore;
|
||||
|
||||
|
@ -120,11 +121,7 @@ describe('Create Execution Handler', () => {
|
|||
mockActionsPlugin.renderActionParameterTemplates.mockImplementation(
|
||||
renderActionParameterTemplatesDefault
|
||||
);
|
||||
alertExecutionStore = {
|
||||
numberOfTriggeredActions: 0,
|
||||
numberOfGeneratedActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
};
|
||||
alertExecutionStore = new AlertExecutionStore();
|
||||
});
|
||||
|
||||
test('enqueues execution per selected action', async () => {
|
||||
|
@ -136,7 +133,8 @@ describe('Create Execution Handler', () => {
|
|||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(1);
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(1);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(1);
|
||||
expect(mockActionsPlugin.getActionsClientWithRequest).toHaveBeenCalledWith(
|
||||
createExecutionHandlerParams.request
|
||||
);
|
||||
|
@ -244,7 +242,7 @@ describe('Create Execution Handler', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(alertExecutionStore.triggeredActionsStatus).toBe(ActionsCompletion.COMPLETE);
|
||||
expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE);
|
||||
});
|
||||
|
||||
test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => {
|
||||
|
@ -255,7 +253,16 @@ describe('Create Execution Handler', () => {
|
|||
const executionHandler = createExecutionHandler({
|
||||
...createExecutionHandlerParams,
|
||||
actions: [
|
||||
...createExecutionHandlerParams.actions,
|
||||
{
|
||||
id: '2',
|
||||
group: 'default',
|
||||
actionTypeId: 'test2',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My other {{context.value}} goes here',
|
||||
stateVal: 'My other {{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
group: 'default',
|
||||
|
@ -275,7 +282,8 @@ describe('Create Execution Handler', () => {
|
|||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(1);
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(1);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(2);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledWith({
|
||||
consumer: 'rule-consumer',
|
||||
|
@ -310,7 +318,14 @@ describe('Create Execution Handler', () => {
|
|||
const executionHandler = createExecutionHandler({
|
||||
...createExecutionHandlerParams,
|
||||
actions: [
|
||||
...createExecutionHandlerParams.actions,
|
||||
{
|
||||
id: '1',
|
||||
group: 'default',
|
||||
actionTypeId: '.slack',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
group: 'default',
|
||||
|
@ -331,7 +346,8 @@ describe('Create Execution Handler', () => {
|
|||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(0);
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(2);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0);
|
||||
|
||||
mockActionsPlugin.isActionExecutable.mockImplementation(() => true);
|
||||
|
@ -358,7 +374,8 @@ describe('Create Execution Handler', () => {
|
|||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(0);
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(0);
|
||||
expect(actionsClient.enqueueExecution).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -371,7 +388,8 @@ describe('Create Execution Handler', () => {
|
|||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(1);
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(1);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(1);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -466,23 +484,30 @@ describe('Create Execution Handler', () => {
|
|||
'Invalid action group "invalid-group" for rule "test".'
|
||||
);
|
||||
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(0);
|
||||
expect(alertExecutionStore.triggeredActionsStatus).toBe(ActionsCompletion.COMPLETE);
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(0);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(0);
|
||||
expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.COMPLETE);
|
||||
});
|
||||
|
||||
test('Stops triggering actions when the number of total triggered actions is reached the number of max executable actions', async () => {
|
||||
const executionHandler = createExecutionHandler({
|
||||
...createExecutionHandlerParams,
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 2 },
|
||||
},
|
||||
actionsConfigMap: {
|
||||
default: {
|
||||
max: 2,
|
||||
},
|
||||
},
|
||||
actions: [
|
||||
...createExecutionHandlerParams.actions,
|
||||
{
|
||||
id: '1',
|
||||
group: 'default',
|
||||
actionTypeId: 'test2',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My other {{context.value}} goes here',
|
||||
stateVal: 'My other {{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
group: 'default',
|
||||
|
@ -506,11 +531,7 @@ describe('Create Execution Handler', () => {
|
|||
],
|
||||
});
|
||||
|
||||
alertExecutionStore = {
|
||||
numberOfTriggeredActions: 0,
|
||||
numberOfGeneratedActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
};
|
||||
alertExecutionStore = new AlertExecutionStore();
|
||||
|
||||
await executionHandler({
|
||||
actionGroup: 'default',
|
||||
|
@ -520,9 +541,90 @@ describe('Create Execution Handler', () => {
|
|||
alertExecutionStore,
|
||||
});
|
||||
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(2);
|
||||
expect(alertExecutionStore.numberOfGeneratedActions).toBe(3);
|
||||
expect(alertExecutionStore.triggeredActionsStatus).toBe(ActionsCompletion.PARTIAL);
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(2);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(3);
|
||||
expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL);
|
||||
expect(createExecutionHandlerParams.logger.debug).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('Skips triggering actions for a specific action type when it reaches the limit for that specific action type', async () => {
|
||||
const executionHandler = createExecutionHandler({
|
||||
...createExecutionHandlerParams,
|
||||
actionsConfigMap: {
|
||||
default: {
|
||||
max: 4,
|
||||
},
|
||||
'test-action-type-id': {
|
||||
max: 1,
|
||||
},
|
||||
},
|
||||
actions: [
|
||||
...createExecutionHandlerParams.actions,
|
||||
{
|
||||
id: '2',
|
||||
group: 'default',
|
||||
actionTypeId: 'test-action-type-id',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My other {{context.value}} goes here',
|
||||
stateVal: 'My other {{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
group: 'default',
|
||||
actionTypeId: 'test-action-type-id',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: '{{context.value}} goes here',
|
||||
stateVal: '{{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
group: 'default',
|
||||
actionTypeId: 'another-action-type-id',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: '{{context.value}} goes here',
|
||||
stateVal: '{{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
group: 'default',
|
||||
actionTypeId: 'another-action-type-id',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: '{{context.value}} goes here',
|
||||
stateVal: '{{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
alertExecutionStore = new AlertExecutionStore();
|
||||
|
||||
await executionHandler({
|
||||
actionGroup: 'default',
|
||||
context: {},
|
||||
state: { value: 'state-val' },
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
|
||||
expect(alertExecutionStore.getNumberOfTriggeredActions()).toBe(4);
|
||||
expect(alertExecutionStore.getNumberOfGeneratedActions()).toBe(5);
|
||||
expect(alertExecutionStore.getStatusByConnectorType('test').numberOfTriggeredActions).toBe(1);
|
||||
expect(
|
||||
alertExecutionStore.getStatusByConnectorType('test-action-type-id').numberOfTriggeredActions
|
||||
).toBe(1);
|
||||
expect(
|
||||
alertExecutionStore.getStatusByConnectorType('another-action-type-id')
|
||||
.numberOfTriggeredActions
|
||||
).toBe(2);
|
||||
expect(alertExecutionStore.getTriggeredActionsStatus()).toBe(ActionsCompletion.PARTIAL);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,6 +46,7 @@ export function createExecutionHandler<
|
|||
ruleParams,
|
||||
supportsEphemeralTasks,
|
||||
maxEphemeralActionsPerRule,
|
||||
actionsConfigMap,
|
||||
}: CreateExecutionHandlerOptions<
|
||||
Params,
|
||||
ExtractedParams,
|
||||
|
@ -58,6 +59,7 @@ export function createExecutionHandler<
|
|||
const ruleTypeActionGroups = new Map(
|
||||
ruleType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name])
|
||||
);
|
||||
|
||||
return async ({
|
||||
actionGroup,
|
||||
actionSubgroup,
|
||||
|
@ -107,7 +109,7 @@ export function createExecutionHandler<
|
|||
}),
|
||||
}));
|
||||
|
||||
alertExecutionStore.numberOfGeneratedActions += actions.length;
|
||||
alertExecutionStore.incrementNumberOfGeneratedActions(actions.length);
|
||||
|
||||
const ruleLabel = `${ruleType.id}:${ruleId}: '${ruleName}'`;
|
||||
|
||||
|
@ -115,21 +117,48 @@ export function createExecutionHandler<
|
|||
let ephemeralActionsToSchedule = maxEphemeralActionsPerRule;
|
||||
|
||||
for (const action of actions) {
|
||||
if (alertExecutionStore.numberOfTriggeredActions >= ruleType.config!.run.actions.max) {
|
||||
alertExecutionStore.triggeredActionsStatus = ActionsCompletion.PARTIAL;
|
||||
const { actionTypeId } = action;
|
||||
|
||||
alertExecutionStore.incrementNumberOfGeneratedActionsByConnectorType(actionTypeId);
|
||||
|
||||
if (alertExecutionStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) {
|
||||
alertExecutionStore.setTriggeredActionsStatusByConnectorType({
|
||||
actionTypeId,
|
||||
status: ActionsCompletion.PARTIAL,
|
||||
});
|
||||
logger.debug(
|
||||
`Rule "${ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
!actionsPlugin.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true })
|
||||
alertExecutionStore.hasReachedTheExecutableActionsLimitByConnectorType({
|
||||
actionTypeId,
|
||||
actionsConfigMap,
|
||||
})
|
||||
) {
|
||||
if (!alertExecutionStore.hasConnectorTypeReachedTheLimit(actionTypeId)) {
|
||||
logger.debug(
|
||||
`Rule "${ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${actionTypeId} has been reached.`
|
||||
);
|
||||
}
|
||||
alertExecutionStore.setTriggeredActionsStatusByConnectorType({
|
||||
actionTypeId,
|
||||
status: ActionsCompletion.PARTIAL,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!actionsPlugin.isActionExecutable(action.id, actionTypeId, { notifyUsage: true })) {
|
||||
logger.warn(
|
||||
`Rule "${ruleId}" skipped scheduling action "${action.id}" because it is disabled`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
alertExecutionStore.numberOfTriggeredActions++;
|
||||
alertExecutionStore.incrementNumberOfTriggeredActions();
|
||||
alertExecutionStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId);
|
||||
|
||||
const namespace = spaceId === 'default' ? {} : { namespace: spaceId };
|
||||
|
||||
|
@ -155,7 +184,7 @@ export function createExecutionHandler<
|
|||
};
|
||||
|
||||
// TODO would be nice to add the action name here, but it's not available
|
||||
const actionLabel = `${action.actionTypeId}:${action.id}`;
|
||||
const actionLabel = `${actionTypeId}:${action.id}`;
|
||||
if (supportsEphemeralTasks && ephemeralActionsToSchedule > 0) {
|
||||
ephemeralActionsToSchedule--;
|
||||
try {
|
||||
|
@ -190,7 +219,7 @@ export function createExecutionHandler<
|
|||
{
|
||||
type: 'action',
|
||||
id: action.id,
|
||||
typeId: action.actionTypeId,
|
||||
typeId: actionTypeId,
|
||||
},
|
||||
],
|
||||
...namespace,
|
||||
|
|
|
@ -108,11 +108,6 @@ export const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockRunNowResponse = {
|
||||
|
|
|
@ -134,6 +134,11 @@ describe('Task Runner', () => {
|
|||
maxEphemeralActionsPerRule: 10,
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
usageCounter: mockUsageCounter,
|
||||
actionsConfigMap: {
|
||||
default: {
|
||||
max: 10000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ephemeralTestParams: Array<
|
||||
|
@ -2641,13 +2646,11 @@ describe('Task Runner', () => {
|
|||
const runnerResult = await taskRunner.run();
|
||||
expect(runnerResult.monitoring?.execution.history.length).toBe(200);
|
||||
});
|
||||
|
||||
test('Actions circuit breaker kicked in, should set status as warning and log a message in event log', async () => {
|
||||
const ruleTypeWithConfig = {
|
||||
...ruleType,
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 3 },
|
||||
},
|
||||
const actionsConfigMap = {
|
||||
default: {
|
||||
max: 3,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -2705,21 +2708,22 @@ describe('Task Runner', () => {
|
|||
...mockedRuleTypeSavedObject,
|
||||
actions: mockActions,
|
||||
} as jest.ResolvedValue<unknown>);
|
||||
ruleTypeRegistry.get.mockReturnValue(ruleTypeWithConfig);
|
||||
ruleTypeRegistry.get.mockReturnValue(ruleType);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT);
|
||||
|
||||
const taskRunner = new TaskRunner(
|
||||
ruleTypeWithConfig,
|
||||
ruleType,
|
||||
mockedTaskInstance,
|
||||
taskRunnerFactoryInitializerParams,
|
||||
{
|
||||
...taskRunnerFactoryInitializerParams,
|
||||
actionsConfigMap,
|
||||
},
|
||||
inMemoryMetrics
|
||||
);
|
||||
|
||||
const runnerResult = await taskRunner.run();
|
||||
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(
|
||||
ruleTypeWithConfig.config.run.actions.max
|
||||
);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(actionsConfigMap.default.max);
|
||||
|
||||
expect(
|
||||
taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update
|
||||
|
@ -2745,6 +2749,15 @@ describe('Task Runner', () => {
|
|||
},
|
||||
})
|
||||
);
|
||||
|
||||
const logger = taskRunnerFactoryInitializerParams.logger;
|
||||
expect(logger.debug).toHaveBeenCalledTimes(5);
|
||||
|
||||
expect(logger.debug).nthCalledWith(
|
||||
3,
|
||||
'Rule "1" skipped scheduling action "4" because the maximum number of allowed actions has been reached.'
|
||||
);
|
||||
|
||||
const eventLogger = taskRunnerFactoryInitializerParams.eventLogger;
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(7);
|
||||
|
||||
|
@ -2818,7 +2831,7 @@ describe('Task Runner', () => {
|
|||
action: EVENT_LOG_ACTIONS.execute,
|
||||
outcome: 'success',
|
||||
status: 'warning',
|
||||
numberOfTriggeredActions: ruleTypeWithConfig.config.run.actions.max,
|
||||
numberOfTriggeredActions: actionsConfigMap.default.max,
|
||||
numberOfGeneratedActions: mockActions.length,
|
||||
reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
task: true,
|
||||
|
@ -2827,6 +2840,138 @@ describe('Task Runner', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('Actions circuit breaker kicked in with connectorType specific config and multiple alerts', async () => {
|
||||
const actionsConfigMap = {
|
||||
default: {
|
||||
max: 30,
|
||||
},
|
||||
'.server-log': {
|
||||
max: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const warning = {
|
||||
reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
message: translations.taskRunner.warning.maxExecutableActions,
|
||||
};
|
||||
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
|
||||
ruleType.executor.mockImplementation(
|
||||
async ({
|
||||
services: executorServices,
|
||||
}: RuleExecutorOptions<
|
||||
RuleTypeParams,
|
||||
RuleTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
string
|
||||
>) => {
|
||||
executorServices.alertFactory.create('1').scheduleActions('default');
|
||||
executorServices.alertFactory.create('2').scheduleActions('default');
|
||||
}
|
||||
);
|
||||
|
||||
rulesClient.get.mockResolvedValue({
|
||||
...mockedRuleTypeSavedObject,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '1',
|
||||
actionTypeId: '.server-log',
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '2',
|
||||
actionTypeId: '.server-log',
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '3',
|
||||
actionTypeId: '.server-log',
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '4',
|
||||
actionTypeId: 'any-action',
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '5',
|
||||
actionTypeId: 'any-action',
|
||||
},
|
||||
],
|
||||
} as jest.ResolvedValue<unknown>);
|
||||
|
||||
ruleTypeRegistry.get.mockReturnValue(ruleType);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT);
|
||||
|
||||
const taskRunner = new TaskRunner(
|
||||
ruleType,
|
||||
mockedTaskInstance,
|
||||
{
|
||||
...taskRunnerFactoryInitializerParams,
|
||||
actionsConfigMap,
|
||||
},
|
||||
inMemoryMetrics
|
||||
);
|
||||
|
||||
const runnerResult = await taskRunner.run();
|
||||
|
||||
// 1x(.server-log) and 2x(any-action) per alert
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(5);
|
||||
|
||||
expect(
|
||||
taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update
|
||||
).toHaveBeenCalledWith(...generateSavedObjectParams({ status: 'warning', warning }));
|
||||
|
||||
expect(runnerResult).toEqual(
|
||||
generateRunnerResult({
|
||||
state: true,
|
||||
history: [true],
|
||||
alertInstances: {
|
||||
'1': {
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(DATE_1970),
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
state: {
|
||||
duration: 0,
|
||||
start: '1970-01-01T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
'2': {
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(DATE_1970),
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
state: {
|
||||
duration: 0,
|
||||
start: '1970-01-01T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const logger = taskRunnerFactoryInitializerParams.logger;
|
||||
expect(logger.debug).toHaveBeenCalledTimes(5);
|
||||
|
||||
expect(logger.debug).nthCalledWith(
|
||||
3,
|
||||
'Rule "1" skipped scheduling action "1" because the maximum number of allowed actions for connector type .server-log has been reached.'
|
||||
);
|
||||
|
||||
const eventLogger = taskRunnerFactoryInitializerParams.eventLogger;
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(11);
|
||||
});
|
||||
|
||||
test('increments monitoring metrics after execution', async () => {
|
||||
const taskRunner = new TaskRunner(
|
||||
ruleType,
|
||||
|
|
|
@ -65,8 +65,6 @@ import {
|
|||
} from '../lib/create_alert_event_log_record_object';
|
||||
import { InMemoryMetrics, IN_MEMORY_METRICS } from '../monitoring';
|
||||
import {
|
||||
ActionsCompletion,
|
||||
AlertExecutionStore,
|
||||
GenerateNewAndRecoveredAlertEventsParams,
|
||||
LogActiveAndRecoveredAlertsParams,
|
||||
RuleTaskInstance,
|
||||
|
@ -74,6 +72,7 @@ import {
|
|||
ScheduleActionsForRecoveredAlertsParams,
|
||||
TrackAlertDurationsParams,
|
||||
} from './types';
|
||||
import { AlertExecutionStore } from '../lib/alert_execution_store';
|
||||
|
||||
const FALLBACK_RETRY_INTERVAL = '5m';
|
||||
const CONNECTIVITY_RETRY_INTERVAL = '5m';
|
||||
|
@ -231,6 +230,7 @@ export class TaskRunner<
|
|||
ruleParams,
|
||||
supportsEphemeralTasks: this.context.supportsEphemeralTasks,
|
||||
maxEphemeralActionsPerRule: this.context.maxEphemeralActionsPerRule,
|
||||
actionsConfigMap: this.context.actionsConfigMap,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -491,11 +491,7 @@ export class TaskRunner<
|
|||
});
|
||||
}
|
||||
|
||||
const alertExecutionStore: AlertExecutionStore = {
|
||||
numberOfTriggeredActions: 0,
|
||||
numberOfGeneratedActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
};
|
||||
const alertExecutionStore = new AlertExecutionStore();
|
||||
|
||||
const ruleIsSnoozed = this.isRuleSnoozed(rule);
|
||||
if (!ruleIsSnoozed && this.shouldLogAndScheduleActionsForAlerts()) {
|
||||
|
@ -567,7 +563,11 @@ export class TaskRunner<
|
|||
|
||||
return {
|
||||
metrics: searchMetrics,
|
||||
alertExecutionStore,
|
||||
alertExecutionMetrics: {
|
||||
numberOfTriggeredActions: alertExecutionStore.getNumberOfTriggeredActions(),
|
||||
numberOfGeneratedActions: alertExecutionStore.getNumberOfGeneratedActions(),
|
||||
triggeredActionsStatus: alertExecutionStore.getTriggeredActionsStatus(),
|
||||
},
|
||||
alertTypeState: updatedRuleTypeState || undefined,
|
||||
alertInstances: mapValues<
|
||||
Record<string, Alert<InstanceState, InstanceContext>>,
|
||||
|
@ -874,7 +874,7 @@ export class TaskRunner<
|
|||
executionState: RuleExecutionState
|
||||
): RuleTaskState => {
|
||||
return {
|
||||
...omit(executionState, ['alertExecutionStore', 'metrics']),
|
||||
...omit(executionState, ['alertExecutionMetrics', 'metrics']),
|
||||
previousStartedAt: startedAt,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -57,11 +57,6 @@ const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
config: {
|
||||
run: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let fakeTimer: sinon.SinonFakeTimers;
|
||||
|
@ -134,6 +129,11 @@ describe('Task Runner Cancel', () => {
|
|||
maxEphemeralActionsPerRule: 10,
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
usageCounter: mockUsageCounter,
|
||||
actionsConfigMap: {
|
||||
default: {
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockDate = new Date('2019-02-12T21:01:22.479Z');
|
||||
|
|
|
@ -101,6 +101,11 @@ describe('Task Runner Factory', () => {
|
|||
cancelAlertsOnRuleTimeout: true,
|
||||
executionContext,
|
||||
usageCounter: mockUsageCounter,
|
||||
actionsConfigMap: {
|
||||
default: {
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -34,6 +34,7 @@ import { TaskRunner } from './task_runner';
|
|||
import { RulesClient } from '../rules_client';
|
||||
import { NormalizedRuleType } from '../rule_type_registry';
|
||||
import { InMemoryMetrics } from '../monitoring';
|
||||
import { ActionsConfigMap } from '../lib/get_actions_config_map';
|
||||
|
||||
export interface TaskRunnerContext {
|
||||
logger: Logger;
|
||||
|
@ -53,6 +54,7 @@ export interface TaskRunnerContext {
|
|||
kibanaBaseUrl: string | undefined;
|
||||
supportsEphemeralTasks: boolean;
|
||||
maxEphemeralActionsPerRule: number;
|
||||
actionsConfigMap: ActionsConfigMap;
|
||||
cancelAlertsOnRuleTimeout: boolean;
|
||||
usageCounter?: UsageCounter;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import { Alert } from '../alert';
|
|||
import { NormalizedRuleType } from '../rule_type_registry';
|
||||
import { ExecutionHandler } from './create_execution_handler';
|
||||
import { RawRule } from '../types';
|
||||
import { ActionsConfigMap } from '../lib/get_actions_config_map';
|
||||
import { AlertExecutionStore } from '../lib/alert_execution_store';
|
||||
|
||||
export interface RuleTaskRunResultWithActions {
|
||||
state: RuleExecutionState;
|
||||
|
@ -145,6 +147,7 @@ export interface CreateExecutionHandlerOptions<
|
|||
ruleParams: RuleTypeParams;
|
||||
supportsEphemeralTasks: boolean;
|
||||
maxEphemeralActionsPerRule: number;
|
||||
actionsConfigMap: ActionsConfigMap;
|
||||
}
|
||||
|
||||
export interface ExecutionHandlerOptions<ActionGroupIds extends string> {
|
||||
|
@ -160,9 +163,3 @@ export enum ActionsCompletion {
|
|||
COMPLETE = 'complete',
|
||||
PARTIAL = 'partial',
|
||||
}
|
||||
|
||||
export interface AlertExecutionStore {
|
||||
numberOfTriggeredActions: number;
|
||||
numberOfGeneratedActions: number;
|
||||
triggeredActionsStatus: ActionsCompletion;
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ import {
|
|||
RuleMonitoring,
|
||||
MappedParams,
|
||||
} from '../common';
|
||||
import { RuleTypeConfig } from './config';
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
|
||||
|
||||
|
@ -170,7 +169,6 @@ export interface RuleType<
|
|||
ruleTaskTimeout?: string;
|
||||
cancelAlertsOnRuleTimeout?: boolean;
|
||||
doesSetRecoveryContext?: boolean;
|
||||
config?: RuleTypeConfig;
|
||||
}
|
||||
export type UntypedRuleType = RuleType<
|
||||
RuleTypeParams,
|
||||
|
|
|
@ -50,6 +50,7 @@ const enabledActionTypes = [
|
|||
'test.no-attempts-rate-limit',
|
||||
'test.throw',
|
||||
'test.excluded',
|
||||
'test.capped',
|
||||
];
|
||||
|
||||
export function createTestConfig(name: string, options: CreateTestConfigOptions) {
|
||||
|
@ -163,6 +164,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
'--xpack.alerting.invalidateApiKeysTask.interval="15s"',
|
||||
'--xpack.alerting.healthCheck.interval="1s"',
|
||||
'--xpack.alerting.rules.minimumScheduleInterval.value="1s"',
|
||||
`--xpack.alerting.rules.run.actions.connectorTypeOverrides=${JSON.stringify([
|
||||
{ id: 'test.capped', max: '1' },
|
||||
])}`,
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
|
||||
`--xpack.actions.rejectUnauthorized=${rejectUnauthorized}`,
|
||||
`--xpack.actions.microsoftGraphApiUrl=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}/api/_actions-FTS-external-service-simulators/exchange/users/test@/sendMail`,
|
||||
|
|
|
@ -31,8 +31,17 @@ export function defineActionTypes(
|
|||
throw new Error('this action is intended to fail');
|
||||
},
|
||||
};
|
||||
const cappedActionType: ActionType = {
|
||||
id: 'test.capped',
|
||||
name: 'Test: Capped',
|
||||
minimumLicenseRequired: 'gold',
|
||||
async executor() {
|
||||
return { status: 'ok', actionId: '' };
|
||||
},
|
||||
};
|
||||
actions.registerType(noopActionType);
|
||||
actions.registerType(throwActionType);
|
||||
actions.registerType(cappedActionType);
|
||||
actions.registerType(getIndexRecordActionType());
|
||||
actions.registerType(getDelayedActionType());
|
||||
actions.registerType(getFailingActionType());
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { Spaces } from '../../scenarios';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { getEventLog, getTestRuleData, getUrlPrefix, ObjectRemover } from '../../../common/lib';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createCappedActionsTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('Capped action type', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
it('should not trigger actions more than connector types limit', async () => {
|
||||
const { body: createdAction01 } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'MY action',
|
||||
connector_type_id: 'test.capped',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
const { body: createdAction02 } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'MY action',
|
||||
connector_type_id: 'test.capped',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
const { body: createdAction03 } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'MY action',
|
||||
connector_type_id: 'test.capped',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdAction01.id, 'action', 'actions');
|
||||
objectRemover.add(Spaces.space1.id, createdAction02.id, 'action', 'actions');
|
||||
objectRemover.add(Spaces.space1.id, createdAction03.id, 'action', 'actions');
|
||||
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
name: 'should not trigger actions when connector type limit is reached',
|
||||
rule_type_id: 'test.patternFiring',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
notify_when: 'onActiveAlert',
|
||||
params: {
|
||||
pattern: { instance: arrayOfTrues(100) },
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createdAction01.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: createdAction02.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: createdAction03.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting');
|
||||
|
||||
await getRuleEvents(createdRule.id);
|
||||
const [executionEvent] = await getRuleEvents(createdRule.id, 1);
|
||||
|
||||
expect(
|
||||
executionEvent?.kibana?.alert?.rule?.execution?.metrics?.number_of_generated_actions
|
||||
).to.be.eql(3, 'all the generated actions');
|
||||
expect(
|
||||
executionEvent?.kibana?.alert?.rule?.execution?.metrics?.number_of_triggered_actions
|
||||
).to.be.eql(1, 'only 1 action was triggered');
|
||||
});
|
||||
});
|
||||
|
||||
async function getRuleEvents(id: string, minActions: number = 1) {
|
||||
return await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: Spaces.space1.id,
|
||||
type: 'alert',
|
||||
id,
|
||||
provider: 'alerting',
|
||||
actions: new Map([['execute', { gte: minActions }]]),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function arrayOfTrues(length: number) {
|
||||
const result = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
result.push(true);
|
||||
}
|
||||
return result;
|
||||
}
|
|
@ -45,6 +45,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
|
|||
loadTestFile(require.resolve('./ephemeral'));
|
||||
loadTestFile(require.resolve('./event_log_alerts'));
|
||||
loadTestFile(require.resolve('./snooze'));
|
||||
loadTestFile(require.resolve('./capped_action_type'));
|
||||
loadTestFile(require.resolve('./scheduled_task_id'));
|
||||
// Do not place test files here, due to https://github.com/elastic/kibana/issues/123059
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue