mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Alerting] Limit the executable actions per Rule execution (#128079)
This commit is contained in:
parent
4920ace1d5
commit
45c4e7dac1
40 changed files with 1630 additions and 758 deletions
|
@ -200,4 +200,7 @@ 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`.
|
||||
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.
|
|
@ -200,6 +200,7 @@ 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,7 +22,14 @@ 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'] as const;
|
||||
export const AlertExecutionStatusValues = [
|
||||
'ok',
|
||||
'active',
|
||||
'error',
|
||||
'pending',
|
||||
'unknown',
|
||||
'warning',
|
||||
] as const;
|
||||
export type AlertExecutionStatuses = typeof AlertExecutionStatusValues[number];
|
||||
|
||||
export enum AlertExecutionStatusErrorReasons {
|
||||
|
@ -35,6 +42,10 @@ export enum AlertExecutionStatusErrorReasons {
|
|||
Disabled = 'disabled',
|
||||
}
|
||||
|
||||
export enum AlertExecutionStatusWarningReasons {
|
||||
MAX_EXECUTABLE_ACTIONS = 'maxExecutableActions',
|
||||
}
|
||||
|
||||
export interface AlertExecutionStatus {
|
||||
status: AlertExecutionStatuses;
|
||||
numberOfTriggeredActions?: number;
|
||||
|
@ -45,6 +56,10 @@ export interface AlertExecutionStatus {
|
|||
reason: AlertExecutionStatusErrorReasons;
|
||||
message: string;
|
||||
};
|
||||
warning?: {
|
||||
reason: AlertExecutionStatusWarningReasons;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AlertActionParams = SavedObjectAttributes;
|
||||
|
|
|
@ -10,13 +10,6 @@ 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),
|
||||
|
@ -29,11 +22,16 @@ 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;
|
||||
triggeredActions: Array<t.TypeOf<typeof actionSchema>>;
|
||||
alertExecutionStore: t.TypeOf<typeof alertExecutionStore>;
|
||||
};
|
||||
|
||||
export const ruleParamsSchema = t.intersection([
|
||||
|
|
|
@ -23,6 +23,13 @@ describe('config validation', () => {
|
|||
},
|
||||
"maxEphemeralActionsPerAlert": 10,
|
||||
"minimumScheduleInterval": "1m",
|
||||
"rules": Object {
|
||||
"execution": Object {
|
||||
"actions": Object {
|
||||
"max": 100000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -8,6 +8,21 @@
|
|||
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({
|
||||
|
@ -23,7 +38,10 @@ 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'>;
|
||||
|
|
22
x-pack/plugins/alerting/server/constants/translations.ts
Normal file
22
x-pack/plugins/alerting/server/constants/translations.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.',
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
45
x-pack/plugins/alerting/server/lib/get_rules_config.test.ts
Normal file
45
x-pack/plugins/alerting/server/lib/get_rules_config.test.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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);
|
||||
});
|
||||
});
|
28
x-pack/plugins/alerting/server/lib/get_rules_config.ts
Normal file
28
x-pack/plugins/alerting/server/lib/get_rules_config.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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,7 +6,11 @@
|
|||
*/
|
||||
|
||||
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import { AlertAction, AlertExecutionStatusErrorReasons, RuleExecutionState } from '../types';
|
||||
import {
|
||||
AlertExecutionStatusErrorReasons,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
RuleExecutionState,
|
||||
} from '../types';
|
||||
import {
|
||||
executionStatusFromState,
|
||||
executionStatusFromError,
|
||||
|
@ -14,6 +18,8 @@ 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 };
|
||||
|
@ -25,42 +31,59 @@ describe('RuleExecutionStatus', () => {
|
|||
|
||||
describe('executionStatusFromState()', () => {
|
||||
test('empty task state', () => {
|
||||
const status = executionStatusFromState({} as RuleExecutionState);
|
||||
const status = executionStatusFromState({
|
||||
alertExecutionStore: {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
},
|
||||
} 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: {},
|
||||
triggeredActions: [],
|
||||
alertExecutionStore: {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
},
|
||||
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: {} },
|
||||
triggeredActions: [],
|
||||
alertExecutionStore: {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
},
|
||||
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({
|
||||
triggeredActions: [{ group: '1' } as AlertAction],
|
||||
alertExecutionStore: {
|
||||
numberOfTriggeredActions: 1,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
},
|
||||
alertInstances: { a: {} },
|
||||
metrics,
|
||||
});
|
||||
|
@ -68,8 +91,27 @@ 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()', () => {
|
||||
|
@ -111,6 +153,7 @@ describe('RuleExecutionStatus', () => {
|
|||
"lastDuration": 0,
|
||||
"lastExecutionDate": "2020-09-03T16:26:58.000Z",
|
||||
"status": "ok",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -126,6 +169,7 @@ describe('RuleExecutionStatus', () => {
|
|||
"lastDuration": 0,
|
||||
"lastExecutionDate": "2020-09-03T16:26:58.000Z",
|
||||
"status": "ok",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -138,6 +182,7 @@ describe('RuleExecutionStatus', () => {
|
|||
"lastDuration": 1234,
|
||||
"lastExecutionDate": "2020-09-03T16:26:58.000Z",
|
||||
"status": "ok",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -151,6 +196,7 @@ describe('RuleExecutionStatus', () => {
|
|||
"lastDuration": 0,
|
||||
"lastExecutionDate": "2020-09-03T16:26:58.000Z",
|
||||
"status": "ok",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -184,6 +230,7 @@ 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,18 +6,43 @@
|
|||
*/
|
||||
|
||||
import { Logger } from 'src/core/server';
|
||||
import { AlertExecutionStatus, RawRuleExecutionStatus, RuleExecutionState } from '../types';
|
||||
import {
|
||||
AlertExecutionStatus,
|
||||
AlertExecutionStatusValues,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
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.triggeredActions?.length ?? 0,
|
||||
numberOfTriggeredActions: state.alertExecutionStore.numberOfTriggeredActions,
|
||||
lastExecutionDate: new Date(),
|
||||
status: alertIds.length === 0 ? 'ok' : 'active',
|
||||
status,
|
||||
...(hasIncompleteAlertExecution && {
|
||||
warning: {
|
||||
reason: AlertExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
message: translations.taskRunner.warning.maxExecutableActions,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,6 +62,7 @@ export function ruleExecutionStatusToRaw({
|
|||
lastDuration,
|
||||
status,
|
||||
error,
|
||||
warning,
|
||||
}: AlertExecutionStatus): RawRuleExecutionStatus {
|
||||
return {
|
||||
lastExecutionDate: lastExecutionDate.toISOString(),
|
||||
|
@ -44,6 +70,7 @@ export function ruleExecutionStatusToRaw({
|
|||
status,
|
||||
// explicitly setting to null (in case undefined) due to partial update concerns
|
||||
error: error ?? null,
|
||||
warning: warning ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -60,6 +87,7 @@ export function ruleExecutionStatusFromRaw(
|
|||
numberOfTriggeredActions,
|
||||
status = 'unknown',
|
||||
error,
|
||||
warning,
|
||||
} = rawRuleExecutionStatus;
|
||||
|
||||
let parsedDateMillis = lastExecutionDate ? Date.parse(lastExecutionDate) : Date.now();
|
||||
|
@ -87,6 +115,10 @@ export function ruleExecutionStatusFromRaw(
|
|||
executionStatus.error = error;
|
||||
}
|
||||
|
||||
if (warning) {
|
||||
executionStatus.warning = warning;
|
||||
}
|
||||
|
||||
return executionStatus;
|
||||
}
|
||||
|
||||
|
@ -94,4 +126,5 @@ export const getRuleExecutionStatusPending = (lastExecutionDate: string) => ({
|
|||
status: 'pending' as AlertExecutionStatuses,
|
||||
lastExecutionDate,
|
||||
error: null,
|
||||
warning: null,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertingPlugin, AlertingPluginsSetup, PluginSetupContract } from './plugin';
|
||||
import { AlertingPlugin, 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,42 +20,70 @@ 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>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 10,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
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, {
|
||||
licensing: licensingMock.createSetup(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsSetup,
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
eventLog: eventLogServiceMock.create(),
|
||||
actions: actionsMock.createSetup(),
|
||||
statusService: statusServiceMock.createSetupContract(),
|
||||
});
|
||||
await plugin.setup(setupMocks, mockPlugins);
|
||||
|
||||
expect(setupMocks.status.set).toHaveBeenCalledTimes(1);
|
||||
expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false);
|
||||
|
@ -65,93 +93,88 @@ describe('Alerting Plugin', () => {
|
|||
});
|
||||
|
||||
it('should create usage counter if usageCollection plugin is defined', async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 10,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
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, {
|
||||
licensing: licensingMock.createSetup(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsSetup,
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
eventLog: eventLogServiceMock.create(),
|
||||
actions: actionsMock.createSetup(),
|
||||
statusService: statusServiceMock.createSetupContract(),
|
||||
usageCollection: usageCollectionSetup,
|
||||
});
|
||||
await plugin.setup(setupMocks, { ...mockPlugins, usageCollection: usageCollectionSetup });
|
||||
|
||||
expect(usageCollectionSetup.createUsageCounter).toHaveBeenCalled();
|
||||
expect(usageCollectionSetup.registerCollector).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`exposes configured minimumScheduleInterval()`, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 100,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
plugin = new AlertingPlugin(context);
|
||||
|
||||
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(),
|
||||
});
|
||||
const setupContract = await plugin.setup(setupMocks, mockPlugins);
|
||||
|
||||
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 () => {
|
||||
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);
|
||||
setup = await plugin.setup(setupMocks, mockPlugins);
|
||||
});
|
||||
|
||||
it('should throw error when license type is invalid', async () => {
|
||||
|
@ -221,19 +244,9 @@ describe('Alerting Plugin', () => {
|
|||
describe('start()', () => {
|
||||
describe('getRulesClientWithRequest()', () => {
|
||||
it('throws error when encryptedSavedObjects plugin is missing encryption key', async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 10,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
|
||||
|
@ -264,19 +277,9 @@ describe('Alerting Plugin', () => {
|
|||
});
|
||||
|
||||
it(`doesn't throw error when encryptedSavedObjects plugin has encryption key`, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 10,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
const encryptedSavedObjectsSetup = {
|
||||
|
@ -321,19 +324,9 @@ describe('Alerting Plugin', () => {
|
|||
});
|
||||
|
||||
test(`exposes getAlertingAuthorizationWithRequest()`, async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '1h',
|
||||
},
|
||||
maxEphemeralActionsPerAlert: 100,
|
||||
defaultRuleTaskTimeout: '5m',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
});
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
const encryptedSavedObjectsSetup = {
|
||||
|
|
|
@ -62,6 +62,7 @@ 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 = {
|
||||
|
@ -262,7 +263,8 @@ export class AlertingPlugin {
|
|||
encryptedSavedObjects: plugins.encryptedSavedObjects,
|
||||
});
|
||||
|
||||
const alertingConfig = this.config;
|
||||
const alertingConfig: AlertingConfig = this.config;
|
||||
|
||||
return {
|
||||
registerType<
|
||||
Params extends AlertTypeParams = AlertTypeParams,
|
||||
|
@ -286,7 +288,10 @@ 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
|
@ -1314,6 +1314,7 @@ export class RulesClient {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: new Date().toISOString(),
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
});
|
||||
try {
|
||||
|
|
|
@ -86,6 +86,7 @@ describe('aggregate()', () => {
|
|||
{ key: 'ok', doc_count: 10 },
|
||||
{ key: 'pending', doc_count: 4 },
|
||||
{ key: 'unknown', doc_count: 2 },
|
||||
{ key: 'warning', doc_count: 1 },
|
||||
],
|
||||
},
|
||||
enabled: {
|
||||
|
@ -135,6 +136,7 @@ describe('aggregate()', () => {
|
|||
"ok": 10,
|
||||
"pending": 4,
|
||||
"unknown": 2,
|
||||
"warning": 1,
|
||||
},
|
||||
"ruleEnabledStatus": Object {
|
||||
"disabled": 2,
|
||||
|
|
|
@ -404,6 +404,7 @@ describe('create()', () => {
|
|||
"error": null,
|
||||
"lastExecutionDate": "2019-02-12T21:01:22.479Z",
|
||||
"status": "pending",
|
||||
"warning": null,
|
||||
},
|
||||
"legacyId": null,
|
||||
"meta": Object {
|
||||
|
@ -608,6 +609,7 @@ describe('create()', () => {
|
|||
"error": null,
|
||||
"lastExecutionDate": "2019-02-12T21:01:22.479Z",
|
||||
"status": "pending",
|
||||
"warning": null,
|
||||
},
|
||||
"legacyId": "123",
|
||||
"meta": Object {
|
||||
|
@ -1035,6 +1037,7 @@ describe('create()', () => {
|
|||
error: null,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
meta: { versionApiKeyLastmodified: kibanaVersion },
|
||||
|
@ -1162,6 +1165,11 @@ describe('create()', () => {
|
|||
extractReferences: extractReferencesFn,
|
||||
injectReferences: injectReferencesFn,
|
||||
},
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
const data = getMockData({
|
||||
params: ruleParams,
|
||||
|
@ -1233,6 +1241,7 @@ describe('create()', () => {
|
|||
error: null,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
meta: { versionApiKeyLastmodified: kibanaVersion },
|
||||
|
@ -1329,6 +1338,11 @@ describe('create()', () => {
|
|||
extractReferences: extractReferencesFn,
|
||||
injectReferences: injectReferencesFn,
|
||||
},
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
const data = getMockData({
|
||||
params: ruleParams,
|
||||
|
@ -1400,6 +1414,7 @@ describe('create()', () => {
|
|||
error: null,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
meta: { versionApiKeyLastmodified: kibanaVersion },
|
||||
|
@ -1577,6 +1592,7 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -1708,6 +1724,7 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -1839,6 +1856,7 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -1985,6 +2003,7 @@ describe('create()', () => {
|
|||
status: 'pending',
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: {
|
||||
execution: {
|
||||
|
@ -2078,6 +2097,11 @@ 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]"`
|
||||
|
@ -2355,6 +2379,7 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -2456,6 +2481,7 @@ describe('create()', () => {
|
|||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
monitoring: getDefaultRuleMonitoring(),
|
||||
},
|
||||
|
@ -2535,6 +2561,11 @@ describe('create()', () => {
|
|||
extractReferences: jest.fn(),
|
||||
injectReferences: jest.fn(),
|
||||
},
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const data = getMockData({ schedule: { interval: '1s' } });
|
||||
|
|
|
@ -242,6 +242,7 @@ describe('enable()', () => {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -353,6 +354,7 @@ describe('enable()', () => {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -521,6 +523,7 @@ describe('enable()', () => {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -92,6 +92,7 @@ const BaseRuleSavedObject: SavedObject<RawRule> = {
|
|||
status: 'unknown',
|
||||
lastExecutionDate: '2020-08-20T19:23:38Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
|
|
|
@ -91,6 +91,11 @@ 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,6 +163,16 @@
|
|||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"message": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
SavedObjectAttribute,
|
||||
SavedObjectReference,
|
||||
} from '../../../../../src/core/server';
|
||||
import { RawRule, RawAlertAction } from '../types';
|
||||
import { RawRule, RawAlertAction, RawRuleExecutionStatus } 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,11 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createExecutionHandler, CreateExecutionHandlerOptions } from './create_execution_handler';
|
||||
import { createExecutionHandler } from './create_execution_handler';
|
||||
import { ActionsCompletion, AlertExecutionStore, CreateExecutionHandlerOptions } from './types';
|
||||
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import {
|
||||
actionsMock,
|
||||
actionsClientMock,
|
||||
actionsMock,
|
||||
renderActionParameterTemplatesDefault,
|
||||
} from '../../../actions/server/mocks';
|
||||
import { eventLoggerMock } from '../../../event_log/server/event_logger.mock';
|
||||
|
@ -18,10 +19,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', () => ({
|
||||
|
@ -52,6 +53,11 @@ const ruleType: NormalizedRuleType<
|
|||
},
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
|
@ -102,6 +108,7 @@ const createExecutionHandlerParams: jest.Mocked<
|
|||
supportsEphemeralTasks: false,
|
||||
maxEphemeralActionsPerRule: 10,
|
||||
};
|
||||
let alertExecutionStore: AlertExecutionStore;
|
||||
|
||||
describe('Create Execution Handler', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -117,17 +124,22 @@ 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);
|
||||
const result = await executionHandler({
|
||||
await executionHandler({
|
||||
actionGroup: 'default',
|
||||
state: {},
|
||||
context: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(1);
|
||||
expect(mockActionsPlugin.getActionsClientWithRequest).toHaveBeenCalledWith(
|
||||
createExecutionHandlerParams.request
|
||||
);
|
||||
|
@ -228,6 +240,8 @@ describe('Create Execution Handler', () => {
|
|||
stateVal: 'My goes here',
|
||||
},
|
||||
});
|
||||
|
||||
expect(alertExecutionStore.triggeredActionsStatus).toBe(ActionsCompletion.COMPLETE);
|
||||
});
|
||||
|
||||
test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => {
|
||||
|
@ -256,7 +270,9 @@ 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',
|
||||
|
@ -304,13 +320,14 @@ describe('Create Execution Handler', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const result = await executionHandler({
|
||||
await executionHandler({
|
||||
actionGroup: 'default',
|
||||
state: {},
|
||||
context: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(0);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0);
|
||||
|
||||
mockActionsPlugin.isActionExecutable.mockImplementation(() => true);
|
||||
|
@ -323,31 +340,34 @@ 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);
|
||||
const result = await executionHandler({
|
||||
await executionHandler({
|
||||
actionGroup: 'other-group',
|
||||
state: {},
|
||||
context: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(0);
|
||||
expect(actionsClient.enqueueExecution).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('context attribute gets parameterized', async () => {
|
||||
const executionHandler = createExecutionHandler(createExecutionHandlerParams);
|
||||
const result = await executionHandler({
|
||||
await executionHandler({
|
||||
actionGroup: 'default',
|
||||
context: { value: 'context-val' },
|
||||
state: {},
|
||||
alertId: '2',
|
||||
alertExecutionStore,
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(alertExecutionStore.numberOfTriggeredActions).toBe(1);
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -384,13 +404,13 @@ describe('Create Execution Handler', () => {
|
|||
|
||||
test('state attribute gets parameterized', async () => {
|
||||
const executionHandler = createExecutionHandler(createExecutionHandlerParams);
|
||||
const result = await executionHandler({
|
||||
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 [
|
||||
|
@ -427,17 +447,74 @@ 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);
|
||||
const result = await executionHandler({
|
||||
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,73 +4,26 @@
|
|||
* 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,
|
||||
PluginStartContract as ActionsPluginStartContract,
|
||||
} from '../../../actions/server';
|
||||
import { IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
|
||||
import { asSavedObjectExecutionSource } from '../../../actions/server';
|
||||
import { SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
|
||||
import { EVENT_LOG_ACTIONS } from '../plugin';
|
||||
import { injectActionParams } from './inject_action_params';
|
||||
import {
|
||||
AlertAction,
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
RawRule,
|
||||
} from '../types';
|
||||
import { NormalizedRuleType, UntypedNormalizedRuleType } from '../rule_type_registry';
|
||||
|
||||
import { UntypedNormalizedRuleType } from '../rule_type_registry';
|
||||
import { isEphemeralTaskRejectedDueToCapacityError } from '../../../task_manager/server';
|
||||
import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object';
|
||||
|
||||
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;
|
||||
}
|
||||
import { ActionsCompletion, CreateExecutionHandlerOptions, ExecutionHandlerOptions } from './types';
|
||||
|
||||
export type ExecutionHandler<ActionGroupIds extends string> = (
|
||||
options: ExecutionHandlerOptions<ActionGroupIds>
|
||||
) => Promise<AlertAction[]>;
|
||||
) => Promise<void>;
|
||||
|
||||
export function createExecutionHandler<
|
||||
Params extends AlertTypeParams,
|
||||
|
@ -114,13 +67,14 @@ 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 triggeredActions;
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = ruleActions
|
||||
.filter(({ group }) => group === actionGroup)
|
||||
.map((action) => {
|
||||
|
@ -163,6 +117,11 @@ 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 })
|
||||
) {
|
||||
|
@ -205,11 +164,11 @@ export function createExecutionHandler<
|
|||
await actionsClient.enqueueExecution(enqueueOptions);
|
||||
}
|
||||
} finally {
|
||||
triggeredActions.push(action);
|
||||
alertExecutionStore.numberOfTriggeredActions++;
|
||||
}
|
||||
} else {
|
||||
await actionsClient.enqueueExecution(enqueueOptions);
|
||||
triggeredActions.push(action);
|
||||
alertExecutionStore.numberOfTriggeredActions++;
|
||||
}
|
||||
|
||||
const event = createAlertEventLogRecordObject({
|
||||
|
@ -244,6 +203,5 @@ export function createExecutionHandler<
|
|||
|
||||
eventLogger.logEvent(event);
|
||||
}
|
||||
return triggeredActions;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -53,7 +53,15 @@ export const RULE_ACTIONS = [
|
|||
},
|
||||
];
|
||||
|
||||
export const SAVED_OBJECT_UPDATE_PARAMS = [
|
||||
export const generateSavedObjectParams = ({
|
||||
error = null,
|
||||
warning = null,
|
||||
status = 'ok',
|
||||
}: {
|
||||
error?: null | { reason: string; message: string };
|
||||
warning?: null | { reason: string; message: string };
|
||||
status?: string;
|
||||
}) => [
|
||||
'alert',
|
||||
'1',
|
||||
{
|
||||
|
@ -71,10 +79,11 @@ export const SAVED_OBJECT_UPDATE_PARAMS = [
|
|||
},
|
||||
},
|
||||
executionStatus: {
|
||||
error: null,
|
||||
error,
|
||||
lastDuration: 0,
|
||||
lastExecutionDate: '1970-01-01T00:00:00.000Z',
|
||||
status: 'ok',
|
||||
status,
|
||||
warning,
|
||||
},
|
||||
},
|
||||
{ refresh: false, namespace: undefined },
|
||||
|
@ -92,6 +101,11 @@ export const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockRunNowResponse = {
|
||||
|
@ -189,6 +203,7 @@ export const generateEventLog = ({
|
|||
instanceId,
|
||||
actionSubgroup,
|
||||
actionGroupId,
|
||||
actionId,
|
||||
status,
|
||||
numberOfTriggeredActions,
|
||||
savedObjects = [generateAlertSO('1')],
|
||||
|
@ -236,11 +251,19 @@ export const generateEventLog = ({
|
|||
...(task && {
|
||||
task: {
|
||||
schedule_delay: 0,
|
||||
scheduled: '1970-01-01T00:00:00.000Z',
|
||||
scheduled: DATE_1970,
|
||||
},
|
||||
}),
|
||||
},
|
||||
message: generateMessage({ action, instanceId, actionGroupId, actionSubgroup, reason, status }),
|
||||
message: generateMessage({
|
||||
action,
|
||||
instanceId,
|
||||
actionGroupId,
|
||||
actionSubgroup,
|
||||
reason,
|
||||
status,
|
||||
actionId,
|
||||
}),
|
||||
rule: {
|
||||
category: 'test',
|
||||
id: '1',
|
||||
|
@ -255,6 +278,7 @@ const generateMessage = ({
|
|||
instanceId,
|
||||
actionGroupId,
|
||||
actionSubgroup,
|
||||
actionId,
|
||||
reason,
|
||||
status,
|
||||
}: GeneratorParams) => {
|
||||
|
@ -279,9 +303,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:${instanceId}`;
|
||||
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: '${actionGroupId}' action: action:${instanceId}`;
|
||||
return `alert: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${actionId}`;
|
||||
}
|
||||
|
||||
if (action === EVENT_LOG_ACTIONS.execute) {
|
||||
|
@ -292,7 +316,7 @@ const generateMessage = ({
|
|||
return `${RULE_TYPE_ID}:${RULE_ID}: execution failed`;
|
||||
}
|
||||
if (actionGroupId === 'recovered') {
|
||||
return `rule-name' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${instanceId}`;
|
||||
return `rule-name' instanceId: '${instanceId}' scheduled actionGroup: '${actionGroupId}' action: action:${actionId}`;
|
||||
}
|
||||
return `rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`;
|
||||
}
|
||||
|
@ -310,6 +334,7 @@ export const generateRunnerResult = ({
|
|||
history = Array(false),
|
||||
state = false,
|
||||
interval = '10s',
|
||||
alertInstances = {},
|
||||
}: GeneratorParams = {}) => {
|
||||
return {
|
||||
monitoring: {
|
||||
|
@ -325,7 +350,7 @@ export const generateRunnerResult = ({
|
|||
interval,
|
||||
},
|
||||
state: {
|
||||
...(state && { alertInstances: {} }),
|
||||
...(state && { alertInstances }),
|
||||
...(state && { alertTypeState: undefined }),
|
||||
...(state && { previousStartedAt: new Date('1970-01-01T00:00:00.000Z') }),
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
} from '../types';
|
||||
import {
|
||||
ConcreteTaskInstance,
|
||||
|
@ -55,7 +56,7 @@ import {
|
|||
generateRunnerResult,
|
||||
RULE_ACTIONS,
|
||||
generateEnqueueFunctionInput,
|
||||
SAVED_OBJECT_UPDATE_PARAMS,
|
||||
generateSavedObjectParams,
|
||||
mockTaskInstance,
|
||||
GENERIC_ERROR_MESSAGE,
|
||||
generateAlertInstance,
|
||||
|
@ -65,6 +66,7 @@ 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',
|
||||
|
@ -240,7 +242,7 @@ describe('Task Runner', () => {
|
|||
|
||||
expect(
|
||||
taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update
|
||||
).toHaveBeenCalledWith(...SAVED_OBJECT_UPDATE_PARAMS);
|
||||
).toHaveBeenCalledWith(...generateSavedObjectParams({}));
|
||||
|
||||
expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1);
|
||||
expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toHaveBeenCalledWith(
|
||||
|
@ -345,6 +347,7 @@ describe('Task Runner', () => {
|
|||
instanceId: '1',
|
||||
actionSubgroup: 'subDefault',
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('1')],
|
||||
actionId: '1',
|
||||
})
|
||||
);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
|
@ -916,6 +919,7 @@ describe('Task Runner', () => {
|
|||
action: EVENT_LOG_ACTIONS.executeAction,
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
actionId: '1',
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('1')],
|
||||
})
|
||||
);
|
||||
|
@ -1043,9 +1047,10 @@ describe('Task Runner', () => {
|
|||
4,
|
||||
generateEventLog({
|
||||
action: EVENT_LOG_ACTIONS.executeAction,
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('2')],
|
||||
actionGroupId: 'recovered',
|
||||
instanceId: '2',
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('1')],
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
actionId: '1',
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -1053,9 +1058,10 @@ describe('Task Runner', () => {
|
|||
5,
|
||||
generateEventLog({
|
||||
action: EVENT_LOG_ACTIONS.executeAction,
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('1')],
|
||||
actionGroupId: 'default',
|
||||
instanceId: '1',
|
||||
savedObjects: [generateAlertSO('1'), generateActionSO('2')],
|
||||
actionGroupId: 'recovered',
|
||||
instanceId: '2',
|
||||
actionId: '2',
|
||||
})
|
||||
);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(
|
||||
|
@ -1142,8 +1148,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('1');
|
||||
expect((enqueueFunction as jest.Mock).mock.calls[0][0].id).toEqual('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(mockUsageCounter.incrementCounter).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
@ -2315,7 +2321,7 @@ describe('Task Runner', () => {
|
|||
);
|
||||
expect(
|
||||
taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update
|
||||
).toHaveBeenCalledWith(...SAVED_OBJECT_UPDATE_PARAMS);
|
||||
).toHaveBeenCalledWith(...generateSavedObjectParams({}));
|
||||
expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -2449,4 +2455,179 @@ 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,56 +5,57 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import apm from 'elastic-apm-node';
|
||||
import { pickBy, mapValues, without, cloneDeep, concat, set, omit } from 'lodash';
|
||||
import { cloneDeep, mapValues, omit, pickBy, set, without } 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 { Logger, KibanaRequest } from '../../../../../src/core/server';
|
||||
import { KibanaRequest, Logger } 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 {
|
||||
validateRuleTypeParams,
|
||||
executionStatusFromState,
|
||||
executionStatusFromError,
|
||||
ruleExecutionStatusToRaw,
|
||||
ErrorWithReason,
|
||||
createWrappedScopedClusterClientFactory,
|
||||
ElasticsearchError,
|
||||
ErrorWithReason,
|
||||
executionStatusFromError,
|
||||
executionStatusFromState,
|
||||
getRecoveredAlerts,
|
||||
ruleExecutionStatusToRaw,
|
||||
validateRuleTypeParams,
|
||||
} from '../lib';
|
||||
import {
|
||||
RawRule,
|
||||
IntervalSchedule,
|
||||
RawAlertInstance,
|
||||
RuleTaskState,
|
||||
Alert,
|
||||
SanitizedAlert,
|
||||
AlertExecutionStatus,
|
||||
AlertExecutionStatusErrorReasons,
|
||||
RuleTypeRegistry,
|
||||
IntervalSchedule,
|
||||
RawAlertInstance,
|
||||
RawRule,
|
||||
RawRuleExecutionStatus,
|
||||
RuleExecutionRunResult,
|
||||
RuleExecutionState,
|
||||
RuleMonitoring,
|
||||
RuleMonitoringHistory,
|
||||
RawRuleExecutionStatus,
|
||||
AlertAction,
|
||||
RuleExecutionState,
|
||||
RuleExecutionRunResult,
|
||||
RuleTaskState,
|
||||
RuleTypeRegistry,
|
||||
SanitizedAlert,
|
||||
} from '../types';
|
||||
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
|
||||
import { getExecutionSuccessRatio, getExecutionDurationPercentiles } from '../lib/monitoring';
|
||||
import { asErr, asOk, map, promiseResult, resolveErr, Resultable } from '../lib/result_type';
|
||||
import { getExecutionDurationPercentiles, getExecutionSuccessRatio } 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,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
WithoutReservedActionGroups,
|
||||
parseDuration,
|
||||
MONITORING_HISTORY_LIMIT,
|
||||
parseDuration,
|
||||
WithoutReservedActionGroups,
|
||||
} from '../../common';
|
||||
import { NormalizedRuleType, UntypedNormalizedRuleType } from '../rule_type_registry';
|
||||
import { getEsErrorMessage } from '../lib/errors';
|
||||
|
@ -62,9 +63,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,
|
||||
|
@ -269,7 +270,8 @@ export class TaskRunner<
|
|||
private async executeAlert(
|
||||
alertId: string,
|
||||
alert: CreatedAlert<InstanceState, InstanceContext>,
|
||||
executionHandler: ExecutionHandler<ActionGroupIds | RecoveryActionGroupId>
|
||||
executionHandler: ExecutionHandler<ActionGroupIds | RecoveryActionGroupId>,
|
||||
alertExecutionStore: AlertExecutionStore
|
||||
) {
|
||||
const {
|
||||
actionGroup,
|
||||
|
@ -279,7 +281,14 @@ export class TaskRunner<
|
|||
} = alert.getScheduledActionOptions()!;
|
||||
alert.updateLastScheduledActions(actionGroup, actionSubgroup);
|
||||
alert.unscheduleActions();
|
||||
return executionHandler({ actionGroup, actionSubgroup, context, state, alertId });
|
||||
return executionHandler({
|
||||
actionGroup,
|
||||
actionSubgroup,
|
||||
context,
|
||||
state,
|
||||
alertId,
|
||||
alertExecutionStore,
|
||||
});
|
||||
}
|
||||
|
||||
private async executeAlerts(
|
||||
|
@ -462,26 +471,15 @@ export class TaskRunner<
|
|||
});
|
||||
}
|
||||
|
||||
let triggeredActions: AlertAction[] = [];
|
||||
const alertExecutionStore: AlertExecutionStore = {
|
||||
numberOfTriggeredActions: 0,
|
||||
triggeredActionsStatus: ActionsCompletion.COMPLETE,
|
||||
};
|
||||
|
||||
if (!muteAll && this.shouldLogAndScheduleActionsForAlerts()) {
|
||||
const mutedAlertIdsSet = new Set(mutedInstanceIds);
|
||||
|
||||
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(
|
||||
const alertsWithExecutableActions = Object.entries(alertsWithScheduledActions).filter(
|
||||
([alertName, alert]: [string, CreatedAlert<InstanceState, InstanceContext>]) => {
|
||||
const throttled = alert.isThrottled(throttle);
|
||||
const muted = mutedAlertIdsSet.has(alertName);
|
||||
|
@ -508,14 +506,26 @@ export class TaskRunner<
|
|||
}
|
||||
);
|
||||
|
||||
const allTriggeredActions = await Promise.all(
|
||||
alertsToExecute.map(
|
||||
await Promise.all(
|
||||
alertsWithExecutableActions.map(
|
||||
([alertId, alert]: [string, CreatedAlert<InstanceState, InstanceContext>]) =>
|
||||
this.executeAlert(alertId, alert, executionHandler)
|
||||
this.executeAlert(alertId, alert, executionHandler, alertExecutionStore)
|
||||
)
|
||||
);
|
||||
|
||||
triggeredActions = concat(triggeredActions, ...allTriggeredActions);
|
||||
await scheduleActionsForRecoveredAlerts<
|
||||
InstanceState,
|
||||
InstanceContext,
|
||||
RecoveryActionGroupId
|
||||
>({
|
||||
recoveryActionGroup: this.ruleType.recoveryActionGroup,
|
||||
recoveredAlerts,
|
||||
executionHandler,
|
||||
mutedAlertIdsSet,
|
||||
logger: this.logger,
|
||||
ruleLabel,
|
||||
alertExecutionStore,
|
||||
});
|
||||
} else {
|
||||
if (muteAll) {
|
||||
this.logger.debug(`no scheduling of actions for rule ${ruleLabel}: rule is muted.`);
|
||||
|
@ -535,7 +545,7 @@ export class TaskRunner<
|
|||
|
||||
return {
|
||||
metrics: searchMetrics,
|
||||
triggeredActions,
|
||||
alertExecutionStore,
|
||||
alertTypeState: updatedRuleTypeState || undefined,
|
||||
alertInstances: mapValues<
|
||||
Record<string, CreatedAlert<InstanceState, InstanceContext>>,
|
||||
|
@ -639,7 +649,7 @@ export class TaskRunner<
|
|||
),
|
||||
schedule: asOk(
|
||||
// fetch the rule again to ensure we return the correct schedule as it may have
|
||||
// cahnged during the task execution
|
||||
// changed during the task execution
|
||||
(await rulesClient.get({ id: ruleId })).schedule
|
||||
),
|
||||
};
|
||||
|
@ -714,7 +724,6 @@ 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);
|
||||
|
@ -757,6 +766,10 @@ 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',
|
||||
|
@ -807,7 +820,7 @@ export class TaskRunner<
|
|||
executionState: RuleExecutionState
|
||||
): RuleTaskState => {
|
||||
return {
|
||||
...omit(executionState, ['triggeredActions', 'metrics']),
|
||||
...omit(executionState, ['alertExecutionStore', 'metrics']),
|
||||
previousStartedAt: startedAt,
|
||||
};
|
||||
};
|
||||
|
@ -1110,7 +1123,7 @@ async function scheduleActionsForRecoveredAlerts<
|
|||
InstanceContext,
|
||||
RecoveryActionGroupId
|
||||
>
|
||||
): Promise<AlertAction[]> {
|
||||
): Promise<void> {
|
||||
const {
|
||||
logger,
|
||||
recoveryActionGroup,
|
||||
|
@ -1118,9 +1131,10 @@ 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(
|
||||
|
@ -1130,17 +1144,16 @@ async function scheduleActionsForRecoveredAlerts<
|
|||
const alert = recoveredAlerts[id];
|
||||
alert.updateLastScheduledActions(recoveryActionGroup.id);
|
||||
alert.unscheduleActions();
|
||||
const triggeredActionsForRecoveredAlert = await executionHandler({
|
||||
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,6 +55,11 @@ const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
config: {
|
||||
execution: {
|
||||
actions: { max: 1000 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let fakeTimer: sinon.SinonFakeTimers;
|
||||
|
@ -362,6 +367,7 @@ describe('Task Runner Cancel', () => {
|
|||
lastDuration: 0,
|
||||
lastExecutionDate: '1970-01-01T00:00:00.000Z',
|
||||
status: 'error',
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
{ refresh: false, namespace: undefined }
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import { Dictionary } from 'lodash';
|
||||
import { Logger } from 'kibana/server';
|
||||
import { KibanaRequest, Logger } from 'kibana/server';
|
||||
import {
|
||||
ActionGroup,
|
||||
AlertAction,
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
AlertTypeParams,
|
||||
|
@ -24,6 +25,8 @@ 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;
|
||||
|
@ -89,6 +92,7 @@ export interface ScheduleActionsForRecoveredAlertsParams<
|
|||
executionHandler: ExecutionHandler<RecoveryActionGroupId | RecoveryActionGroupId>;
|
||||
mutedAlertIdsSet: Set<string>;
|
||||
ruleLabel: string;
|
||||
alertExecutionStore: AlertExecutionStore;
|
||||
}
|
||||
|
||||
export interface LogActiveAndRecoveredAlertsParams<
|
||||
|
@ -103,3 +107,59 @@ 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,9 +39,10 @@ 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;
|
||||
|
||||
|
@ -124,6 +125,7 @@ export type ExecutorType<
|
|||
export interface AlertTypeParamsValidator<Params extends AlertTypeParams> {
|
||||
validate: (object: unknown) => Params;
|
||||
}
|
||||
|
||||
export interface RuleType<
|
||||
Params extends AlertTypeParams = never,
|
||||
ExtractedParams extends AlertTypeParams = never,
|
||||
|
@ -168,6 +170,7 @@ export interface RuleType<
|
|||
ruleTaskTimeout?: string;
|
||||
cancelAlertsOnRuleTimeout?: boolean;
|
||||
doesSetRecoveryContext?: boolean;
|
||||
config?: RulesConfig;
|
||||
}
|
||||
export type UntypedRuleType = RuleType<
|
||||
AlertTypeParams,
|
||||
|
@ -199,6 +202,10 @@ 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'> &
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
RULE_STATUS_ERROR,
|
||||
RULE_STATUS_PENDING,
|
||||
RULE_STATUS_UNKNOWN,
|
||||
RULE_STATUS_WARNING,
|
||||
} from './translations';
|
||||
import { AlertExecutionStatuses } from '../../../../alerting/common';
|
||||
import { Rule, RuleTypeIndex, RuleType } from '../../../../triggers_actions_ui/public';
|
||||
|
@ -50,6 +51,7 @@ export const rulesStatusesTranslationsMapping = {
|
|||
error: RULE_STATUS_ERROR,
|
||||
pending: RULE_STATUS_PENDING,
|
||||
unknown: RULE_STATUS_UNKNOWN,
|
||||
warning: RULE_STATUS_WARNING,
|
||||
};
|
||||
|
||||
export const OBSERVABILITY_RULE_TYPES = [
|
||||
|
|
|
@ -46,6 +46,13 @@ export const RULE_STATUS_UNKNOWN = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const RULE_STATUS_WARNING = i18n.translate(
|
||||
'xpack.observability.rules.rulesTable.ruleStatusWarning',
|
||||
{
|
||||
defaultMessage: 'warning',
|
||||
}
|
||||
);
|
||||
|
||||
export const LAST_RESPONSE_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.observability.rules.rulesTable.columns.lastResponseTitle',
|
||||
{
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
import {
|
||||
ActionGroup,
|
||||
AlertExecutionStatusErrorReasons,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
ALERTS_FEATURE_ID,
|
||||
} from '../../../../../../alerting/common';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -119,6 +120,28 @@ 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,7 +40,10 @@ import { RuleRouteWithApi } from './rule_route';
|
|||
import { ViewInApp } from './view_in_app';
|
||||
import { RuleEdit } from '../../rule_form';
|
||||
import { routeToRuleDetails } from '../../../constants';
|
||||
import { rulesErrorReasonTranslationsMapping } from '../../rules_list/translations';
|
||||
import {
|
||||
rulesErrorReasonTranslationsMapping,
|
||||
rulesWarningReasonTranslationsMapping,
|
||||
} 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';
|
||||
|
@ -135,7 +138,8 @@ 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 [dissmissRuleErrors, setDissmissRuleErrors] = useState<boolean>(false);
|
||||
const [dismissRuleErrors, setDismissRuleErrors] = useState<boolean>(false);
|
||||
const [dismissRuleWarning, setDismissRuleWarning] = useState<boolean>(false);
|
||||
|
||||
const setRule = async () => {
|
||||
history.push(routeToRuleDetails.replace(`:ruleId`, rule.id));
|
||||
|
@ -149,6 +153,14 @@ 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
|
||||
? [
|
||||
<>
|
||||
|
@ -294,7 +306,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
setIsEnabled(false);
|
||||
await disableRule(rule);
|
||||
// Reset dismiss if previously clicked
|
||||
setDissmissRuleErrors(false);
|
||||
setDismissRuleErrors(false);
|
||||
} else {
|
||||
setIsEnabled(true);
|
||||
await enableRule(rule);
|
||||
|
@ -357,7 +369,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{rule.enabled && !dissmissRuleErrors && rule.executionStatus.status === 'error' ? (
|
||||
{rule.enabled && !dismissRuleErrors && rule.executionStatus.status === 'error' ? (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut
|
||||
|
@ -376,7 +388,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
<EuiButton
|
||||
data-test-subj="dismiss-execution-error"
|
||||
color="danger"
|
||||
onClick={() => setDissmissRuleErrors(true)}
|
||||
onClick={() => setDismissRuleErrors(true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.dismissButtonTitle"
|
||||
|
@ -404,6 +416,39 @@ 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,6 +102,8 @@ export function getHealthColor(status: AlertExecutionStatuses) {
|
|||
return 'primary';
|
||||
case 'pending':
|
||||
return 'accent';
|
||||
case 'warning':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'subdued';
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { RulesList, percentileFields } from './rules_list';
|
|||
import { RuleTypeModel, ValidationResult, Percentiles } from '../../../../types';
|
||||
import {
|
||||
AlertExecutionStatusErrorReasons,
|
||||
AlertExecutionStatusWarningReasons,
|
||||
ALERTS_FEATURE_ID,
|
||||
parseDuration,
|
||||
} from '../../../../../../alerting/common';
|
||||
|
@ -30,6 +31,7 @@ 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,
|
||||
|
@ -55,7 +57,8 @@ jest.mock('../../../lib/capabilities', () => ({
|
|||
hasShowActionsCapability: jest.fn(() => true),
|
||||
hasExecuteActionsCapability: jest.fn(() => true),
|
||||
}));
|
||||
const { loadRules, loadRuleTypes } = jest.requireMock('../../../lib/rule_api');
|
||||
const { loadRules, loadRuleTypes, loadRuleAggregations } =
|
||||
jest.requireMock('../../../lib/rule_api');
|
||||
const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api');
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
@ -331,6 +334,32 @@ 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) {
|
||||
|
@ -352,6 +381,11 @@ 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',
|
||||
|
@ -381,6 +415,7 @@ describe('rules_list component with items', () => {
|
|||
|
||||
expect(loadRules).toHaveBeenCalled();
|
||||
expect(loadActionTypes).toHaveBeenCalled();
|
||||
expect(loadRuleAggregations).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
it('renders table of rules', async () => {
|
||||
|
@ -471,6 +506,7 @@ 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
|
||||
|
@ -728,6 +764,28 @@ 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', () => {
|
||||
|
@ -893,7 +951,7 @@ describe('rules_list with show only capability', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('rules_list with disabled itmes', () => {
|
||||
describe('rules_list with disabled items', () => {
|
||||
let wrapper: ReactWrapper<any>;
|
||||
|
||||
async function setup() {
|
||||
|
|
|
@ -986,6 +986,17 @@ 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,12 +49,20 @@ 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(
|
||||
|
@ -106,6 +114,20 @@ 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,
|
||||
|
@ -115,3 +137,8 @@ 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,6 +36,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
|
|||
error: 0,
|
||||
pending: 0,
|
||||
unknown: 0,
|
||||
warning: 0,
|
||||
},
|
||||
rule_muted_status: {
|
||||
muted: 0,
|
||||
|
@ -111,6 +112,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
|
|||
error: NumErrorAlerts,
|
||||
pending: 0,
|
||||
unknown: 0,
|
||||
warning: 0,
|
||||
},
|
||||
rule_muted_status: {
|
||||
muted: 0,
|
||||
|
@ -183,6 +185,7 @@ 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