Revert "[Alerting] Limit the executable actions per Rule execution (#126902)"

This reverts commit 31099bc68f.
This commit is contained in:
Brian Seeders 2022-03-18 10:20:18 -04:00
parent 0847d47f8b
commit 242c5fd63f
No known key found for this signature in database
GPG key ID: 424927146CC9A682
38 changed files with 748 additions and 1611 deletions

View file

@ -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`.

View file

@ -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

View file

@ -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;

View file

@ -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([

View file

@ -23,13 +23,6 @@ describe('config validation', () => {
},
"maxEphemeralActionsPerAlert": 10,
"minimumScheduleInterval": "1m",
"rules": Object {
"execution": Object {
"actions": Object {
"max": 100000,
},
},
},
}
`);
});

View file

@ -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'>;

View file

@ -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.',
}
),
},
},
};

View file

@ -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);
});
});

View file

@ -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,
},
};
};

View file

@ -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'
);

View file

@ -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,
});

View file

@ -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 = {

View file

@ -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

View file

@ -1305,7 +1305,6 @@ export class RulesClient {
lastDuration: 0,
lastExecutionDate: new Date().toISOString(),
error: null,
warning: null,
},
});
try {

View file

@ -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,

View file

@ -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' } });

View file

@ -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,
},
},
{

View file

@ -92,7 +92,6 @@ const BaseRuleSavedObject: SavedObject<RawRule> = {
status: 'unknown',
lastExecutionDate: '2020-08-20T19:23:38Z',
error: null,
warning: null,
},
},
references: [],

View file

@ -91,11 +91,6 @@ export function getBeforeSetup(
isExportable: true,
async executor() {},
producer: 'alerts',
config: {
execution: {
actions: { max: 1000 },
},
},
}));
rulesClientParams.getEventLogClient.mockResolvedValue(
eventLogClient ?? eventLogClientMock.create()

View file

@ -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)
);

View file

@ -163,16 +163,6 @@
"type": "keyword"
}
}
},
"warning": {
"properties": {
"reason": {
"type": "keyword"
},
"message": {
"type": "keyword"
}
}
}
}
},

View file

@ -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,
},
},
};
}

View file

@ -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);
});
});

View file

@ -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;
};
}

View file

@ -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') }),
},

View file

@ -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,
})
);
});
});

View file

@ -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<

View file

@ -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 }

View file

@ -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;
}

View file

@ -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'> &

View file

@ -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({

View file

@ -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>

View file

@ -102,8 +102,6 @@ export function getHealthColor(status: AlertExecutionStatuses) {
return 'primary';
case 'pending':
return 'accent';
case 'warning':
return 'warning';
default:
return 'subdued';
}

View file

@ -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() {

View file

@ -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

View file

@ -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,
};

View file

@ -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,