mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Revert "[Alerting] Limit the executable actions per Rule execution (#126902)"
This reverts commit 31099bc68f
.
This commit is contained in:
parent
0847d47f8b
commit
242c5fd63f
38 changed files with 748 additions and 1611 deletions
|
@ -200,7 +200,4 @@ Specifies the minimum interval allowed for the all rules. This minimum is enforc
|
|||
+
|
||||
`<count>[s,m,h,d]`
|
||||
+
|
||||
For example, `20m`, `24h`, `7d`. Default: `1m`.
|
||||
|
||||
`xpack.alerting.rules.execution.actions.max`
|
||||
Specifies the maximum number of actions that a rule can trigger each time detection checks run.
|
||||
For example, `20m`, `24h`, `7d`. Default: `1m`.
|
|
@ -200,7 +200,6 @@ kibana_vars=(
|
|||
xpack.alerting.invalidateApiKeysTask.removalDelay
|
||||
xpack.alerting.defaultRuleTaskTimeout
|
||||
xpack.alerting.cancelAlertsOnRuleTimeout
|
||||
xpack.alerting.rules.execution.actions.max
|
||||
xpack.alerts.healthCheck.interval
|
||||
xpack.alerts.invalidateApiKeysTask.interval
|
||||
xpack.alerts.invalidateApiKeysTask.removalDelay
|
||||
|
|
|
@ -22,14 +22,7 @@ export interface IntervalSchedule extends SavedObjectAttributes {
|
|||
|
||||
// for the `typeof ThingValues[number]` types below, become string types that
|
||||
// only accept the values in the associated string arrays
|
||||
export const AlertExecutionStatusValues = [
|
||||
'ok',
|
||||
'active',
|
||||
'error',
|
||||
'pending',
|
||||
'unknown',
|
||||
'warning',
|
||||
] as const;
|
||||
export const AlertExecutionStatusValues = ['ok', 'active', 'error', 'pending', 'unknown'] as const;
|
||||
export type AlertExecutionStatuses = typeof AlertExecutionStatusValues[number];
|
||||
|
||||
export enum AlertExecutionStatusErrorReasons {
|
||||
|
@ -42,10 +35,6 @@ export enum AlertExecutionStatusErrorReasons {
|
|||
Disabled = 'disabled',
|
||||
}
|
||||
|
||||
export enum AlertExecutionStatusWarningReasons {
|
||||
MAX_EXECUTABLE_ACTIONS = 'maxExecutableActions',
|
||||
}
|
||||
|
||||
export interface AlertExecutionStatus {
|
||||
status: AlertExecutionStatuses;
|
||||
numberOfTriggeredActions?: number;
|
||||
|
@ -56,10 +45,6 @@ export interface AlertExecutionStatus {
|
|||
reason: AlertExecutionStatusErrorReasons;
|
||||
message: string;
|
||||
};
|
||||
warning?: {
|
||||
reason: AlertExecutionStatusWarningReasons;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AlertActionParams = SavedObjectAttributes;
|
||||
|
|
|
@ -10,6 +10,13 @@ import { rawAlertInstance } from './alert_instance';
|
|||
import { DateFromString } from './date_from_string';
|
||||
import { IntervalSchedule, RuleMonitoring } from './alert';
|
||||
|
||||
const actionSchema = t.partial({
|
||||
group: t.string,
|
||||
id: t.string,
|
||||
actionTypeId: t.string,
|
||||
params: t.record(t.string, t.unknown),
|
||||
});
|
||||
|
||||
export const ruleStateSchema = t.partial({
|
||||
alertTypeState: t.record(t.string, t.unknown),
|
||||
alertInstances: t.record(t.string, rawAlertInstance),
|
||||
|
@ -22,16 +29,11 @@ const ruleExecutionMetricsSchema = t.partial({
|
|||
esSearchDurationMs: t.number,
|
||||
});
|
||||
|
||||
const alertExecutionStore = t.partial({
|
||||
numberOfTriggeredActions: t.number,
|
||||
triggeredActionsStatus: t.string,
|
||||
});
|
||||
|
||||
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>;
|
||||
triggeredActions: Array<t.TypeOf<typeof actionSchema>>;
|
||||
};
|
||||
|
||||
export const ruleParamsSchema = t.intersection([
|
||||
|
|
|
@ -23,13 +23,6 @@ describe('config validation', () => {
|
|||
},
|
||||
"maxEphemeralActionsPerAlert": 10,
|
||||
"minimumScheduleInterval": "1m",
|
||||
"rules": Object {
|
||||
"execution": Object {
|
||||
"actions": Object {
|
||||
"max": 100000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -8,21 +8,6 @@
|
|||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { validateDurationSchema } from './lib';
|
||||
|
||||
const ruleTypeSchema = schema.object({
|
||||
id: schema.string(),
|
||||
timeout: schema.maybe(schema.string({ validate: validateDurationSchema })),
|
||||
});
|
||||
|
||||
const rulesSchema = schema.object({
|
||||
execution: schema.object({
|
||||
timeout: schema.maybe(schema.string({ validate: validateDurationSchema })),
|
||||
actions: schema.object({
|
||||
max: schema.number({ defaultValue: 100000 }),
|
||||
}),
|
||||
ruleTypeOverrides: schema.maybe(schema.arrayOf(ruleTypeSchema)),
|
||||
}),
|
||||
});
|
||||
|
||||
export const DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT = 10;
|
||||
export const configSchema = schema.object({
|
||||
healthCheck: schema.object({
|
||||
|
@ -38,10 +23,7 @@ export const configSchema = schema.object({
|
|||
defaultRuleTaskTimeout: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }),
|
||||
cancelAlertsOnRuleTimeout: schema.boolean({ defaultValue: true }),
|
||||
minimumScheduleInterval: schema.string({ validate: validateDurationSchema, defaultValue: '1m' }),
|
||||
rules: rulesSchema,
|
||||
});
|
||||
|
||||
export type AlertingConfig = TypeOf<typeof configSchema>;
|
||||
export type PublicAlertingConfig = Pick<AlertingConfig, 'minimumScheduleInterval'>;
|
||||
export type RulesConfig = TypeOf<typeof rulesSchema>;
|
||||
export type RuleTypeConfig = Omit<RulesConfig, 'ruleTypeOverrides'>;
|
||||
|
|
|
@ -1,22 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const translations = {
|
||||
taskRunner: {
|
||||
warning: {
|
||||
maxExecutableActions: i18n.translate(
|
||||
'xpack.alerting.taskRunner.warning.maxExecutableActions',
|
||||
{
|
||||
defaultMessage:
|
||||
'The maximum number of actions for this rule type was reached; excess actions were not triggered.',
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,45 +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 { getRulesConfig } from './get_rules_config';
|
||||
import { RulesConfig } from '../config';
|
||||
|
||||
const ruleTypeId = 'test-rule-type-id';
|
||||
const config = {
|
||||
execution: {
|
||||
timeout: '1m',
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
} as RulesConfig;
|
||||
|
||||
const configWithRuleType = {
|
||||
execution: {
|
||||
...config.execution,
|
||||
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(getRulesConfig({ config: configWithRuleType, ruleTypeId })).toEqual({
|
||||
execution: {
|
||||
id: ruleTypeId,
|
||||
timeout: '1m',
|
||||
actions: { max: 20 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('returns the default config when there is no rule type specific config', () => {
|
||||
expect(getRulesConfig({ config, ruleTypeId })).toEqual(config);
|
||||
});
|
||||
});
|
|
@ -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 getRulesConfig = ({
|
||||
config,
|
||||
ruleTypeId,
|
||||
}: {
|
||||
config: RulesConfig;
|
||||
ruleTypeId: string;
|
||||
}): RuleTypeConfig => {
|
||||
const ruleTypeConfig = config.execution.ruleTypeOverrides?.find(
|
||||
(ruleType) => ruleType.id === ruleTypeId
|
||||
);
|
||||
|
||||
return {
|
||||
execution: {
|
||||
...omit(config.execution, 'ruleTypeOverrides'),
|
||||
...ruleTypeConfig,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -6,11 +6,7 @@
|
|||
*/
|
||||
|
||||
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import {
|
||||
AlertExecutionStatusErrorReasons,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
RuleExecutionState,
|
||||
} from '../types';
|
||||
import { AlertAction, AlertExecutionStatusErrorReasons, RuleExecutionState } from '../types';
|
||||
import {
|
||||
executionStatusFromState,
|
||||
executionStatusFromError,
|
||||
|
@ -18,8 +14,6 @@ import {
|
|||
ruleExecutionStatusFromRaw,
|
||||
} from './rule_execution_status';
|
||||
import { ErrorWithReason } from './error_with_reason';
|
||||
import { translations } from '../constants/translations';
|
||||
import { ActionsCompletion } from '../task_runner/types';
|
||||
|
||||
const MockLogger = loggingSystemMock.create().get();
|
||||
const metrics = { numSearches: 1, esSearchDurationMs: 10, totalSearchDurationMs: 20 };
|
||||
|
@ -31,59 +25,42 @@ describe('RuleExecutionStatus', () => {
|
|||
|
||||
describe('executionStatusFromState()', () => {
|
||||
test('empty task state', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertExecutionStore: {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
},
|
||||
} as RuleExecutionState);
|
||||
const status = executionStatusFromState({} as RuleExecutionState);
|
||||
checkDateIsNearNow(status.lastExecutionDate);
|
||||
expect(status.numberOfTriggeredActions).toBe(0);
|
||||
expect(status.status).toBe('ok');
|
||||
expect(status.error).toBe(undefined);
|
||||
expect(status.warning).toBe(undefined);
|
||||
});
|
||||
|
||||
test('task state with no instances', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertInstances: {},
|
||||
alertExecutionStore: {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
},
|
||||
triggeredActions: [],
|
||||
metrics,
|
||||
});
|
||||
checkDateIsNearNow(status.lastExecutionDate);
|
||||
expect(status.numberOfTriggeredActions).toBe(0);
|
||||
expect(status.status).toBe('ok');
|
||||
expect(status.error).toBe(undefined);
|
||||
expect(status.warning).toBe(undefined);
|
||||
expect(status.metrics).toBe(metrics);
|
||||
});
|
||||
|
||||
test('task state with one instance', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertInstances: { a: {} },
|
||||
alertExecutionStore: {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
},
|
||||
triggeredActions: [],
|
||||
metrics,
|
||||
});
|
||||
checkDateIsNearNow(status.lastExecutionDate);
|
||||
expect(status.numberOfTriggeredActions).toBe(0);
|
||||
expect(status.status).toBe('active');
|
||||
expect(status.error).toBe(undefined);
|
||||
expect(status.warning).toBe(undefined);
|
||||
expect(status.metrics).toBe(metrics);
|
||||
});
|
||||
|
||||
test('task state with numberOfTriggeredActions', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertExecutionStore: {
|
||||
numberOfTriggeredActions: 1,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
},
|
||||
triggeredActions: [{ group: '1' } as AlertAction],
|
||||
alertInstances: { a: {} },
|
||||
metrics,
|
||||
});
|
||||
|
@ -91,27 +68,8 @@ describe('RuleExecutionStatus', () => {
|
|||
expect(status.numberOfTriggeredActions).toBe(1);
|
||||
expect(status.status).toBe('active');
|
||||
expect(status.error).toBe(undefined);
|
||||
expect(status.warning).toBe(undefined);
|
||||
expect(status.metrics).toBe(metrics);
|
||||
});
|
||||
|
||||
test('task state with warning', () => {
|
||||
const status = executionStatusFromState({
|
||||
alertInstances: { a: {} },
|
||||
alertExecutionStore: {
|
||||
numberOfTriggeredActions: 3,
|
||||
triggeredActionsStatus: ActionsCompletion.PARTIAL,
|
||||
},
|
||||
metrics,
|
||||
});
|
||||
checkDateIsNearNow(status.lastExecutionDate);
|
||||
expect(status.warning).toEqual({
|
||||
message: translations.taskRunner.warning.maxExecutableActions,
|
||||
reason: AlertExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
});
|
||||
expect(status.status).toBe('warning');
|
||||
expect(status.error).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executionStatusFromError()', () => {
|
||||
|
@ -153,7 +111,6 @@ describe('RuleExecutionStatus', () => {
|
|||
"lastDuration": 0,
|
||||
"lastExecutionDate": "2020-09-03T16:26:58.000Z",
|
||||
"status": "ok",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -169,7 +126,6 @@ describe('RuleExecutionStatus', () => {
|
|||
"lastDuration": 0,
|
||||
"lastExecutionDate": "2020-09-03T16:26:58.000Z",
|
||||
"status": "ok",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -182,7 +138,6 @@ describe('RuleExecutionStatus', () => {
|
|||
"lastDuration": 1234,
|
||||
"lastExecutionDate": "2020-09-03T16:26:58.000Z",
|
||||
"status": "ok",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -196,7 +151,6 @@ describe('RuleExecutionStatus', () => {
|
|||
"lastDuration": 0,
|
||||
"lastExecutionDate": "2020-09-03T16:26:58.000Z",
|
||||
"status": "ok",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -230,7 +184,6 @@ describe('RuleExecutionStatus', () => {
|
|||
checkDateIsNearNow(result.lastExecutionDate);
|
||||
expect(result.status).toBe('unknown');
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.warning).toBe(undefined);
|
||||
expect(MockLogger.debug).toBeCalledWith(
|
||||
'invalid ruleExecutionStatus lastExecutionDate "an invalid date" in raw rule rule-id'
|
||||
);
|
||||
|
|
|
@ -6,43 +6,18 @@
|
|||
*/
|
||||
|
||||
import { Logger } from 'src/core/server';
|
||||
import {
|
||||
AlertExecutionStatus,
|
||||
AlertExecutionStatusValues,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
RawRuleExecutionStatus,
|
||||
RuleExecutionState,
|
||||
} from '../types';
|
||||
import { AlertExecutionStatus, RawRuleExecutionStatus, RuleExecutionState } from '../types';
|
||||
import { getReasonFromError } from './error_with_reason';
|
||||
import { getEsErrorMessage } from './errors';
|
||||
import { AlertExecutionStatuses } from '../../common';
|
||||
import { translations } from '../constants/translations';
|
||||
import { ActionsCompletion } from '../task_runner/types';
|
||||
|
||||
export function executionStatusFromState(state: RuleExecutionState): AlertExecutionStatus {
|
||||
const alertIds = Object.keys(state.alertInstances ?? {});
|
||||
|
||||
const hasIncompleteAlertExecution =
|
||||
state.alertExecutionStore.triggeredActionsStatus === ActionsCompletion.PARTIAL;
|
||||
|
||||
let status: AlertExecutionStatuses =
|
||||
alertIds.length === 0 ? AlertExecutionStatusValues[0] : AlertExecutionStatusValues[1];
|
||||
|
||||
if (hasIncompleteAlertExecution) {
|
||||
status = AlertExecutionStatusValues[5];
|
||||
}
|
||||
|
||||
return {
|
||||
metrics: state.metrics,
|
||||
numberOfTriggeredActions: state.alertExecutionStore.numberOfTriggeredActions,
|
||||
numberOfTriggeredActions: state.triggeredActions?.length ?? 0,
|
||||
lastExecutionDate: new Date(),
|
||||
status,
|
||||
...(hasIncompleteAlertExecution && {
|
||||
warning: {
|
||||
reason: AlertExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
message: translations.taskRunner.warning.maxExecutableActions,
|
||||
},
|
||||
}),
|
||||
status: alertIds.length === 0 ? 'ok' : 'active',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -62,7 +37,6 @@ export function ruleExecutionStatusToRaw({
|
|||
lastDuration,
|
||||
status,
|
||||
error,
|
||||
warning,
|
||||
}: AlertExecutionStatus): RawRuleExecutionStatus {
|
||||
return {
|
||||
lastExecutionDate: lastExecutionDate.toISOString(),
|
||||
|
@ -70,7 +44,6 @@ export function ruleExecutionStatusToRaw({
|
|||
status,
|
||||
// explicitly setting to null (in case undefined) due to partial update concerns
|
||||
error: error ?? null,
|
||||
warning: warning ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -87,7 +60,6 @@ export function ruleExecutionStatusFromRaw(
|
|||
numberOfTriggeredActions,
|
||||
status = 'unknown',
|
||||
error,
|
||||
warning,
|
||||
} = rawRuleExecutionStatus;
|
||||
|
||||
let parsedDateMillis = lastExecutionDate ? Date.parse(lastExecutionDate) : Date.now();
|
||||
|
@ -115,10 +87,6 @@ export function ruleExecutionStatusFromRaw(
|
|||
executionStatus.error = error;
|
||||
}
|
||||
|
||||
if (warning) {
|
||||
executionStatus.warning = warning;
|
||||
}
|
||||
|
||||
return executionStatus;
|
||||
}
|
||||
|
||||
|
@ -126,5 +94,4 @@ export const getRuleExecutionStatusPending = (lastExecutionDate: string) => ({
|
|||
status: 'pending' as AlertExecutionStatuses,
|
||||
lastExecutionDate,
|
||||
error: null,
|
||||
warning: null,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertingPlugin, PluginSetupContract } from './plugin';
|
||||
import { AlertingPlugin, AlertingPluginsSetup, PluginSetupContract } from './plugin';
|
||||
import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/mocks';
|
||||
import { coreMock, statusServiceMock } from '../../../../src/core/server/mocks';
|
||||
import { licensingMock } from '../../licensing/server/mocks';
|
||||
|
@ -20,70 +20,42 @@ import { RuleType } from './types';
|
|||
import { eventLogMock } from '../../event_log/server/mocks';
|
||||
import { actionsMock } from '../../actions/server/mocks';
|
||||
|
||||
const generateAlertingConfig = (): AlertingConfig => ({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 10,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
rules: {
|
||||
execution: {
|
||||
actions: {
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sampleRuleType: RuleType<never, never, never, never, never, 'default'> = {
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
actionGroups: [],
|
||||
defaultActionGroupId: 'default',
|
||||
producer: 'test',
|
||||
config: {
|
||||
execution: {
|
||||
actions: {
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
async executor() {},
|
||||
};
|
||||
|
||||
describe('Alerting Plugin', () => {
|
||||
describe('setup()', () => {
|
||||
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
|
||||
const setupMocks = coreMock.createSetup();
|
||||
const mockPlugins = {
|
||||
licensing: licensingMock.createSetup(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsSetup,
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
eventLog: eventLogServiceMock.create(),
|
||||
actions: actionsMock.createSetup(),
|
||||
statusService: statusServiceMock.createSetupContract(),
|
||||
};
|
||||
|
||||
let plugin: AlertingPlugin;
|
||||
let coreSetup: ReturnType<typeof coreMock.createSetup>;
|
||||
let pluginsSetup: jest.Mocked<AlertingPluginsSetup>;
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('should log warning when Encrypted Saved Objects plugin is missing encryption key', async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 10,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
plugin = new AlertingPlugin(context);
|
||||
|
||||
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
|
||||
|
||||
const setupMocks = coreMock.createSetup();
|
||||
// need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices()
|
||||
await plugin.setup(setupMocks, mockPlugins);
|
||||
await plugin.setup(setupMocks, {
|
||||
licensing: licensingMock.createSetup(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsSetup,
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
eventLog: eventLogServiceMock.create(),
|
||||
actions: actionsMock.createSetup(),
|
||||
statusService: statusServiceMock.createSetupContract(),
|
||||
});
|
||||
|
||||
expect(setupMocks.status.set).toHaveBeenCalledTimes(1);
|
||||
expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false);
|
||||
|
@ -93,88 +65,93 @@ describe('Alerting Plugin', () => {
|
|||
});
|
||||
|
||||
it('should create usage counter if usageCollection plugin is defined', async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 10,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
plugin = new AlertingPlugin(context);
|
||||
|
||||
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
|
||||
const usageCollectionSetup = createUsageCollectionSetupMock();
|
||||
|
||||
const setupMocks = coreMock.createSetup();
|
||||
// need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices()
|
||||
await plugin.setup(setupMocks, { ...mockPlugins, usageCollection: usageCollectionSetup });
|
||||
await plugin.setup(setupMocks, {
|
||||
licensing: licensingMock.createSetup(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsSetup,
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
eventLog: eventLogServiceMock.create(),
|
||||
actions: actionsMock.createSetup(),
|
||||
statusService: statusServiceMock.createSetupContract(),
|
||||
usageCollection: usageCollectionSetup,
|
||||
});
|
||||
|
||||
expect(usageCollectionSetup.createUsageCounter).toHaveBeenCalled();
|
||||
expect(usageCollectionSetup.registerCollector).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`exposes configured minimumScheduleInterval()`, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 100,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
plugin = new AlertingPlugin(context);
|
||||
|
||||
const setupContract = await plugin.setup(setupMocks, mockPlugins);
|
||||
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
|
||||
const setupContract = plugin.setup(coreMock.createSetup(), {
|
||||
licensing: licensingMock.createSetup(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsSetup,
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
eventLog: eventLogServiceMock.create(),
|
||||
actions: actionsMock.createSetup(),
|
||||
statusService: statusServiceMock.createSetupContract(),
|
||||
});
|
||||
|
||||
expect(setupContract.getConfig()).toEqual({ minimumScheduleInterval: '1m' });
|
||||
});
|
||||
|
||||
it(`applies the default config if there is no rule type specific config `, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
...generateAlertingConfig(),
|
||||
rules: {
|
||||
execution: {
|
||||
actions: {
|
||||
max: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
plugin = new AlertingPlugin(context);
|
||||
|
||||
const setupContract = await plugin.setup(setupMocks, mockPlugins);
|
||||
|
||||
const ruleType = { ...sampleRuleType };
|
||||
setupContract.registerType(ruleType);
|
||||
|
||||
expect(ruleType.config).toEqual({
|
||||
execution: {
|
||||
actions: { max: 123 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`applies rule type specific config if defined in config`, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
...generateAlertingConfig(),
|
||||
rules: {
|
||||
execution: {
|
||||
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({
|
||||
execution: {
|
||||
id: sampleRuleType.id,
|
||||
actions: {
|
||||
max: 123,
|
||||
},
|
||||
timeout: '1d',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerType()', () => {
|
||||
let setup: PluginSetupContract;
|
||||
const sampleRuleType: RuleType<never, never, never, never, never, 'default'> = {
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
actionGroups: [],
|
||||
defaultActionGroupId: 'default',
|
||||
producer: 'test',
|
||||
async executor() {},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
setup = await plugin.setup(setupMocks, mockPlugins);
|
||||
coreSetup = coreMock.createSetup();
|
||||
pluginsSetup = {
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(),
|
||||
licensing: licensingMock.createSetup(),
|
||||
eventLog: eventLogMock.createSetup(),
|
||||
actions: actionsMock.createSetup(),
|
||||
statusService: statusServiceMock.createSetupContract(),
|
||||
};
|
||||
setup = plugin.setup(coreSetup, pluginsSetup);
|
||||
});
|
||||
|
||||
it('should throw error when license type is invalid', async () => {
|
||||
|
@ -244,9 +221,19 @@ describe('Alerting Plugin', () => {
|
|||
describe('start()', () => {
|
||||
describe('getRulesClientWithRequest()', () => {
|
||||
it('throws error when encryptedSavedObjects plugin is missing encryption key', async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 10,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
|
||||
|
@ -277,9 +264,19 @@ describe('Alerting Plugin', () => {
|
|||
});
|
||||
|
||||
it(`doesn't throw error when encryptedSavedObjects plugin has encryption key`, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 10,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
const encryptedSavedObjectsSetup = {
|
||||
|
@ -324,9 +321,19 @@ describe('Alerting Plugin', () => {
|
|||
});
|
||||
|
||||
test(`exposes getAlertingAuthorizationWithRequest()`, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 100,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
const encryptedSavedObjectsSetup = {
|
||||
|
|
|
@ -62,7 +62,6 @@ import { getHealth } from './health/get_health';
|
|||
import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory';
|
||||
import { AlertingAuthorization } from './authorization';
|
||||
import { getSecurityHealth, SecurityHealth } from './lib/get_security_health';
|
||||
import { getRulesConfig } from './lib/get_rules_config';
|
||||
|
||||
export const EVENT_LOG_PROVIDER = 'alerting';
|
||||
export const EVENT_LOG_ACTIONS = {
|
||||
|
@ -263,8 +262,7 @@ export class AlertingPlugin {
|
|||
encryptedSavedObjects: plugins.encryptedSavedObjects,
|
||||
});
|
||||
|
||||
const alertingConfig: AlertingConfig = this.config;
|
||||
|
||||
const alertingConfig = this.config;
|
||||
return {
|
||||
registerType<
|
||||
Params extends AlertTypeParams = AlertTypeParams,
|
||||
|
@ -288,10 +286,7 @@ export class AlertingPlugin {
|
|||
if (!(ruleType.minimumLicenseRequired in LICENSE_TYPE)) {
|
||||
throw new Error(`"${ruleType.minimumLicenseRequired}" is not a valid license type`);
|
||||
}
|
||||
ruleType.config = getRulesConfig({
|
||||
config: alertingConfig.rules,
|
||||
ruleTypeId: ruleType.id,
|
||||
});
|
||||
|
||||
ruleType.ruleTaskTimeout =
|
||||
ruleType.ruleTaskTimeout ?? alertingConfig.defaultRuleTaskTimeout;
|
||||
ruleType.cancelAlertsOnRuleTimeout =
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1305,7 +1305,6 @@ export class RulesClient {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: new Date().toISOString(),
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
});
|
||||
try {
|
||||
|
|
|
@ -86,7 +86,6 @@ describe('aggregate()', () => {
|
|||
{ key: 'ok', doc_count: 10 },
|
||||
{ key: 'pending', doc_count: 4 },
|
||||
{ key: 'unknown', doc_count: 2 },
|
||||
{ key: 'warning', doc_count: 1 },
|
||||
],
|
||||
},
|
||||
enabled: {
|
||||
|
@ -136,7 +135,6 @@ describe('aggregate()', () => {
|
|||
"ok": 10,
|
||||
"pending": 4,
|
||||
"unknown": 2,
|
||||
"warning": 1,
|
||||
},
|
||||
"ruleEnabledStatus": Object {
|
||||
"disabled": 2,
|
||||
|
|
|
@ -403,7 +403,6 @@ describe('create()', () => {
|
|||
"error": null,
|
||||
"lastExecutionDate": "2019-02-12T21:01:22.479Z",
|
||||
"status": "pending",
|
||||
"warning": null,
|
||||
},
|
||||
"legacyId": null,
|
||||
"meta": Object {
|
||||
|
@ -605,7 +604,6 @@ describe('create()', () => {
|
|||
"error": null,
|
||||
"lastExecutionDate": "2019-02-12T21:01:22.479Z",
|
||||
"status": "pending",
|
||||
"warning": null,
|
||||
},
|
||||
"legacyId": "123",
|
||||
"meta": Object {
|
||||
|
@ -1032,7 +1030,6 @@ describe('create()', () => {
|
|||
error: null,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
meta: { versionApiKeyLastmodified: kibanaVersion },
|
||||
|
@ -1159,11 +1156,6 @@ describe('create()', () => {
|
|||
extractReferences: extractReferencesFn,
|
||||
injectReferences: injectReferencesFn,
|
||||
},
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
const data = getMockData({
|
||||
params: ruleParams,
|
||||
|
@ -1235,7 +1227,6 @@ describe('create()', () => {
|
|||
error: null,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
meta: { versionApiKeyLastmodified: kibanaVersion },
|
||||
|
@ -1331,11 +1322,6 @@ describe('create()', () => {
|
|||
extractReferences: extractReferencesFn,
|
||||
injectReferences: injectReferencesFn,
|
||||
},
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
const data = getMockData({
|
||||
params: ruleParams,
|
||||
|
@ -1407,7 +1393,6 @@ describe('create()', () => {
|
|||
error: null,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
meta: { versionApiKeyLastmodified: kibanaVersion },
|
||||
|
@ -1582,7 +1567,6 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -1712,7 +1696,6 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -1842,7 +1825,6 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -1987,7 +1969,6 @@ describe('create()', () => {
|
|||
status: 'pending',
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: {
|
||||
execution: {
|
||||
|
@ -2081,11 +2062,6 @@ describe('create()', () => {
|
|||
isExportable: true,
|
||||
async executor() {},
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"params invalid: [param1]: expected value of type [string] but got [undefined]"`
|
||||
|
@ -2362,7 +2338,6 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -2463,7 +2438,6 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -2543,11 +2517,6 @@ describe('create()', () => {
|
|||
extractReferences: jest.fn(),
|
||||
injectReferences: jest.fn(),
|
||||
},
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const data = getMockData({ schedule: { interval: '1s' } });
|
||||
|
|
|
@ -242,7 +242,6 @@ describe('enable()', () => {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -354,7 +353,6 @@ describe('enable()', () => {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -523,7 +521,6 @@ describe('enable()', () => {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -92,7 +92,6 @@ const BaseRuleSavedObject: SavedObject<RawRule> = {
|
|||
status: 'unknown',
|
||||
lastExecutionDate: '2020-08-20T19:23:38Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
|
|
|
@ -91,11 +91,6 @@ export function getBeforeSetup(
|
|||
isExportable: true,
|
||||
async executor() {},
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
rulesClientParams.getEventLogClient.mockResolvedValue(
|
||||
eventLogClient ?? eventLogClientMock.create()
|
||||
|
|
|
@ -295,7 +295,7 @@ function setupRawAlertMocks(
|
|||
|
||||
// splitting this out as it's easier to set a breakpoint :-)
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
unsecuredSavedObjectsClient.get.mockImplementation(async () =>
|
||||
unsecuredSavedObjectsClient.get.mockImplementation(async () =>
|
||||
cloneDeep(rawAlert)
|
||||
);
|
||||
|
||||
|
|
|
@ -163,16 +163,6 @@
|
|||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"message": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
SavedObjectAttribute,
|
||||
SavedObjectReference,
|
||||
} from '../../../../../src/core/server';
|
||||
import { RawRule, RawAlertAction, RawRuleExecutionStatus } from '../types';
|
||||
import { RawRule, RawAlertAction } from '../types';
|
||||
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
|
||||
import type { IsMigrationNeededPredicate } from '../../../encrypted_saved_objects/server';
|
||||
import { extractRefsFromGeoContainmentAlert } from './geo_containment/migrations';
|
||||
|
@ -280,7 +280,7 @@ function initializeExecutionStatus(
|
|||
status: 'pending',
|
||||
lastExecutionDate: new Date().toISOString(),
|
||||
error: null,
|
||||
} as RawRuleExecutionStatus,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,12 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createExecutionHandler } from './create_execution_handler';
|
||||
import { ActionsCompletion, AlertExecutionStore, CreateExecutionHandlerOptions } from './types';
|
||||
import { createExecutionHandler, CreateExecutionHandlerOptions } from './create_execution_handler';
|
||||
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import {
|
||||
actionsClientMock,
|
||||
actionsMock,
|
||||
actionsClientMock,
|
||||
renderActionParameterTemplatesDefault,
|
||||
} from '../../../actions/server/mocks';
|
||||
import { eventLoggerMock } from '../../../event_log/server/event_logger.mock';
|
||||
|
@ -19,10 +18,10 @@ import { asSavedObjectExecutionSource } from '../../../actions/server';
|
|||
import { InjectActionParamsOpts } from './inject_action_params';
|
||||
import { NormalizedRuleType } from '../rule_type_registry';
|
||||
import {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
} from '../types';
|
||||
|
||||
jest.mock('./inject_action_params', () => ({
|
||||
|
@ -53,11 +52,6 @@ const ruleType: NormalizedRuleType<
|
|||
},
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
|
@ -108,7 +102,6 @@ const createExecutionHandlerParams: jest.Mocked<
|
|||
supportsEphemeralTasks: false,
|
||||
maxEphemeralActionsPerRule: 10,
|
||||
};
|
||||
let alertExecutionStore: AlertExecutionStore;
|
||||
|
||||
describe('Create Execution Handler', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -124,22 +117,17 @@ describe('Create Execution Handler', () => {
|
|||
mockActionsPlugin.renderActionParameterTemplates.mockImplementation(
|
||||
renderActionParameterTemplatesDefault
|
||||
);
|
||||
alertExecutionStore = {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
};
|
||||
});
|
||||
|
||||
test('enqueues execution per selected action', async () => {
|
||||
const executionHandler = createExecutionHandler(createExecutionHandlerParams);
|
||||
await executionHandler({
|
||||
const result = await executionHandler({
|
||||
actionGroup: 'default',
|
||||
state: {},
|
||||
context: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(mockActionsPlugin.getActionsClientWithRequest).toHaveBeenCalledWith(
|
||||
createExecutionHandlerParams.request
|
||||
);
|
||||
|
@ -240,8 +228,6 @@ describe('Create Execution Handler', () => {
|
|||
stateVal: 'My goes here',
|
||||
},
|
||||
});
|
||||
|
||||
expect(alertExecutionStore.triggeredActionsStatus).toBe(ActionsCompletion.COMPLETE);
|
||||
});
|
||||
|
||||
test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => {
|
||||
|
@ -270,9 +256,7 @@ describe('Create Execution Handler', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(1);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledWith({
|
||||
id: '2',
|
||||
|
@ -320,14 +304,13 @@ describe('Create Execution Handler', () => {
|
|||
],
|
||||
});
|
||||
|
||||
await executionHandler({
|
||||
const result = await executionHandler({
|
||||
actionGroup: 'default',
|
||||
state: {},
|
||||
context: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(0);
|
||||
expect(result).toEqual([]);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0);
|
||||
|
||||
mockActionsPlugin.isActionExecutable.mockImplementation(() => true);
|
||||
|
@ -340,34 +323,31 @@ describe('Create Execution Handler', () => {
|
|||
state: {},
|
||||
context: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('limits actionsPlugin.execute per action group', async () => {
|
||||
const executionHandler = createExecutionHandler(createExecutionHandlerParams);
|
||||
await executionHandler({
|
||||
const result = await executionHandler({
|
||||
actionGroup: 'other-group',
|
||||
state: {},
|
||||
context: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(0);
|
||||
expect(result).toEqual([]);
|
||||
expect(actionsClient.enqueueExecution).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('context attribute gets parameterized', async () => {
|
||||
const executionHandler = createExecutionHandler(createExecutionHandlerParams);
|
||||
await executionHandler({
|
||||
const result = await executionHandler({
|
||||
actionGroup: 'default',
|
||||
context: { value: 'context-val' },
|
||||
state: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -404,13 +384,13 @@ describe('Create Execution Handler', () => {
|
|||
|
||||
test('state attribute gets parameterized', async () => {
|
||||
const executionHandler = createExecutionHandler(createExecutionHandlerParams);
|
||||
await executionHandler({
|
||||
const result = await executionHandler({
|
||||
actionGroup: 'default',
|
||||
context: {},
|
||||
state: { value: 'state-val' },
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -447,74 +427,17 @@ describe('Create Execution Handler', () => {
|
|||
|
||||
test(`logs an error when action group isn't part of actionGroups available for the ruleType`, async () => {
|
||||
const executionHandler = createExecutionHandler(createExecutionHandlerParams);
|
||||
await executionHandler({
|
||||
const result = await executionHandler({
|
||||
// we have to trick the compiler as this is an invalid type and this test checks whether we
|
||||
// enforce this at runtime as well as compile time
|
||||
actionGroup: 'invalid-group' as 'default' | 'other-group',
|
||||
context: {},
|
||||
state: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
expect(createExecutionHandlerParams.logger.error).toHaveBeenCalledWith(
|
||||
'Invalid action group "invalid-group" for rule "test".'
|
||||
);
|
||||
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(0);
|
||||
expect(alertExecutionStore.triggeredActionsStatus).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: {
|
||||
execution: {
|
||||
actions: { max: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
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: '3',
|
||||
group: 'default',
|
||||
actionTypeId: 'test3',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: '{{context.value}} goes here',
|
||||
stateVal: '{{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
alertExecutionStore = {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
};
|
||||
|
||||
await executionHandler({
|
||||
actionGroup: 'default',
|
||||
context: {},
|
||||
state: { value: 'state-val' },
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(2);
|
||||
expect(alertExecutionStore.triggeredActionsStatus).toBe(ActionsCompletion.PARTIAL);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,26 +4,73 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { Logger, KibanaRequest } from '../../../../../src/core/server';
|
||||
import { transformActionParams } from './transform_action_params';
|
||||
import { asSavedObjectExecutionSource } from '../../../actions/server';
|
||||
import { SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
|
||||
import {
|
||||
asSavedObjectExecutionSource,
|
||||
PluginStartContract as ActionsPluginStartContract,
|
||||
} from '../../../actions/server';
|
||||
import { IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
|
||||
import { EVENT_LOG_ACTIONS } from '../plugin';
|
||||
import { injectActionParams } from './inject_action_params';
|
||||
import {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
AlertAction,
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
RawRule,
|
||||
} from '../types';
|
||||
|
||||
import { UntypedNormalizedRuleType } from '../rule_type_registry';
|
||||
import { NormalizedRuleType, UntypedNormalizedRuleType } from '../rule_type_registry';
|
||||
import { isEphemeralTaskRejectedDueToCapacityError } from '../../../task_manager/server';
|
||||
import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object';
|
||||
import { ActionsCompletion, CreateExecutionHandlerOptions, ExecutionHandlerOptions } from './types';
|
||||
|
||||
export interface CreateExecutionHandlerOptions<
|
||||
Params extends AlertTypeParams,
|
||||
ExtractedParams extends AlertTypeParams,
|
||||
State extends AlertTypeState,
|
||||
InstanceState extends AlertInstanceState,
|
||||
InstanceContext extends AlertInstanceContext,
|
||||
ActionGroupIds extends string,
|
||||
RecoveryActionGroupId extends string
|
||||
> {
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
executionId: string;
|
||||
tags?: string[];
|
||||
actionsPlugin: ActionsPluginStartContract;
|
||||
actions: AlertAction[];
|
||||
spaceId: string;
|
||||
apiKey: RawRule['apiKey'];
|
||||
kibanaBaseUrl: string | undefined;
|
||||
ruleType: NormalizedRuleType<
|
||||
Params,
|
||||
ExtractedParams,
|
||||
State,
|
||||
InstanceState,
|
||||
InstanceContext,
|
||||
ActionGroupIds,
|
||||
RecoveryActionGroupId
|
||||
>;
|
||||
logger: Logger;
|
||||
eventLogger: IEventLogger;
|
||||
request: KibanaRequest;
|
||||
ruleParams: AlertTypeParams;
|
||||
supportsEphemeralTasks: boolean;
|
||||
maxEphemeralActionsPerRule: number;
|
||||
}
|
||||
|
||||
interface ExecutionHandlerOptions<ActionGroupIds extends string> {
|
||||
actionGroup: ActionGroupIds;
|
||||
actionSubgroup?: string;
|
||||
alertId: string;
|
||||
context: AlertInstanceContext;
|
||||
state: AlertInstanceState;
|
||||
}
|
||||
|
||||
export type ExecutionHandler<ActionGroupIds extends string> = (
|
||||
options: ExecutionHandlerOptions<ActionGroupIds>
|
||||
) => Promise<void>;
|
||||
) => Promise<AlertAction[]>;
|
||||
|
||||
export function createExecutionHandler<
|
||||
Params extends AlertTypeParams,
|
||||
|
@ -67,14 +114,13 @@ export function createExecutionHandler<
|
|||
actionSubgroup,
|
||||
context,
|
||||
state,
|
||||
alertExecutionStore,
|
||||
alertId,
|
||||
}: ExecutionHandlerOptions<ActionGroupIds | RecoveryActionGroupId>) => {
|
||||
const triggeredActions: AlertAction[] = [];
|
||||
if (!ruleTypeActionGroups.has(actionGroup)) {
|
||||
logger.error(`Invalid action group "${actionGroup}" for rule "${ruleType.id}".`);
|
||||
return;
|
||||
return triggeredActions;
|
||||
}
|
||||
|
||||
const actions = ruleActions
|
||||
.filter(({ group }) => group === actionGroup)
|
||||
.map((action) => {
|
||||
|
@ -117,11 +163,6 @@ export function createExecutionHandler<
|
|||
let ephemeralActionsToSchedule = maxEphemeralActionsPerRule;
|
||||
|
||||
for (const action of actions) {
|
||||
if (alertExecutionStore.numberOfTriggeredActions >= ruleType.config!.execution.actions.max) {
|
||||
alertExecutionStore.triggeredActionsStatus = ActionsCompletion.PARTIAL;
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
!actionsPlugin.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true })
|
||||
) {
|
||||
|
@ -164,11 +205,11 @@ export function createExecutionHandler<
|
|||
await actionsClient.enqueueExecution(enqueueOptions);
|
||||
}
|
||||
} finally {
|
||||
alertExecutionStore.numberOfTriggeredActions++;
|
||||
triggeredActions.push(action);
|
||||
}
|
||||
} else {
|
||||
await actionsClient.enqueueExecution(enqueueOptions);
|
||||
alertExecutionStore.numberOfTriggeredActions++;
|
||||
triggeredActions.push(action);
|
||||
}
|
||||
|
||||
const event = createAlertEventLogRecordObject({
|
||||
|
@ -203,5 +244,6 @@ export function createExecutionHandler<
|
|||
|
||||
eventLogger.logEvent(event);
|
||||
}
|
||||
return triggeredActions;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -53,15 +53,7 @@ export const RULE_ACTIONS = [
|
|||
},
|
||||
];
|
||||
|
||||
export const generateSavedObjectParams = ({
|
||||
error = null,
|
||||
warning = null,
|
||||
status = 'ok',
|
||||
}: {
|
||||
error?: null | { reason: string; message: string };
|
||||
warning?: null | { reason: string; message: string };
|
||||
status?: string;
|
||||
}) => [
|
||||
export const SAVED_OBJECT_UPDATE_PARAMS = [
|
||||
'alert',
|
||||
'1',
|
||||
{
|
||||
|
@ -79,11 +71,10 @@ export const generateSavedObjectParams = ({
|
|||
},
|
||||
},
|
||||
executionStatus: {
|
||||
error,
|
||||
error: null,
|
||||
lastDuration: 0,
|
||||
lastExecutionDate: '1970-01-01T00:00:00.000Z',
|
||||
status,
|
||||
warning,
|
||||
status: 'ok',
|
||||
},
|
||||
},
|
||||
{ refresh: false, namespace: undefined },
|
||||
|
@ -101,11 +92,6 @@ export const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockRunNowResponse = {
|
||||
|
@ -203,7 +189,6 @@ export const generateEventLog = ({
|
|||
instanceId,
|
||||
actionSubgroup,
|
||||
actionGroupId,
|
||||
actionId,
|
||||
status,
|
||||
numberOfTriggeredActions,
|
||||
savedObjects = [generateAlertSO('1')],
|
||||
|
@ -251,19 +236,11 @@ export const generateEventLog = ({
|
|||
...(task && {
|
||||
task: {
|
||||
schedule_delay: 0,
|
||||
scheduled: DATE_1970,
|
||||
scheduled: '1970-01-01T00:00:00.000Z',
|
||||
},
|
||||
}),
|
||||
},
|
||||
message: generateMessage({
|
||||
action,
|
||||
instanceId,
|
||||
actionGroupId,
|
||||
actionSubgroup,
|
||||
reason,
|
||||
status,
|
||||
actionId,
|
||||
}),
|
||||
message: generateMessage({ action, instanceId, actionGroupId, actionSubgroup, reason, status }),
|
||||
rule: {
|
||||
category: 'test',
|
||||
id: '1',
|
||||
|
@ -278,7 +255,6 @@ const generateMessage = ({
|
|||
instanceId,
|
||||
actionGroupId,
|
||||
actionSubgroup,
|
||||
actionId,
|
||||
reason,
|
||||
status,
|
||||
}: GeneratorParams) => {
|
||||
|
@ -303,9 +279,9 @@ const generateMessage = ({
|
|||
|
||||
if (action === EVENT_LOG_ACTIONS.executeAction) {
|
||||
if (actionSubgroup) {
|
||||
return `alert: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' instanceId: '${instanceId}' scheduled actionGroup(subgroup): 'default(${actionSubgroup})' action: action:${actionId}`;
|
||||
return `alert: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' instanceId: '${instanceId}' scheduled actionGroup(subgroup): 'default(${actionSubgroup})' action: action:${instanceId}`;
|
||||
}
|
||||
return `alert: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${actionId}`;
|
||||
return `alert: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${instanceId}`;
|
||||
}
|
||||
|
||||
if (action === EVENT_LOG_ACTIONS.execute) {
|
||||
|
@ -316,7 +292,7 @@ const generateMessage = ({
|
|||
return `${RULE_TYPE_ID}:${RULE_ID}: execution failed`;
|
||||
}
|
||||
if (actionGroupId === 'recovered') {
|
||||
return `rule-name' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${actionId}`;
|
||||
return `rule-name' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${instanceId}`;
|
||||
}
|
||||
return `rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`;
|
||||
}
|
||||
|
@ -334,7 +310,6 @@ export const generateRunnerResult = ({
|
|||
history = Array(false),
|
||||
state = false,
|
||||
interval = '10s',
|
||||
alertInstances = {},
|
||||
}: GeneratorParams = {}) => {
|
||||
return {
|
||||
monitoring: {
|
||||
|
@ -350,7 +325,7 @@ export const generateRunnerResult = ({
|
|||
interval,
|
||||
},
|
||||
state: {
|
||||
...(state && { alertInstances }),
|
||||
...(state && { alertInstances: {} }),
|
||||
...(state && { alertTypeState: undefined }),
|
||||
...(state && { previousStartedAt: new Date('1970-01-01T00:00:00.000Z') }),
|
||||
},
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
} from '../types';
|
||||
import {
|
||||
ConcreteTaskInstance,
|
||||
|
@ -56,7 +55,7 @@ import {
|
|||
generateRunnerResult,
|
||||
RULE_ACTIONS,
|
||||
generateEnqueueFunctionInput,
|
||||
generateSavedObjectParams,
|
||||
SAVED_OBJECT_UPDATE_PARAMS,
|
||||
mockTaskInstance,
|
||||
GENERIC_ERROR_MESSAGE,
|
||||
generateAlertInstance,
|
||||
|
@ -66,7 +65,6 @@ import {
|
|||
DATE_1970_5_MIN,
|
||||
} from './fixtures';
|
||||
import { EVENT_LOG_ACTIONS } from '../plugin';
|
||||
import { translations } from '../constants/translations';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
|
||||
|
@ -242,7 +240,7 @@ describe('Task Runner', () => {
|
|||
|
||||
expect(
|
||||
taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update
|
||||
).toHaveBeenCalledWith(...generateSavedObjectParams({}));
|
||||
).toHaveBeenCalledWith(...SAVED_OBJECT_UPDATE_PARAMS);
|
||||
|
||||
expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1);
|
||||
expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toHaveBeenCalledWith(
|
||||
|
@ -347,7 +345,6 @@ describe('Task Runner', () => {
|
|||
instanceId: '1',
|
||||
actionSubgroup: 'subDefault',
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('1')],
|
||||
actionId: '1',
|
||||
})
|
||||
);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
|
@ -919,7 +916,6 @@ describe('Task Runner', () => {
|
|||
action: EVENT_LOG_ACTIONS.executeAction,
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
actionId: '1',
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('1')],
|
||||
})
|
||||
);
|
||||
|
@ -1047,10 +1043,9 @@ describe('Task Runner', () => {
|
|||
4,
|
||||
generateEventLog({
|
||||
action: EVENT_LOG_ACTIONS.executeAction,
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('1')],
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
actionId: '1',
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('2')],
|
||||
actionGroupId: 'recovered',
|
||||
instanceId: '2',
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -1058,10 +1053,9 @@ describe('Task Runner', () => {
|
|||
5,
|
||||
generateEventLog({
|
||||
action: EVENT_LOG_ACTIONS.executeAction,
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('2')],
|
||||
actionGroupId: 'recovered',
|
||||
instanceId: '2',
|
||||
actionId: '2',
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('1')],
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
})
|
||||
);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
|
@ -1148,8 +1142,8 @@ describe('Task Runner', () => {
|
|||
const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger;
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(6);
|
||||
expect(enqueueFunction).toHaveBeenCalledTimes(2);
|
||||
expect((enqueueFunction as jest.Mock).mock.calls[1][0].id).toEqual('2');
|
||||
expect((enqueueFunction as jest.Mock).mock.calls[0][0].id).toEqual('1');
|
||||
expect((enqueueFunction as jest.Mock).mock.calls[1][0].id).toEqual('1');
|
||||
expect((enqueueFunction as jest.Mock).mock.calls[0][0].id).toEqual('2');
|
||||
expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
@ -2321,7 +2315,7 @@ describe('Task Runner', () => {
|
|||
);
|
||||
expect(
|
||||
taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update
|
||||
).toHaveBeenCalledWith(...generateSavedObjectParams({}));
|
||||
).toHaveBeenCalledWith(...SAVED_OBJECT_UPDATE_PARAMS);
|
||||
expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -2455,179 +2449,4 @@ 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: {
|
||||
execution: {
|
||||
actions: { max: 3 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const warning = {
|
||||
reason: AlertExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
message: translations.taskRunner.warning.maxExecutableActions,
|
||||
};
|
||||
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
|
||||
ruleType.executor.mockImplementation(
|
||||
async ({
|
||||
services: executorServices,
|
||||
}: AlertExecutorOptions<
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
string
|
||||
>) => {
|
||||
executorServices.alertFactory.create('1').scheduleActions('default');
|
||||
}
|
||||
);
|
||||
|
||||
rulesClient.get.mockResolvedValue({
|
||||
...mockedRuleTypeSavedObject,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '1',
|
||||
actionTypeId: 'action',
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '2',
|
||||
actionTypeId: 'action',
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '3',
|
||||
actionTypeId: 'action',
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '4',
|
||||
actionTypeId: 'action',
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '5',
|
||||
actionTypeId: 'action',
|
||||
},
|
||||
],
|
||||
} as jest.ResolvedValue<unknown>);
|
||||
ruleTypeRegistry.get.mockReturnValue(ruleTypeWithConfig);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT);
|
||||
|
||||
const taskRunner = new TaskRunner(
|
||||
ruleTypeWithConfig,
|
||||
mockedTaskInstance,
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
|
||||
const runnerResult = await taskRunner.run();
|
||||
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(
|
||||
ruleTypeWithConfig.config.execution.actions.max
|
||||
);
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const eventLogger = taskRunnerFactoryInitializerParams.eventLogger;
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(7);
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
generateEventLog({
|
||||
task: true,
|
||||
action: EVENT_LOG_ACTIONS.executeStart,
|
||||
})
|
||||
);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
generateEventLog({
|
||||
duration: 0,
|
||||
start: DATE_1970,
|
||||
action: EVENT_LOG_ACTIONS.newInstance,
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
})
|
||||
);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
generateEventLog({
|
||||
duration: 0,
|
||||
start: DATE_1970,
|
||||
action: EVENT_LOG_ACTIONS.activeInstance,
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
})
|
||||
);
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
generateEventLog({
|
||||
action: EVENT_LOG_ACTIONS.executeAction,
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('1')],
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
actionId: '1',
|
||||
})
|
||||
);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
generateEventLog({
|
||||
action: EVENT_LOG_ACTIONS.executeAction,
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('2')],
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
actionId: '2',
|
||||
})
|
||||
);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
6,
|
||||
generateEventLog({
|
||||
action: EVENT_LOG_ACTIONS.executeAction,
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('3')],
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
actionId: '3',
|
||||
})
|
||||
);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
7,
|
||||
generateEventLog({
|
||||
action: EVENT_LOG_ACTIONS.execute,
|
||||
outcome: 'success',
|
||||
status: 'warning',
|
||||
numberOfTriggeredActions: ruleTypeWithConfig.config.execution.actions.max,
|
||||
reason: AlertExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
task: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,57 +5,56 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import apm from 'elastic-apm-node';
|
||||
import { cloneDeep, mapValues, omit, pickBy, set, without } from 'lodash';
|
||||
import { pickBy, mapValues, without, cloneDeep, concat, set, omit } from 'lodash';
|
||||
import type { Request } from '@hapi/hapi';
|
||||
import { UsageCounter } from 'src/plugins/usage_collection/server';
|
||||
import uuid from 'uuid';
|
||||
import { addSpaceIdToPath } from '../../../spaces/server';
|
||||
import { KibanaRequest, Logger } from '../../../../../src/core/server';
|
||||
import { Logger, KibanaRequest } from '../../../../../src/core/server';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server';
|
||||
import { createExecutionHandler, ExecutionHandler } from './create_execution_handler';
|
||||
import { Alert as CreatedAlert, createAlertFactory } from '../alert';
|
||||
import {
|
||||
createWrappedScopedClusterClientFactory,
|
||||
ElasticsearchError,
|
||||
ErrorWithReason,
|
||||
executionStatusFromError,
|
||||
executionStatusFromState,
|
||||
getRecoveredAlerts,
|
||||
ruleExecutionStatusToRaw,
|
||||
validateRuleTypeParams,
|
||||
executionStatusFromState,
|
||||
executionStatusFromError,
|
||||
ruleExecutionStatusToRaw,
|
||||
ErrorWithReason,
|
||||
ElasticsearchError,
|
||||
} from '../lib';
|
||||
import {
|
||||
Alert,
|
||||
AlertExecutionStatus,
|
||||
AlertExecutionStatusErrorReasons,
|
||||
RawRule,
|
||||
IntervalSchedule,
|
||||
RawAlertInstance,
|
||||
RawRule,
|
||||
RawRuleExecutionStatus,
|
||||
RuleExecutionRunResult,
|
||||
RuleExecutionState,
|
||||
RuleTaskState,
|
||||
Alert,
|
||||
SanitizedAlert,
|
||||
AlertExecutionStatus,
|
||||
AlertExecutionStatusErrorReasons,
|
||||
RuleTypeRegistry,
|
||||
RuleMonitoring,
|
||||
RuleMonitoringHistory,
|
||||
RuleTaskState,
|
||||
RuleTypeRegistry,
|
||||
SanitizedAlert,
|
||||
RawRuleExecutionStatus,
|
||||
AlertAction,
|
||||
RuleExecutionState,
|
||||
RuleExecutionRunResult,
|
||||
} from '../types';
|
||||
import { asErr, asOk, map, promiseResult, resolveErr, Resultable } from '../lib/result_type';
|
||||
import { getExecutionDurationPercentiles, getExecutionSuccessRatio } from '../lib/monitoring';
|
||||
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
|
||||
import { getExecutionSuccessRatio, getExecutionDurationPercentiles } from '../lib/monitoring';
|
||||
import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
|
||||
import { EVENT_LOG_ACTIONS } from '../plugin';
|
||||
import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
|
||||
import { isAlertSavedObjectNotFoundError, isEsUnavailableError } from '../lib/is_alerting_error';
|
||||
import { partiallyUpdateAlert } from '../saved_objects';
|
||||
import {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
MONITORING_HISTORY_LIMIT,
|
||||
parseDuration,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
WithoutReservedActionGroups,
|
||||
parseDuration,
|
||||
MONITORING_HISTORY_LIMIT,
|
||||
} from '../../common';
|
||||
import { NormalizedRuleType, UntypedNormalizedRuleType } from '../rule_type_registry';
|
||||
import { getEsErrorMessage } from '../lib/errors';
|
||||
|
@ -63,9 +62,9 @@ import {
|
|||
createAlertEventLogRecordObject,
|
||||
Event,
|
||||
} from '../lib/create_alert_event_log_record_object';
|
||||
import { createWrappedScopedClusterClientFactory } from '../lib';
|
||||
import { getRecoveredAlerts } from '../lib';
|
||||
import {
|
||||
ActionsCompletion,
|
||||
AlertExecutionStore,
|
||||
GenerateNewAndRecoveredAlertEventsParams,
|
||||
LogActiveAndRecoveredAlertsParams,
|
||||
RuleTaskInstance,
|
||||
|
@ -270,8 +269,7 @@ export class TaskRunner<
|
|||
private async executeAlert(
|
||||
alertId: string,
|
||||
alert: CreatedAlert<InstanceState, InstanceContext>,
|
||||
executionHandler: ExecutionHandler<ActionGroupIds | RecoveryActionGroupId>,
|
||||
alertExecutionStore: AlertExecutionStore
|
||||
executionHandler: ExecutionHandler<ActionGroupIds | RecoveryActionGroupId>
|
||||
) {
|
||||
const {
|
||||
actionGroup,
|
||||
|
@ -281,14 +279,7 @@ export class TaskRunner<
|
|||
} = alert.getScheduledActionOptions()!;
|
||||
alert.updateLastScheduledActions(actionGroup, actionSubgroup);
|
||||
alert.unscheduleActions();
|
||||
return executionHandler({
|
||||
actionGroup,
|
||||
actionSubgroup,
|
||||
context,
|
||||
state,
|
||||
alertId,
|
||||
alertExecutionStore,
|
||||
});
|
||||
return executionHandler({ actionGroup, actionSubgroup, context, state, alertId });
|
||||
}
|
||||
|
||||
private async executeAlerts(
|
||||
|
@ -471,15 +462,26 @@ export class TaskRunner<
|
|||
});
|
||||
}
|
||||
|
||||
const alertExecutionStore: AlertExecutionStore = {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
};
|
||||
|
||||
let triggeredActions: AlertAction[] = [];
|
||||
if (!muteAll && this.shouldLogAndScheduleActionsForAlerts()) {
|
||||
const mutedAlertIdsSet = new Set(mutedInstanceIds);
|
||||
|
||||
const alertsWithExecutableActions = Object.entries(alertsWithScheduledActions).filter(
|
||||
const scheduledActionsForRecoveredAlerts = await scheduleActionsForRecoveredAlerts<
|
||||
InstanceState,
|
||||
InstanceContext,
|
||||
RecoveryActionGroupId
|
||||
>({
|
||||
recoveryActionGroup: this.ruleType.recoveryActionGroup,
|
||||
recoveredAlerts,
|
||||
executionHandler,
|
||||
mutedAlertIdsSet,
|
||||
logger: this.logger,
|
||||
ruleLabel,
|
||||
});
|
||||
|
||||
triggeredActions = concat(triggeredActions, scheduledActionsForRecoveredAlerts);
|
||||
|
||||
const alertsToExecute = Object.entries(alertsWithScheduledActions).filter(
|
||||
([alertName, alert]: [string, CreatedAlert<InstanceState, InstanceContext>]) => {
|
||||
const throttled = alert.isThrottled(throttle);
|
||||
const muted = mutedAlertIdsSet.has(alertName);
|
||||
|
@ -506,26 +508,14 @@ export class TaskRunner<
|
|||
}
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
alertsWithExecutableActions.map(
|
||||
const allTriggeredActions = await Promise.all(
|
||||
alertsToExecute.map(
|
||||
([alertId, alert]: [string, CreatedAlert<InstanceState, InstanceContext>]) =>
|
||||
this.executeAlert(alertId, alert, executionHandler, alertExecutionStore)
|
||||
this.executeAlert(alertId, alert, executionHandler)
|
||||
)
|
||||
);
|
||||
|
||||
await scheduleActionsForRecoveredAlerts<
|
||||
InstanceState,
|
||||
InstanceContext,
|
||||
RecoveryActionGroupId
|
||||
>({
|
||||
recoveryActionGroup: this.ruleType.recoveryActionGroup,
|
||||
recoveredAlerts,
|
||||
executionHandler,
|
||||
mutedAlertIdsSet,
|
||||
logger: this.logger,
|
||||
ruleLabel,
|
||||
alertExecutionStore,
|
||||
});
|
||||
triggeredActions = concat(triggeredActions, ...allTriggeredActions);
|
||||
} else {
|
||||
if (muteAll) {
|
||||
this.logger.debug(`no scheduling of actions for rule ${ruleLabel}: rule is muted.`);
|
||||
|
@ -545,7 +535,7 @@ export class TaskRunner<
|
|||
|
||||
return {
|
||||
metrics: searchMetrics,
|
||||
alertExecutionStore,
|
||||
triggeredActions,
|
||||
alertTypeState: updatedRuleTypeState || undefined,
|
||||
alertInstances: mapValues<
|
||||
Record<string, CreatedAlert<InstanceState, InstanceContext>>,
|
||||
|
@ -649,7 +639,7 @@ export class TaskRunner<
|
|||
),
|
||||
schedule: asOk(
|
||||
// fetch the rule again to ensure we return the correct schedule as it may have
|
||||
// changed during the task execution
|
||||
// cahnged during the task execution
|
||||
(await rulesClient.get({ id: ruleId })).schedule
|
||||
),
|
||||
};
|
||||
|
@ -724,6 +714,7 @@ export class TaskRunner<
|
|||
(ruleExecutionState) => executionStatusFromState(ruleExecutionState),
|
||||
(err: ElasticsearchError) => executionStatusFromError(err)
|
||||
);
|
||||
|
||||
// set the executionStatus date to same as event, if it's set
|
||||
if (event.event?.start) {
|
||||
executionStatus.lastExecutionDate = new Date(event.event.start);
|
||||
|
@ -766,10 +757,6 @@ export class TaskRunner<
|
|||
}
|
||||
monitoringHistory.success = false;
|
||||
} else {
|
||||
if (executionStatus.warning) {
|
||||
set(event, 'event.reason', executionStatus.warning?.reason || 'unknown');
|
||||
set(event, 'message', event?.message || executionStatus.warning.message);
|
||||
}
|
||||
set(
|
||||
event,
|
||||
'kibana.alert.rule.execution.metrics.number_of_triggered_actions',
|
||||
|
@ -820,7 +807,7 @@ export class TaskRunner<
|
|||
executionState: RuleExecutionState
|
||||
): RuleTaskState => {
|
||||
return {
|
||||
...omit(executionState, ['alertExecutionStore', 'metrics']),
|
||||
...omit(executionState, ['triggeredActions', 'metrics']),
|
||||
previousStartedAt: startedAt,
|
||||
};
|
||||
};
|
||||
|
@ -1123,7 +1110,7 @@ async function scheduleActionsForRecoveredAlerts<
|
|||
InstanceContext,
|
||||
RecoveryActionGroupId
|
||||
>
|
||||
): Promise<void> {
|
||||
): Promise<AlertAction[]> {
|
||||
const {
|
||||
logger,
|
||||
recoveryActionGroup,
|
||||
|
@ -1131,10 +1118,9 @@ async function scheduleActionsForRecoveredAlerts<
|
|||
executionHandler,
|
||||
mutedAlertIdsSet,
|
||||
ruleLabel,
|
||||
alertExecutionStore,
|
||||
} = params;
|
||||
const recoveredIds = Object.keys(recoveredAlerts);
|
||||
|
||||
let triggeredActions: AlertAction[] = [];
|
||||
for (const id of recoveredIds) {
|
||||
if (mutedAlertIdsSet.has(id)) {
|
||||
logger.debug(
|
||||
|
@ -1144,16 +1130,17 @@ async function scheduleActionsForRecoveredAlerts<
|
|||
const alert = recoveredAlerts[id];
|
||||
alert.updateLastScheduledActions(recoveryActionGroup.id);
|
||||
alert.unscheduleActions();
|
||||
await executionHandler({
|
||||
const triggeredActionsForRecoveredAlert = await executionHandler({
|
||||
actionGroup: recoveryActionGroup.id,
|
||||
context: alert.getContext(),
|
||||
state: {},
|
||||
alertId: id,
|
||||
alertExecutionStore,
|
||||
});
|
||||
alert.scheduleActions(recoveryActionGroup.id);
|
||||
triggeredActions = concat(triggeredActions, triggeredActionsForRecoveredAlert);
|
||||
}
|
||||
}
|
||||
return triggeredActions;
|
||||
}
|
||||
|
||||
function logActiveAndRecoveredAlerts<
|
||||
|
|
|
@ -55,11 +55,6 @@ const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let fakeTimer: sinon.SinonFakeTimers;
|
||||
|
@ -367,7 +362,6 @@ describe('Task Runner Cancel', () => {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: '1970-01-01T00:00:00.000Z',
|
||||
status: 'error',
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
{ refresh: false, namespace: undefined }
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
*/
|
||||
|
||||
import { Dictionary } from 'lodash';
|
||||
import { KibanaRequest, Logger } from 'kibana/server';
|
||||
import { Logger } from 'kibana/server';
|
||||
import {
|
||||
ActionGroup,
|
||||
AlertAction,
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
AlertTypeParams,
|
||||
|
@ -25,8 +24,6 @@ import { Alert as CreatedAlert } from '../alert';
|
|||
import { IEventLogger } from '../../../event_log/server';
|
||||
import { NormalizedRuleType } from '../rule_type_registry';
|
||||
import { ExecutionHandler } from './create_execution_handler';
|
||||
import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server';
|
||||
import { RawRule } from '../types';
|
||||
|
||||
export interface RuleTaskRunResultWithActions {
|
||||
state: RuleExecutionState;
|
||||
|
@ -92,7 +89,6 @@ export interface ScheduleActionsForRecoveredAlertsParams<
|
|||
executionHandler: ExecutionHandler<RecoveryActionGroupId | RecoveryActionGroupId>;
|
||||
mutedAlertIdsSet: Set<string>;
|
||||
ruleLabel: string;
|
||||
alertExecutionStore: AlertExecutionStore;
|
||||
}
|
||||
|
||||
export interface LogActiveAndRecoveredAlertsParams<
|
||||
|
@ -107,59 +103,3 @@ export interface LogActiveAndRecoveredAlertsParams<
|
|||
ruleLabel: string;
|
||||
canSetRecoveryContext: boolean;
|
||||
}
|
||||
|
||||
// / ExecutionHandler
|
||||
|
||||
export interface CreateExecutionHandlerOptions<
|
||||
Params extends AlertTypeParams,
|
||||
ExtractedParams extends AlertTypeParams,
|
||||
State extends AlertTypeState,
|
||||
InstanceState extends AlertInstanceState,
|
||||
InstanceContext extends AlertInstanceContext,
|
||||
ActionGroupIds extends string,
|
||||
RecoveryActionGroupId extends string
|
||||
> {
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
executionId: string;
|
||||
tags?: string[];
|
||||
actionsPlugin: ActionsPluginStartContract;
|
||||
actions: AlertAction[];
|
||||
spaceId: string;
|
||||
apiKey: RawRule['apiKey'];
|
||||
kibanaBaseUrl: string | undefined;
|
||||
ruleType: NormalizedRuleType<
|
||||
Params,
|
||||
ExtractedParams,
|
||||
State,
|
||||
InstanceState,
|
||||
InstanceContext,
|
||||
ActionGroupIds,
|
||||
RecoveryActionGroupId
|
||||
>;
|
||||
logger: Logger;
|
||||
eventLogger: IEventLogger;
|
||||
request: KibanaRequest;
|
||||
ruleParams: AlertTypeParams;
|
||||
supportsEphemeralTasks: boolean;
|
||||
maxEphemeralActionsPerRule: number;
|
||||
}
|
||||
|
||||
export interface ExecutionHandlerOptions<ActionGroupIds extends string> {
|
||||
actionGroup: ActionGroupIds;
|
||||
actionSubgroup?: string;
|
||||
alertId: string;
|
||||
context: AlertInstanceContext;
|
||||
state: AlertInstanceState;
|
||||
alertExecutionStore: AlertExecutionStore;
|
||||
}
|
||||
|
||||
export enum ActionsCompletion {
|
||||
COMPLETE = 'complete',
|
||||
PARTIAL = 'partial',
|
||||
}
|
||||
|
||||
export interface AlertExecutionStore {
|
||||
numberOfTriggeredActions: number;
|
||||
triggeredActionsStatus: ActionsCompletion;
|
||||
}
|
||||
|
|
|
@ -39,10 +39,9 @@ import {
|
|||
SanitizedRuleConfig,
|
||||
RuleMonitoring,
|
||||
MappedParams,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
} from '../common';
|
||||
import { LicenseType } from '../../licensing/server';
|
||||
import { RulesConfig } from './config';
|
||||
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
|
||||
|
||||
|
@ -125,7 +124,6 @@ export type ExecutorType<
|
|||
export interface AlertTypeParamsValidator<Params extends AlertTypeParams> {
|
||||
validate: (object: unknown) => Params;
|
||||
}
|
||||
|
||||
export interface RuleType<
|
||||
Params extends AlertTypeParams = never,
|
||||
ExtractedParams extends AlertTypeParams = never,
|
||||
|
@ -170,7 +168,6 @@ export interface RuleType<
|
|||
ruleTaskTimeout?: string;
|
||||
cancelAlertsOnRuleTimeout?: boolean;
|
||||
doesSetRecoveryContext?: boolean;
|
||||
config?: RulesConfig;
|
||||
}
|
||||
export type UntypedRuleType = RuleType<
|
||||
AlertTypeParams,
|
||||
|
@ -202,10 +199,6 @@ export interface RawRuleExecutionStatus extends SavedObjectAttributes {
|
|||
reason: AlertExecutionStatusErrorReasons;
|
||||
message: string;
|
||||
};
|
||||
warning: null | {
|
||||
reason: AlertExecutionStatusWarningReasons;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type PartialAlert<Params extends AlertTypeParams = never> = Pick<Alert<Params>, 'id'> &
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
import {
|
||||
ActionGroup,
|
||||
AlertExecutionStatusErrorReasons,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
ALERTS_FEATURE_ID,
|
||||
} from '../../../../../../alerting/common';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -120,28 +119,6 @@ describe('rule_details', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the rule warning banner with warning message, when rule status is a warning', () => {
|
||||
const rule = mockRule({
|
||||
executionStatus: {
|
||||
status: 'warning',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
warning: {
|
||||
reason: AlertExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
message: 'warning message',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
shallow(
|
||||
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
|
||||
).containsMatchingElement(
|
||||
<EuiText size="s" color="warning" data-test-subj="ruleWarningMessageText">
|
||||
{'warning message'}
|
||||
</EuiText>
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('renders an rule action', () => {
|
||||
const rule = mockRule({
|
||||
|
|
|
@ -40,10 +40,7 @@ import { RuleRouteWithApi } from './rule_route';
|
|||
import { ViewInApp } from './view_in_app';
|
||||
import { RuleEdit } from '../../rule_form';
|
||||
import { routeToRuleDetails } from '../../../constants';
|
||||
import {
|
||||
rulesErrorReasonTranslationsMapping,
|
||||
rulesWarningReasonTranslationsMapping,
|
||||
} from '../../rules_list/translations';
|
||||
import { rulesErrorReasonTranslationsMapping } from '../../rules_list/translations';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ruleReducer } from '../../rule_form/rule_reducer';
|
||||
import { loadAllActions as loadConnectors } from '../../../lib/action_connector_api';
|
||||
|
@ -138,8 +135,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
const [isMutedUpdating, setIsMutedUpdating] = useState<boolean>(false);
|
||||
const [isMuted, setIsMuted] = useState<boolean>(rule.muteAll);
|
||||
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
|
||||
const [dismissRuleErrors, setDismissRuleErrors] = useState<boolean>(false);
|
||||
const [dismissRuleWarning, setDismissRuleWarning] = useState<boolean>(false);
|
||||
const [dissmissRuleErrors, setDissmissRuleErrors] = useState<boolean>(false);
|
||||
|
||||
const setRule = async () => {
|
||||
history.push(routeToRuleDetails.replace(`:ruleId`, rule.id));
|
||||
|
@ -153,14 +149,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const getRuleStatusWarningReasonText = () => {
|
||||
if (rule.executionStatus.warning && rule.executionStatus.warning.reason) {
|
||||
return rulesWarningReasonTranslationsMapping[rule.executionStatus.warning.reason];
|
||||
} else {
|
||||
return rulesWarningReasonTranslationsMapping.unknown;
|
||||
}
|
||||
};
|
||||
|
||||
const rightPageHeaderButtons = hasEditButton
|
||||
? [
|
||||
<>
|
||||
|
@ -306,7 +294,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
setIsEnabled(false);
|
||||
await disableRule(rule);
|
||||
// Reset dismiss if previously clicked
|
||||
setDismissRuleErrors(false);
|
||||
setDissmissRuleErrors(false);
|
||||
} else {
|
||||
setIsEnabled(true);
|
||||
await enableRule(rule);
|
||||
|
@ -369,7 +357,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{rule.enabled && !dismissRuleErrors && rule.executionStatus.status === 'error' ? (
|
||||
{rule.enabled && !dissmissRuleErrors && rule.executionStatus.status === 'error' ? (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut
|
||||
|
@ -388,7 +376,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
<EuiButton
|
||||
data-test-subj="dismiss-execution-error"
|
||||
color="danger"
|
||||
onClick={() => setDismissRuleErrors(true)}
|
||||
onClick={() => setDissmissRuleErrors(true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.dismissButtonTitle"
|
||||
|
@ -416,39 +404,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
|
||||
{rule.enabled && !dismissRuleWarning && rule.executionStatus.status === 'warning' ? (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
data-test-subj="ruleWarningBanner"
|
||||
size="s"
|
||||
title={getRuleStatusWarningReasonText()}
|
||||
iconType="alert"
|
||||
>
|
||||
<EuiText size="s" color="warning" data-test-subj="ruleWarningMessageText">
|
||||
{rule.executionStatus.warning?.message}
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="s" wrap={true}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="dismiss-execution-warning"
|
||||
color="warning"
|
||||
onClick={() => setDismissRuleWarning(true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.dismissButtonTitle"
|
||||
defaultMessage="Dismiss"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
{hasActionsWithBrokenConnector && (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -102,8 +102,6 @@ export function getHealthColor(status: AlertExecutionStatuses) {
|
|||
return 'primary';
|
||||
case 'pending':
|
||||
return 'accent';
|
||||
case 'warning':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'subdued';
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import { RulesList, percentileFields } from './rules_list';
|
|||
import { RuleTypeModel, ValidationResult, Percentiles } from '../../../../types';
|
||||
import {
|
||||
AlertExecutionStatusErrorReasons,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
ALERTS_FEATURE_ID,
|
||||
parseDuration,
|
||||
} from '../../../../../../alerting/common';
|
||||
|
@ -31,7 +30,6 @@ jest.mock('../../../lib/action_connector_api', () => ({
|
|||
jest.mock('../../../lib/rule_api', () => ({
|
||||
loadRules: jest.fn(),
|
||||
loadRuleTypes: jest.fn(),
|
||||
loadRuleAggregations: jest.fn(),
|
||||
alertingFrameworkHealth: jest.fn(() => ({
|
||||
isSufficientlySecure: true,
|
||||
hasPermanentEncryptionKey: true,
|
||||
|
@ -57,8 +55,7 @@ jest.mock('../../../lib/capabilities', () => ({
|
|||
hasShowActionsCapability: jest.fn(() => true),
|
||||
hasExecuteActionsCapability: jest.fn(() => true),
|
||||
}));
|
||||
const { loadRules, loadRuleTypes, loadRuleAggregations } =
|
||||
jest.requireMock('../../../lib/rule_api');
|
||||
const { loadRules, loadRuleTypes } = jest.requireMock('../../../lib/rule_api');
|
||||
const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api');
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
@ -334,32 +331,6 @@ describe('rules_list component with items', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'test rule warning',
|
||||
tags: [],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'warning',
|
||||
lastDuration: 500,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
warning: {
|
||||
reason: AlertExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
message: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function setup(editable: boolean = true) {
|
||||
|
@ -381,11 +352,6 @@ describe('rules_list component with items', () => {
|
|||
]);
|
||||
loadRuleTypes.mockResolvedValue([ruleTypeFromApi]);
|
||||
loadAllActions.mockResolvedValue([]);
|
||||
loadRuleAggregations.mockResolvedValue({
|
||||
ruleEnabledStatus: { enabled: 2, disabled: 0 },
|
||||
ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 },
|
||||
ruleMutedStatus: { muted: 0, unmuted: 2 },
|
||||
});
|
||||
|
||||
const ruleTypeMock: RuleTypeModel = {
|
||||
id: 'test_rule_type',
|
||||
|
@ -415,7 +381,6 @@ describe('rules_list component with items', () => {
|
|||
|
||||
expect(loadRules).toHaveBeenCalled();
|
||||
expect(loadActionTypes).toHaveBeenCalled();
|
||||
expect(loadRuleAggregations).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
it('renders table of rules', async () => {
|
||||
|
@ -506,7 +471,6 @@ describe('rules_list component with items', () => {
|
|||
expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-pending"]').length).toEqual(1);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-unknown"]').length).toEqual(0);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').length).toEqual(2);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-warning"]').length).toEqual(1);
|
||||
expect(wrapper.find('[data-test-subj="ruleStatus-error-tooltip"]').length).toEqual(2);
|
||||
expect(
|
||||
wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').length
|
||||
|
@ -764,28 +728,6 @@ describe('rules_list component with items', () => {
|
|||
expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders brief', async () => {
|
||||
await setup();
|
||||
|
||||
// { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="totalOkRulesCount"]').text()).toEqual('Ok: 1');
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="totalActiveRulesCount"]').text()).toEqual(
|
||||
'Active: 2'
|
||||
);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="totalErrorRulesCount"]').text()).toEqual(
|
||||
'Error: 3'
|
||||
);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="totalPendingRulesCount"]').text()).toEqual(
|
||||
'Pending: 4'
|
||||
);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="totalUnknownRulesCount"]').text()).toEqual(
|
||||
'Unknown: 5'
|
||||
);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="totalWarningRulesCount"]').text()).toEqual(
|
||||
'Warning: 6'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rules_list component empty with show only capability', () => {
|
||||
|
@ -951,7 +893,7 @@ describe('rules_list with show only capability', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('rules_list with disabled items', () => {
|
||||
describe('rules_list with disabled itmes', () => {
|
||||
let wrapper: ReactWrapper<any>;
|
||||
|
||||
async function setup() {
|
||||
|
|
|
@ -986,17 +986,6 @@ export const RulesList: React.FunctionComponent = () => {
|
|||
/>
|
||||
</EuiHealth>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHealth color="warning" data-test-subj="totalWarningRulesCount">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.rulesList.totalStatusesWarningDescription"
|
||||
defaultMessage="Warning: {totalStatusesWarning}"
|
||||
values={{
|
||||
totalStatusesWarning: rulesStatusesTotal.warning,
|
||||
}}
|
||||
/>
|
||||
</EuiHealth>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHealth color="primary" data-test-subj="totalOkRulesCount">
|
||||
<FormattedMessage
|
||||
|
|
|
@ -49,20 +49,12 @@ export const ALERT_STATUS_UNKNOWN = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ALERT_STATUS_WARNING = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.ruleStatusWarning',
|
||||
{
|
||||
defaultMessage: 'Warning',
|
||||
}
|
||||
);
|
||||
|
||||
export const rulesStatusesTranslationsMapping = {
|
||||
ok: ALERT_STATUS_OK,
|
||||
active: ALERT_STATUS_ACTIVE,
|
||||
error: ALERT_STATUS_ERROR,
|
||||
pending: ALERT_STATUS_PENDING,
|
||||
unknown: ALERT_STATUS_UNKNOWN,
|
||||
warning: ALERT_STATUS_WARNING,
|
||||
};
|
||||
|
||||
export const ALERT_ERROR_UNKNOWN_REASON = i18n.translate(
|
||||
|
@ -114,20 +106,6 @@ export const ALERT_ERROR_DISABLED_REASON = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ALERT_WARNING_MAX_EXECUTABLE_ACTIONS_REASON = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.ruleWarningReasonMaxExecutableActions',
|
||||
{
|
||||
defaultMessage: 'Action limit exceeded',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALERT_WARNING_UNKNOWN_REASON = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.ruleWarningReasonUnknown',
|
||||
{
|
||||
defaultMessage: 'Unknown reason',
|
||||
}
|
||||
);
|
||||
|
||||
export const rulesErrorReasonTranslationsMapping = {
|
||||
read: ALERT_ERROR_READING_REASON,
|
||||
decrypt: ALERT_ERROR_DECRYPTING_REASON,
|
||||
|
@ -137,8 +115,3 @@ export const rulesErrorReasonTranslationsMapping = {
|
|||
timeout: ALERT_ERROR_TIMEOUT_REASON,
|
||||
disabled: ALERT_ERROR_DISABLED_REASON,
|
||||
};
|
||||
|
||||
export const rulesWarningReasonTranslationsMapping = {
|
||||
maxExecutableActions: ALERT_WARNING_MAX_EXECUTABLE_ACTIONS_REASON,
|
||||
unknown: ALERT_WARNING_UNKNOWN_REASON,
|
||||
};
|
||||
|
|
|
@ -36,7 +36,6 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
|
|||
error: 0,
|
||||
pending: 0,
|
||||
unknown: 0,
|
||||
warning: 0,
|
||||
},
|
||||
rule_muted_status: {
|
||||
muted: 0,
|
||||
|
@ -112,7 +111,6 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
|
|||
error: NumErrorAlerts,
|
||||
pending: 0,
|
||||
unknown: 0,
|
||||
warning: 0,
|
||||
},
|
||||
rule_muted_status: {
|
||||
muted: 0,
|
||||
|
@ -185,7 +183,6 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
|
|||
error: NumErrorAlerts,
|
||||
pending: 0,
|
||||
unknown: 0,
|
||||
warning: 0,
|
||||
},
|
||||
ruleEnabledStatus: {
|
||||
disabled: 0,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue