[Alerting] Warn by default when rule schedule interval is set below the minimum configured. (#127498)

* Changing structure of minimumScheduleInterval config

* Updating rules client logic to follow enforce flag

* Updating UI to use enforce value

* Updating config key in functional tests

* Fixes

* Fixes

* Updating help text

* Wording suggestsion from PR review

* Log warning instead of throwing an error if rule has default interval less than minimum

* Updating default interval to be minimum if minimum is greater than hardcoded default

* Fixing checks

* Fixing tests

* Fixing tests

* Fixing config

* Fixing checks

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ying Mao 2022-03-21 18:55:30 -04:00 committed by GitHub
parent bdecf1568e
commit 0b1425b974
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 586 additions and 154 deletions

View file

@ -195,12 +195,15 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `5m`.
`xpack.alerting.cancelAlertsOnRuleTimeout`::
Specifies whether to skip writing alerts and scheduling actions if rule execution is cancelled due to timeout. Default: `true`. This setting can be overridden by individual rule types.
`xpack.alerting.minimumScheduleInterval`::
Specifies the minimum interval allowed for the all rules. This minimum is enforced for all rules created or updated after the introduction of this setting. The time is formatted as:
`xpack.alerting.rules.minimumScheduleInterval.value`::
Specifies the minimum schedule interval for rules. This minimum is applied to all rules created or updated after you set this value. The time is formatted as:
+
`<count>[s,m,h,d]`
+
For example, `20m`, `24h`, `7d`. Default: `1m`.
`xpack.alerting.rules.execution.actions.max`
Specifies the maximum number of actions that a rule can trigger each time detection checks run.
`xpack.alerting.rules.minimumScheduleInterval.enforce`::
Specifies the behavior when a new or changed rule has a schedule interval less than the value defined in `xpack.alerting.rules.minimumScheduleInterval.value`. If `false`, rules with schedules less than the interval will be created but warnings will be logged. If `true`, rules with schedules less than the interval cannot be created. Default: `false`.
`xpack.alerting.rules.execution.actions.max`::
Specifies the maximum number of actions that a rule can trigger each time detection checks run.

View file

@ -200,6 +200,8 @@ kibana_vars=(
xpack.alerting.invalidateApiKeysTask.removalDelay
xpack.alerting.defaultRuleTaskTimeout
xpack.alerting.cancelAlertsOnRuleTimeout
xpack.alerting.rules.minimumScheduleInterval.value
xpack.alerting.rules.minimumScheduleInterval.enforce
xpack.alerting.rules.execution.actions.max
xpack.alerts.healthCheck.interval
xpack.alerts.invalidateApiKeysTask.interval

View file

@ -22,13 +22,16 @@ describe('config validation', () => {
"removalDelay": "1h",
},
"maxEphemeralActionsPerAlert": 10,
"minimumScheduleInterval": "1m",
"rules": Object {
"execution": Object {
"actions": Object {
"max": 100000,
},
},
"minimumScheduleInterval": Object {
"enforce": false,
"value": "1m",
},
},
}
`);

View file

@ -14,6 +14,13 @@ const ruleTypeSchema = schema.object({
});
const rulesSchema = schema.object({
minimumScheduleInterval: schema.object({
value: schema.string({
validate: validateDurationSchema,
defaultValue: '1m',
}),
enforce: schema.boolean({ defaultValue: false }), // if enforce is false, only warnings will be shown
}),
execution: schema.object({
timeout: schema.maybe(schema.string({ validate: validateDurationSchema })),
actions: schema.object({
@ -37,11 +44,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'>;
export type RuleTypeConfig = Omit<RulesConfig, 'ruleTypeOverrides' | 'minimumScheduleInterval'>;
export type AlertingRulesConfig = Pick<AlertingConfig['rules'], 'minimumScheduleInterval'>;

View file

@ -35,7 +35,7 @@ export type { FindResult } from './rules_client';
export type { PublicAlert as Alert } from './alert';
export { parseDuration } from './lib';
export { getEsErrorMessage } from './lib/errors';
export type { PublicAlertingConfig } from './config';
export type { AlertingRulesConfig } from './config';
export {
ReadOperations,
AlertingAuthorizationFilterType,

View file

@ -5,11 +5,15 @@
* 2.0.
*/
import { getRulesConfig } from './get_rules_config';
import { getExecutionConfigForRuleType } from './get_rules_config';
import { RulesConfig } from '../config';
const ruleTypeId = 'test-rule-type-id';
const config = {
minimumScheduleInterval: {
value: '2m',
enforce: false,
},
execution: {
timeout: '1m',
actions: { max: 1000 },
@ -17,6 +21,7 @@ const config = {
} as RulesConfig;
const configWithRuleType = {
...config,
execution: {
...config.execution,
ruleTypeOverrides: [
@ -30,7 +35,7 @@ const configWithRuleType = {
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({
expect(getExecutionConfigForRuleType({ config: configWithRuleType, ruleTypeId })).toEqual({
execution: {
id: ruleTypeId,
timeout: '1m',
@ -40,6 +45,8 @@ describe('get rules config', () => {
});
test('returns the default config when there is no rule type specific config', () => {
expect(getRulesConfig({ config, ruleTypeId })).toEqual(config);
expect(getExecutionConfigForRuleType({ config, ruleTypeId })).toEqual({
execution: config.execution,
});
});
});

View file

@ -8,21 +8,21 @@
import { omit } from 'lodash';
import { RulesConfig, RuleTypeConfig } from '../config';
export const getRulesConfig = ({
export const getExecutionConfigForRuleType = ({
config,
ruleTypeId,
}: {
config: RulesConfig;
ruleTypeId: string;
}): RuleTypeConfig => {
const ruleTypeConfig = config.execution.ruleTypeOverrides?.find(
const ruleTypeExecutionConfig = config.execution.ruleTypeOverrides?.find(
(ruleType) => ruleType.id === ruleTypeId
);
return {
execution: {
...omit(config.execution, 'ruleTypeOverrides'),
...ruleTypeConfig,
...ruleTypeExecutionConfig,
},
};
};

View file

@ -31,8 +31,8 @@ const generateAlertingConfig = (): AlertingConfig => ({
maxEphemeralActionsPerAlert: 10,
defaultRuleTaskTimeout: '5m',
cancelAlertsOnRuleTimeout: true,
minimumScheduleInterval: '1m',
rules: {
minimumScheduleInterval: { value: '1m', enforce: false },
execution: {
actions: {
max: 1000,
@ -115,13 +115,16 @@ describe('Alerting Plugin', () => {
const setupContract = await plugin.setup(setupMocks, mockPlugins);
expect(setupContract.getConfig()).toEqual({ minimumScheduleInterval: '1m' });
expect(setupContract.getConfig()).toEqual({
minimumScheduleInterval: { value: '1m', enforce: false },
});
});
it(`applies the default config if there is no rule type specific config `, async () => {
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
...generateAlertingConfig(),
rules: {
minimumScheduleInterval: { value: '1m', enforce: false },
execution: {
actions: {
max: 123,
@ -147,6 +150,7 @@ describe('Alerting Plugin', () => {
const context = coreMock.createPluginInitializerContext<AlertingConfig>({
...generateAlertingConfig(),
rules: {
minimumScheduleInterval: { value: '1m', enforce: false },
execution: {
actions: { max: 123 },
ruleTypeOverrides: [{ id: sampleRuleType.id, timeout: '1d' }],

View file

@ -7,6 +7,7 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import { BehaviorSubject } from 'rxjs';
import { pick } from 'lodash';
import { UsageCollectionSetup, UsageCounter } from 'src/plugins/usage_collection/server';
import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
import {
@ -57,12 +58,12 @@ import {
scheduleApiKeyInvalidatorTask,
} from './invalidate_pending_api_keys/task';
import { scheduleAlertingHealthCheck, initializeAlertingHealth } from './health';
import { AlertingConfig, PublicAlertingConfig } from './config';
import { AlertingConfig, AlertingRulesConfig } from './config';
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';
import { getExecutionConfigForRuleType } from './lib/get_rules_config';
export const EVENT_LOG_PROVIDER = 'alerting';
export const EVENT_LOG_ACTIONS = {
@ -99,7 +100,7 @@ export interface PluginSetupContract {
>
): void;
getSecurityHealth: () => Promise<SecurityHealth>;
getConfig: () => PublicAlertingConfig;
getConfig: () => AlertingRulesConfig;
}
export interface PluginStartContract {
@ -198,11 +199,12 @@ export class AlertingPlugin {
plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS));
const ruleTypeRegistry = new RuleTypeRegistry({
logger: this.logger,
taskManager: plugins.taskManager,
taskRunnerFactory: this.taskRunnerFactory,
licenseState: this.licenseState,
licensing: plugins.licensing,
minimumScheduleInterval: this.config.minimumScheduleInterval,
minimumScheduleInterval: this.config.rules.minimumScheduleInterval,
});
this.ruleTypeRegistry = ruleTypeRegistry;
@ -288,7 +290,7 @@ export class AlertingPlugin {
if (!(ruleType.minimumLicenseRequired in LICENSE_TYPE)) {
throw new Error(`"${ruleType.minimumLicenseRequired}" is not a valid license type`);
}
ruleType.config = getRulesConfig({
ruleType.config = getExecutionConfigForRuleType({
config: alertingConfig.rules,
ruleTypeId: ruleType.id,
});
@ -310,9 +312,7 @@ export class AlertingPlugin {
);
},
getConfig: () => {
return {
minimumScheduleInterval: alertingConfig.minimumScheduleInterval,
};
return pick(alertingConfig.rules, 'minimumScheduleInterval');
},
};
}
@ -370,7 +370,7 @@ export class AlertingPlugin {
kibanaVersion: this.kibanaVersion,
authorization: alertingAuthorizationClientFactory,
eventLogger: this.eventLogger,
minimumScheduleInterval: this.config.minimumScheduleInterval,
minimumScheduleInterval: this.config.rules.minimumScheduleInterval,
});
const getRulesClientWithRequest = (request: KibanaRequest) => {

View file

@ -12,6 +12,9 @@ import { taskManagerMock } from '../../task_manager/server/mocks';
import { ILicenseState } from './lib/license_state';
import { licenseStateMock } from './lib/license_state.mock';
import { licensingMock } from '../../licensing/server/mocks';
import { loggingSystemMock } from 'src/core/server/mocks';
const logger = loggingSystemMock.create().get();
let mockedLicenseState: jest.Mocked<ILicenseState>;
let ruleTypeRegistryParams: ConstructorOptions;
@ -21,11 +24,12 @@ beforeEach(() => {
jest.resetAllMocks();
mockedLicenseState = licenseStateMock.create();
ruleTypeRegistryParams = {
logger,
taskManager,
taskRunnerFactory: new TaskRunnerFactory(),
licenseState: mockedLicenseState,
licensing: licensingMock.createSetup(),
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
};
});
@ -192,7 +196,32 @@ describe('Create Lifecycle', () => {
);
});
test('throws if defaultScheduleInterval is less than configured minimumScheduleInterval', () => {
test('logs warning if defaultScheduleInterval is less than configured minimumScheduleInterval and enforce = false', () => {
const ruleType: RuleType<never, never, never, never, never, 'default'> = {
id: '123',
name: 'Test',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
executor: jest.fn(),
producer: 'alerts',
defaultScheduleInterval: '10s',
};
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
registry.register(ruleType);
expect(logger.warn).toHaveBeenCalledWith(
`Rule type "123" has a default interval of "10s", which is less than the configured minimum of "1m".`
);
});
test('logs warning and updates default if defaultScheduleInterval is less than configured minimumScheduleInterval and enforce = true', () => {
const ruleType: RuleType<never, never, never, never, never, 'default'> = {
id: '123',
name: 'Test',
@ -209,17 +238,17 @@ describe('Create Lifecycle', () => {
executor: jest.fn(),
producer: 'alerts',
defaultScheduleInterval: '10s',
config: {
execution: {
actions: { max: 1000 },
},
},
};
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
const registry = new RuleTypeRegistry({
...ruleTypeRegistryParams,
minimumScheduleInterval: { value: '1m', enforce: true },
});
registry.register(ruleType);
expect(() => registry.register(ruleType)).toThrowError(
new Error(`Rule type \"123\" cannot specify a default interval less than 1m.`)
expect(logger.warn).toHaveBeenCalledWith(
`Rule type "123" cannot specify a default interval less than the configured minimum of "1m". "1m" will be used.`
);
expect(registry.get('123').defaultScheduleInterval).toEqual('1m');
});
test('throws if RuleType action groups contains reserved group id', () => {

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import typeDetect from 'type-detect';
import { intersection } from 'lodash';
import { Logger } from 'kibana/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { RunContext, TaskManagerSetupContract } from '../../task_manager/server';
import { TaskRunnerFactory } from './task_runner';
@ -30,13 +31,15 @@ import {
} from '../common';
import { ILicenseState } from './lib/license_state';
import { getRuleTypeFeatureUsageName } from './lib/get_rule_type_feature_usage_name';
import { AlertingRulesConfig } from '.';
export interface ConstructorOptions {
logger: Logger;
taskManager: TaskManagerSetupContract;
taskRunnerFactory: TaskRunnerFactory;
licenseState: ILicenseState;
licensing: LicensingPluginSetup;
minimumScheduleInterval: string;
minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
}
export interface RegistryRuleType
@ -126,20 +129,23 @@ export type UntypedNormalizedRuleType = NormalizedRuleType<
>;
export class RuleTypeRegistry {
private readonly logger: Logger;
private readonly taskManager: TaskManagerSetupContract;
private readonly ruleTypes: Map<string, UntypedNormalizedRuleType> = new Map();
private readonly taskRunnerFactory: TaskRunnerFactory;
private readonly licenseState: ILicenseState;
private readonly minimumScheduleInterval: string;
private readonly minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
private readonly licensing: LicensingPluginSetup;
constructor({
logger,
taskManager,
taskRunnerFactory,
licenseState,
licensing,
minimumScheduleInterval,
}: ConstructorOptions) {
this.logger = logger;
this.taskManager = taskManager;
this.taskRunnerFactory = taskRunnerFactory;
this.licenseState = licenseState;
@ -220,21 +226,18 @@ export class RuleTypeRegistry {
}
const defaultIntervalInMs = parseDuration(ruleType.defaultScheduleInterval);
const minimumIntervalInMs = parseDuration(this.minimumScheduleInterval);
const minimumIntervalInMs = parseDuration(this.minimumScheduleInterval.value);
if (defaultIntervalInMs < minimumIntervalInMs) {
throw new Error(
i18n.translate(
'xpack.alerting.ruleTypeRegistry.register.defaultTimeoutTooShortRuleTypeError',
{
defaultMessage:
'Rule type "{id}" cannot specify a default interval less than {minimumInterval}.',
values: {
id: ruleType.id,
minimumInterval: this.minimumScheduleInterval,
},
}
)
);
if (this.minimumScheduleInterval.enforce) {
this.logger.warn(
`Rule type "${ruleType.id}" cannot specify a default interval less than the configured minimum of "${this.minimumScheduleInterval.value}". "${this.minimumScheduleInterval.value}" will be used.`
);
ruleType.defaultScheduleInterval = this.minimumScheduleInterval.value;
} else {
this.logger.warn(
`Rule type "${ruleType.id}" has a default interval of "${ruleType.defaultScheduleInterval}", which is less than the configured minimum of "${this.minimumScheduleInterval.value}".`
);
}
}
}

View file

@ -84,6 +84,7 @@ import {
getModifiedSearch,
modifyFilterKueryNode,
} from './lib/mapped_params_utils';
import { AlertingRulesConfig } from '../config';
import {
formatExecutionLogResult,
getExecutionLogAggregation,
@ -133,7 +134,7 @@ export interface ConstructorOptions {
authorization: AlertingAuthorization;
actionsAuthorization: ActionsAuthorization;
ruleTypeRegistry: RuleTypeRegistry;
minimumScheduleInterval: string;
minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
spaceId?: string;
namespace?: string;
@ -269,7 +270,7 @@ export class RulesClient {
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
private readonly authorization: AlertingAuthorization;
private readonly ruleTypeRegistry: RuleTypeRegistry;
private readonly minimumScheduleInterval: string;
private readonly minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
private readonly minimumScheduleIntervalInMs: number;
private readonly createAPIKey: (name: string) => Promise<CreateAPIKeyResult>;
private readonly getActionsClient: () => Promise<ActionsClient>;
@ -311,7 +312,7 @@ export class RulesClient {
this.taskManager = taskManager;
this.ruleTypeRegistry = ruleTypeRegistry;
this.minimumScheduleInterval = minimumScheduleInterval;
this.minimumScheduleIntervalInMs = parseDuration(minimumScheduleInterval);
this.minimumScheduleIntervalInMs = parseDuration(minimumScheduleInterval.value);
this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
this.authorization = authorization;
this.createAPIKey = createAPIKey;
@ -370,9 +371,16 @@ export class RulesClient {
// Validate that schedule interval is not less than configured minimum
const intervalInMs = parseDuration(data.schedule.interval);
if (intervalInMs < this.minimumScheduleIntervalInMs) {
throw Boom.badRequest(
`Error creating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval}`
);
if (this.minimumScheduleInterval.enforce) {
throw Boom.badRequest(
`Error creating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval.value}`
);
} else {
// just log warning but allow rule to be created
this.logger.warn(
`Rule schedule interval (${data.schedule.interval}) is less than the minimum value (${this.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent creation of these rules.`
);
}
}
// Extract saved object references for this rule
@ -1112,9 +1120,16 @@ export class RulesClient {
// Validate that schedule interval is not less than configured minimum
const intervalInMs = parseDuration(data.schedule.interval);
if (intervalInMs < this.minimumScheduleIntervalInMs) {
throw Boom.badRequest(
`Error updating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval}`
);
if (this.minimumScheduleInterval.enforce) {
throw Boom.badRequest(
`Error updating rule: the interval is less than the allowed minimum interval of ${this.minimumScheduleInterval.value}`
);
} else {
// just log warning but allow rule to be updated
this.logger.warn(
`Rule schedule interval (${data.schedule.interval}) is less than the minimum value (${this.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.`
);
}
}
// Extract saved object references for this rule

View file

@ -33,7 +33,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
taskManager,
ruleTypeRegistry,
unsecuredSavedObjectsClient,
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
authorization: authorization as unknown as AlertingAuthorization,
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',

View file

@ -52,7 +52,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
};
beforeEach(() => {
@ -2546,7 +2546,73 @@ describe('create()', () => {
expect(taskManager.schedule).not.toHaveBeenCalled();
});
test('throws error when creating with an interval less than the minimum configured one', async () => {
test('logs warning when creating with an interval less than the minimum configured one when enforce = false', async () => {
const data = getMockData({ schedule: { interval: '1s' } });
ruleTypeRegistry.get.mockImplementation(() => ({
id: '123',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'alerts',
useSavedObjectReferences: {
extractReferences: jest.fn(),
injectReferences: jest.fn(),
},
}));
const createdAttributes = {
...data,
alertTypeId: '123',
schedule: { interval: '1s' },
params: {
bar: true,
},
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
mutedInstanceIds: [],
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
};
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: createdAttributes,
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
});
await rulesClient.create({ data });
expect(rulesClientParams.logger.warn).toHaveBeenCalledWith(
`Rule schedule interval (1s) is less than the minimum value (1m). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent creation of these rules.`
);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalled();
expect(taskManager.schedule).toHaveBeenCalled();
});
test('throws error when creating with an interval less than the minimum configured one when enforce = true', async () => {
rulesClient = new RulesClient({
...rulesClientParams,
minimumScheduleInterval: { value: '1m', enforce: true },
});
ruleTypeRegistry.get.mockImplementation(() => ({
id: '123',
name: 'Test',

View file

@ -30,7 +30,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
taskManager,
ruleTypeRegistry,
unsecuredSavedObjectsClient,
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
authorization: authorization as unknown as AlertingAuthorization,
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',

View file

@ -42,7 +42,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -36,7 +36,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -37,7 +37,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -35,7 +35,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -34,7 +34,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -40,7 +40,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -41,7 +41,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -38,7 +38,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -34,7 +34,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -34,7 +34,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -35,7 +35,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -34,7 +34,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -34,7 +34,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -54,7 +54,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
};
beforeEach(() => {
@ -1809,7 +1809,154 @@ describe('update()', () => {
expect(taskManager.schedule).not.toHaveBeenCalled();
});
test('throws error when updating with an interval less than the minimum configured one', async () => {
test('logs warning when creating with an interval less than the minimum configured one when enforce = false', async () => {
actionsClient.getBulk.mockReset();
actionsClient.getBulk.mockResolvedValue([
{
id: '1',
actionTypeId: 'test',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'email connector',
isPreconfigured: false,
},
{
id: '2',
actionTypeId: 'test2',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'email connector',
isPreconfigured: false,
},
]);
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
enabled: true,
schedule: { interval: '1m' },
params: {
bar: true,
},
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'action_1',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'action_2',
actionTypeId: 'test2',
params: {
foo: true,
},
},
],
notifyWhen: 'onActiveAlert',
scheduledTaskId: 'task-123',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'action_1',
type: 'action',
id: '1',
},
{
name: 'action_2',
type: 'action',
id: '2',
},
],
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'api_key_pending_invalidation',
attributes: {
apiKeyId: '234',
createdAt: '2019-02-12T21:01:22.479Z',
},
references: [],
});
await rulesClient.update({
id: '1',
data: {
schedule: { interval: '1s' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
throttle: null,
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
{
group: 'default',
id: '2',
params: {
foo: true,
},
},
],
},
});
expect(rulesClientParams.logger.warn).toHaveBeenCalledWith(
`Rule schedule interval (1s) is less than the minimum value (1m). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.`
);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalled();
});
test('throws error when updating with an interval less than the minimum configured one when enforce = true', async () => {
rulesClient = new RulesClient({
...rulesClientParams,
minimumScheduleInterval: { value: '1m', enforce: true },
});
await expect(
rulesClient.update({
id: '1',

View file

@ -35,7 +35,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),

View file

@ -52,7 +52,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
};
// this suite consists of two suites running tests against mutable RulesClient APIs:

View file

@ -35,7 +35,7 @@ const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(
const securityPluginSetup = securityMock.createSetup();
const securityPluginStart = securityMock.createStart();
const alertsAuthorization = alertingAuthorizationMock.create();
const alertingAuthorization = alertingAuthorizationMock.create();
const alertingAuthorizationClientFactory = alertingAuthorizationClientFactoryMock.createFactory();
const rulesClientFactoryParams: jest.Mocked<RulesClientFactoryOpts> = {
@ -44,7 +44,7 @@ const rulesClientFactoryParams: jest.Mocked<RulesClientFactoryOpts> = {
ruleTypeRegistry: ruleTypeRegistryMock.create(),
getSpaceId: jest.fn(),
spaceIdToNamespace: jest.fn(),
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(),
actions: actionsMock.createStart(),
eventLog: eventLogMock.createStart(),
@ -82,14 +82,14 @@ beforeEach(() => {
rulesClientFactoryParams.spaceIdToNamespace.mockReturnValue('default');
});
test('creates an alerts client with proper constructor arguments when security is enabled', async () => {
test('creates a rules client with proper constructor arguments when security is enabled', async () => {
const factory = new RulesClientFactory();
factory.initialize({ securityPluginSetup, securityPluginStart, ...rulesClientFactoryParams });
const request = KibanaRequest.from(fakeRequest);
savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient);
alertingAuthorizationClientFactory.create.mockReturnValue(
alertsAuthorization as unknown as AlertingAuthorization
alertingAuthorization as unknown as AlertingAuthorization
);
factory.create(request, savedObjectsService);
@ -107,7 +107,7 @@ test('creates an alerts client with proper constructor arguments when security i
expect(jest.requireMock('./rules_client').RulesClient).toHaveBeenCalledWith({
unsecuredSavedObjectsClient: savedObjectsClient,
authorization: alertsAuthorization,
authorization: alertingAuthorization,
actionsAuthorization,
logger: rulesClientFactoryParams.logger,
taskManager: rulesClientFactoryParams.taskManager,
@ -120,18 +120,18 @@ test('creates an alerts client with proper constructor arguments when security i
createAPIKey: expect.any(Function),
encryptedSavedObjectsClient: rulesClientFactoryParams.encryptedSavedObjectsClient,
kibanaVersion: '7.10.0',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
});
});
test('creates an alerts client with proper constructor arguments', async () => {
test('creates a rules client with proper constructor arguments', async () => {
const factory = new RulesClientFactory();
factory.initialize(rulesClientFactoryParams);
const request = KibanaRequest.from(fakeRequest);
savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient);
alertingAuthorizationClientFactory.create.mockReturnValue(
alertsAuthorization as unknown as AlertingAuthorization
alertingAuthorization as unknown as AlertingAuthorization
);
factory.create(request, savedObjectsService);
@ -145,7 +145,7 @@ test('creates an alerts client with proper constructor arguments', async () => {
expect(jest.requireMock('./rules_client').RulesClient).toHaveBeenCalledWith({
unsecuredSavedObjectsClient: savedObjectsClient,
authorization: alertsAuthorization,
authorization: alertingAuthorization,
actionsAuthorization,
logger: rulesClientFactoryParams.logger,
taskManager: rulesClientFactoryParams.taskManager,
@ -158,7 +158,7 @@ test('creates an alerts client with proper constructor arguments', async () => {
getActionsClient: expect.any(Function),
getEventLogClient: expect.any(Function),
kibanaVersion: '7.10.0',
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
});
});

View file

@ -19,6 +19,7 @@ import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/serve
import { TaskManagerStartContract } from '../../task_manager/server';
import { IEventLogClientService, IEventLogger } from '../../../plugins/event_log/server';
import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory';
import { AlertingRulesConfig } from './config';
export interface RulesClientFactoryOpts {
logger: Logger;
taskManager: TaskManagerStartContract;
@ -33,7 +34,7 @@ export interface RulesClientFactoryOpts {
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
authorization: AlertingAuthorizationClientFactory;
eventLogger?: IEventLogger;
minimumScheduleInterval: string;
minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
}
export class RulesClientFactory {
@ -51,7 +52,7 @@ export class RulesClientFactory {
private kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version'];
private authorization!: AlertingAuthorizationClientFactory;
private eventLogger?: IEventLogger;
private minimumScheduleInterval!: string;
private minimumScheduleInterval!: AlertingRulesConfig['minimumScheduleInterval'];
public initialize(options: RulesClientFactoryOpts) {
if (this.isInitialized) {

View file

@ -13,6 +13,7 @@ import { ILicenseState } from '../lib/license_state';
import { licenseStateMock } from '../lib/license_state.mock';
import { licensingMock } from '../../../licensing/server/mocks';
import { isRuleExportable } from './is_rule_exportable';
import { loggingSystemMock } from 'src/core/server/mocks';
let ruleTypeRegistryParams: ConstructorOptions;
let logger: MockedLogger;
@ -24,11 +25,12 @@ beforeEach(() => {
mockedLicenseState = licenseStateMock.create();
logger = loggerMock.create();
ruleTypeRegistryParams = {
logger: loggingSystemMock.create().get(),
taskManager,
taskRunnerFactory: new TaskRunnerFactory(),
licenseState: mockedLicenseState,
licensing: licensingMock.createSetup(),
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
};
});

View file

@ -42,7 +42,7 @@ import {
AlertExecutionStatusWarningReasons,
} from '../common';
import { LicenseType } from '../../licensing/server';
import { RulesConfig } from './config';
import { RuleTypeConfig } from './config';
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
@ -170,7 +170,7 @@ export interface RuleType<
ruleTaskTimeout?: string;
cancelAlertsOnRuleTimeout?: boolean;
doesSetRecoveryContext?: boolean;
config?: RulesConfig;
config?: RuleTypeConfig;
}
export type UntypedRuleType = RuleType<
AlertTypeParams,

View file

@ -135,7 +135,7 @@ describe('alert_form', () => {
<KibanaReactContext.Provider>
<RuleForm
rule={initialAlert}
config={{ minimumScheduleInterval: '1m' }}
config={{ minimumScheduleInterval: { value: '1m', enforce: false } }}
dispatch={() => {}}
errors={{ name: [], 'schedule.interval': [] }}
operation="create"

View file

@ -37,4 +37,4 @@ export enum SORT_ORDERS {
export const DEFAULT_SEARCH_PAGE_SIZE: number = 10;
export const DEFAULT_ALERT_INTERVAL = '1m';
export const DEFAULT_RULE_INTERVAL = '1m';

View file

@ -23,6 +23,7 @@ const rewriteBodyReq: RewriteRequestCase<RuleType> = ({
authorized_consumers: authorizedConsumers,
rule_task_timeout: ruleTaskTimeout,
does_set_recovery_context: doesSetRecoveryContext,
default_schedule_interval: defaultScheduleInterval,
...rest
}: AsApiContract<RuleType>) => ({
enabledInLicense,
@ -34,6 +35,7 @@ const rewriteBodyReq: RewriteRequestCase<RuleType> = ({
authorizedConsumers,
ruleTaskTimeout,
doesSetRecoveryContext,
defaultScheduleInterval,
...rest,
});

View file

@ -0,0 +1,23 @@
/*
* 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 { getInitialInterval } from './get_initial_interval';
import { DEFAULT_RULE_INTERVAL } from '../../constants';
describe('getInitialInterval', () => {
test('should return DEFAULT_RULE_INTERVAL if minimumScheduleInterval is undefined', () => {
expect(getInitialInterval()).toEqual(DEFAULT_RULE_INTERVAL);
});
test('should return DEFAULT_RULE_INTERVAL if minimumScheduleInterval is smaller than or equal to default', () => {
expect(getInitialInterval('1m')).toEqual(DEFAULT_RULE_INTERVAL);
});
test('should return minimumScheduleInterval if minimumScheduleInterval is greater than default', () => {
expect(getInitialInterval('5m')).toEqual('5m');
});
});

View file

@ -0,0 +1,19 @@
/*
* 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 { DEFAULT_RULE_INTERVAL } from '../../constants';
import { parseDuration } from '../../../../../alerting/common';
export function getInitialInterval(minimumScheduleInterval?: string) {
if (minimumScheduleInterval) {
// return minimum schedule interval if it is larger than the default
if (parseDuration(minimumScheduleInterval) > parseDuration(DEFAULT_RULE_INTERVAL)) {
return minimumScheduleInterval;
}
}
return DEFAULT_RULE_INTERVAL;
}

View file

@ -27,6 +27,7 @@ import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
import { ReactWrapper } from 'enzyme';
import { ALERTS_FEATURE_ID } from '../../../../../alerting/common';
import { useKibana } from '../../../common/lib/kibana';
import { triggersActionsUiConfig } from '../../../common/lib/config_api';
jest.mock('../../../common/lib/kibana');
@ -40,7 +41,7 @@ jest.mock('../../lib/rule_api', () => ({
}));
jest.mock('../../../common/lib/config_api', () => ({
triggersActionsUiConfig: jest.fn().mockResolvedValue({ minimumScheduleInterval: '1m' }),
triggersActionsUiConfig: jest.fn(),
}));
jest.mock('../../../common/lib/health_api', () => ({
@ -175,6 +176,9 @@ describe('rule_add', () => {
}
it('renders rule add flyout', async () => {
(triggersActionsUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
await setup({}, onClose);
@ -194,6 +198,9 @@ describe('rule_add', () => {
});
it('renders rule add flyout with initial values', async () => {
(triggersActionsUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
await setup(
{
@ -207,13 +214,33 @@ describe('rule_add', () => {
);
expect(wrapper.find('input#ruleName').props().value).toBe('Simple status rule');
expect(wrapper.find('[data-test-subj="tagsComboBox"]').first().text()).toBe('uptimelogs');
expect(wrapper.find('[data-test-subj="intervalInput"]').first().props().value).toEqual(1);
expect(wrapper.find('[data-test-subj="intervalInputUnit"]').first().props().value).toBe('h');
});
expect(wrapper.find('.euiSelect').first().props().value).toBe('h');
it('renders rule add flyout with DEFAULT_RULE_INTERVAL if no initialValues specified and no minimumScheduleInterval', async () => {
(triggersActionsUiConfig as jest.Mock).mockResolvedValue({});
await setup();
expect(wrapper.find('[data-test-subj="intervalInput"]').first().props().value).toEqual(1);
expect(wrapper.find('[data-test-subj="intervalInputUnit"]').first().props().value).toBe('m');
});
it('renders rule add flyout with minimumScheduleInterval if minimumScheduleInterval is greater than DEFAULT_RULE_INTERVAL', async () => {
(triggersActionsUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '5m', enforce: false },
});
await setup();
expect(wrapper.find('[data-test-subj="intervalInput"]').first().props().value).toEqual(5);
expect(wrapper.find('[data-test-subj="intervalInputUnit"]').first().props().value).toBe('m');
});
it('emit an onClose event when the rule is saved', async () => {
(triggersActionsUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
const rule = mockRule();
@ -242,7 +269,10 @@ describe('rule_add', () => {
expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.SAVED);
});
it('should enforce any default inteval', async () => {
it('should enforce any default interval', async () => {
(triggersActionsUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
await setup({ ruleTypeId: 'my-rule-type' }, jest.fn(), '3h');
// Wait for handlers to fire

View file

@ -33,8 +33,9 @@ import { HealthContextProvider } from '../../context/health_context';
import { useKibana } from '../../../common/lib/kibana';
import { hasRuleChanged, haveRuleParamsChanged } from './has_rule_changed';
import { getRuleWithInvalidatedFields } from '../../lib/value_validators';
import { DEFAULT_ALERT_INTERVAL } from '../../constants';
import { DEFAULT_RULE_INTERVAL } from '../../constants';
import { triggersActionsUiConfig } from '../../../common/lib/config_api';
import { getInitialInterval } from './get_initial_interval';
const RuleAdd = ({
consumer,
@ -58,7 +59,7 @@ const RuleAdd = ({
consumer,
ruleTypeId,
schedule: {
interval: DEFAULT_ALERT_INTERVAL,
interval: DEFAULT_RULE_INTERVAL,
},
actions: [],
tags: [],
@ -146,6 +147,14 @@ const RuleAdd = ({
})();
}, [rule, actionTypeRegistry]);
useEffect(() => {
if (config.minimumScheduleInterval && !initialValues?.schedule?.interval) {
setRuleProperty('schedule', {
interval: getInitialInterval(config.minimumScheduleInterval.value),
});
}
}, [config.minimumScheduleInterval, initialValues]);
useEffect(() => {
if (rule.ruleTypeId && ruleTypeIndex) {
const type = ruleTypeIndex.get(rule.ruleTypeId);
@ -156,7 +165,7 @@ const RuleAdd = ({
}, [rule.ruleTypeId, ruleTypeIndex, rule.schedule.interval, changedFromDefaultInterval]);
useEffect(() => {
if (rule.schedule.interval !== DEFAULT_ALERT_INTERVAL && !changedFromDefaultInterval) {
if (rule.schedule.interval !== DEFAULT_RULE_INTERVAL && !changedFromDefaultInterval) {
setChangedFromDefaultInterval(true);
}
}, [rule.schedule.interval, changedFromDefaultInterval]);

View file

@ -36,7 +36,9 @@ jest.mock('../../lib/rule_api', () => ({
}));
jest.mock('../../../common/lib/config_api', () => ({
triggersActionsUiConfig: jest.fn().mockResolvedValue({ minimumScheduleInterval: '1m' }),
triggersActionsUiConfig: jest
.fn()
.mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false } }),
}));
jest.mock('./rule_errors', () => ({
@ -229,6 +231,6 @@ describe('rule_edit', () => {
await setup();
const lastCall = getRuleErrors.mock.calls[getRuleErrors.mock.calls.length - 1];
expect(lastCall[2]).toBeDefined();
expect(lastCall[2]).toEqual({ minimumScheduleInterval: '1m' });
expect(lastCall[2]).toEqual({ minimumScheduleInterval: { value: '1m', enforce: false } });
});
});

View file

@ -17,7 +17,7 @@ import {
import { Rule, RuleTypeModel } from '../../../types';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
const config = { minimumScheduleInterval: '1m' };
const config = { minimumScheduleInterval: { value: '1m', enforce: false } };
describe('rule_errors', () => {
describe('validateBaseProperties()', () => {
it('should validate the name', () => {
@ -44,10 +44,24 @@ describe('rule_errors', () => {
});
});
it('should validate the minimumScheduleInterval', () => {
it('should validate the minimumScheduleInterval if enforce = false', () => {
const rule = mockRule();
rule.schedule.interval = '2s';
const result = validateBaseProperties(rule, config);
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': [],
ruleTypeId: [],
actionConnectors: [],
});
});
it('should validate the minimumScheduleInterval if enforce = true', () => {
const rule = mockRule();
rule.schedule.interval = '2s';
const result = validateBaseProperties(rule, {
minimumScheduleInterval: { value: '1m', enforce: true },
});
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': ['Interval must be at least 1 minute.'],

View file

@ -43,15 +43,15 @@ export function validateBaseProperties(
defaultMessage: 'Check interval is required.',
})
);
} else if (config.minimumScheduleInterval) {
} else if (config.minimumScheduleInterval && config.minimumScheduleInterval.enforce) {
const duration = parseDuration(ruleObject.schedule.interval);
const minimumDuration = parseDuration(config.minimumScheduleInterval);
const minimumDuration = parseDuration(config.minimumScheduleInterval.value);
if (duration < minimumDuration) {
errors['schedule.interval'].push(
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.belowMinimumText', {
defaultMessage: 'Interval must be at least {minimum}.',
values: {
minimum: formatDuration(config.minimumScheduleInterval, true),
minimum: formatDuration(config.minimumScheduleInterval.value, true),
},
})
);

View file

@ -94,7 +94,7 @@ describe('rule_form', () => {
describe('rule_form create rule', () => {
let wrapper: ReactWrapper<any>;
async function setup() {
async function setup(enforceMinimum = false, schedule = '1m') {
const mocks = coreMock.createSetup();
const { useLoadRuleTypes } = jest.requireMock('../../hooks/use_load_rule_types');
const ruleTypes: RuleType[] = [
@ -174,7 +174,7 @@ describe('rule_form', () => {
params: {},
consumer: ALERTS_FEATURE_ID,
schedule: {
interval: '1m',
interval: schedule,
},
actions: [],
tags: [],
@ -186,7 +186,7 @@ describe('rule_form', () => {
wrapper = mountWithIntl(
<RuleForm
rule={initialRule}
config={{ minimumScheduleInterval: '1m' }}
config={{ minimumScheduleInterval: { value: '1m', enforce: enforceMinimum } }}
dispatch={() => {}}
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [] }}
operation="create"
@ -214,13 +214,27 @@ describe('rule_form', () => {
expect(ruleTypeSelectOptions.exists()).toBeTruthy();
});
it('renders minimum schedule interval', async () => {
await setup();
it('renders minimum schedule interval helper text when enforce = true', async () => {
await setup(true);
expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual(
`Interval must be at least 1 minute.`
);
});
it('renders minimum schedule interval helper suggestion when enforce = false and schedule is less than configuration', async () => {
await setup(false, '10s');
expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual(
`Intervals less than 1 minute are not recommended due to performance considerations.`
);
});
it('does not render minimum schedule interval helper when enforce = false and schedule is greater than configuration', async () => {
await setup();
expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual(
``
);
});
it('does not render registered rule type which non editable', async () => {
await setup();
const ruleTypeSelectOptions = wrapper.find(
@ -368,7 +382,7 @@ describe('rule_form', () => {
wrapper = mountWithIntl(
<RuleForm
rule={initialRule}
config={{ minimumScheduleInterval: '1m' }}
config={{ minimumScheduleInterval: { value: '1m', enforce: false } }}
dispatch={() => {}}
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [] }}
operation="create"
@ -431,7 +445,7 @@ describe('rule_form', () => {
wrapper = mountWithIntl(
<RuleForm
rule={initialRule}
config={{ minimumScheduleInterval: '1m' }}
config={{ minimumScheduleInterval: { value: '1m', enforce: false } }}
dispatch={() => {}}
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [] }}
operation="create"

View file

@ -41,6 +41,7 @@ import {
formatDuration,
getDurationNumberInItsUnit,
getDurationUnitValue,
parseDuration,
} from '../../../../../alerting/common/parse_duration';
import { RuleReducerAction, InitialRule } from './rule_reducer';
import {
@ -73,8 +74,8 @@ import { checkRuleTypeEnabled } from '../../lib/check_rule_type_enabled';
import { ruleTypeCompare, ruleTypeGroupCompare } from '../../lib/rule_type_compare';
import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants';
import { SectionLoading } from '../../components/section_loading';
import { DEFAULT_ALERT_INTERVAL } from '../../constants';
import { useLoadRuleTypes } from '../../hooks/use_load_rule_types';
import { getInitialInterval } from './get_initial_interval';
const ENTER_KEY = 13;
@ -97,9 +98,6 @@ interface RuleFormProps<MetaData = Record<string, any>> {
filteredSolutions?: string[] | undefined;
}
const defaultScheduleInterval = getDurationNumberInItsUnit(DEFAULT_ALERT_INTERVAL);
const defaultScheduleIntervalUnit = getDurationUnitValue(DEFAULT_ALERT_INTERVAL);
export const RuleForm = ({
rule,
config,
@ -126,6 +124,10 @@ export const RuleForm = ({
const [ruleTypeModel, setRuleTypeModel] = useState<RuleTypeModel | null>(null);
const defaultRuleInterval = getInitialInterval(config.minimumScheduleInterval?.value);
const defaultScheduleInterval = getDurationNumberInItsUnit(defaultRuleInterval);
const defaultScheduleIntervalUnit = getDurationUnitValue(defaultRuleInterval);
const [ruleInterval, setRuleInterval] = useState<number | undefined>(
rule.schedule.interval
? getDurationNumberInItsUnit(rule.schedule.interval)
@ -238,15 +240,10 @@ export const RuleForm = ({
if (rule.schedule.interval) {
const interval = getDurationNumberInItsUnit(rule.schedule.interval);
const intervalUnit = getDurationUnitValue(rule.schedule.interval);
if (interval !== defaultScheduleInterval) {
setRuleInterval(interval);
}
if (intervalUnit !== defaultScheduleIntervalUnit) {
setRuleIntervalUnit(intervalUnit);
}
setRuleInterval(interval);
setRuleIntervalUnit(intervalUnit);
}
}, [rule.schedule.interval]);
}, [rule.schedule.interval, defaultScheduleInterval, defaultScheduleIntervalUnit]);
const setRuleProperty = useCallback(
<Key extends keyof Rule>(key: Key, value: Rule[Key] | null) => {
@ -588,12 +585,50 @@ export const RuleForm = ({
type="questionInCircle"
content={i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip', {
defaultMessage:
'Define how often to evaluate the condition. Checks are queued; they run as close to the defined value as capacity allows. The xpack.alerting.minimumScheduleInterval setting defines the minimum value.',
'Define how often to evaluate the condition. Checks are queued; they run as close to the defined value as capacity allows. The xpack.alerting.rules.minimumScheduleInterval.value setting defines the minimum value. The xpack.alerting.rules.minimumScheduleInterval.enforce setting defines whether this minimum is required or suggested.',
})}
/>
</>
);
const getHelpTextForInterval = () => {
if (!config || !config.minimumScheduleInterval) {
return '';
}
// No help text if there is an error
if (errors['schedule.interval'].length > 0) {
return '';
}
if (config.minimumScheduleInterval.enforce) {
// Always show help text if minimum is enforced
return i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpText', {
defaultMessage: 'Interval must be at least {minimum}.',
values: {
minimum: formatDuration(config.minimumScheduleInterval.value, true),
},
});
} else if (
rule.schedule.interval &&
parseDuration(rule.schedule.interval) < parseDuration(config.minimumScheduleInterval.value)
) {
// Only show help text if current interval is less than suggested
return i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpSuggestionText',
{
defaultMessage:
'Intervals less than {minimum} are not recommended due to performance considerations.',
values: {
minimum: formatDuration(config.minimumScheduleInterval.value, true),
},
}
);
} else {
return '';
}
};
return (
<EuiForm>
<EuiFlexGrid columns={2}>
@ -669,16 +704,7 @@ export const RuleForm = ({
fullWidth
data-test-subj="intervalFormRow"
display="rowCompressed"
helpText={
errors['schedule.interval'].length > 0
? ''
: i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpText', {
defaultMessage: 'Interval must be at least {minimum}.',
values: {
minimum: formatDuration(config.minimumScheduleInterval ?? '1m', true),
},
})
}
helpText={getHelpTextForInterval()}
label={labelForRuleChecked}
isInvalid={errors['schedule.interval'].length > 0}
error={errors['schedule.interval']}

View file

@ -41,7 +41,9 @@ jest.mock('../../../../common/lib/health_api', () => ({
triggersActionsUiHealth: jest.fn(() => ({ isRulesAvailable: true })),
}));
jest.mock('../../../../common/lib/config_api', () => ({
triggersActionsUiConfig: jest.fn().mockResolvedValue({ minimumScheduleInterval: '1m' }),
triggersActionsUiConfig: jest
.fn()
.mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false } }),
}));
jest.mock('react-router-dom', () => ({
useHistory: () => ({

View file

@ -349,5 +349,8 @@ export enum Percentiles {
}
export interface TriggersActionsUiConfig {
minimumScheduleInterval?: string;
minimumScheduleInterval?: {
value: string;
enforce: boolean;
};
}

View file

@ -13,7 +13,7 @@ describe('createConfigRoute', () => {
const router = httpServiceMock.createRouter();
const logger = loggingSystemMock.create().get();
createConfigRoute(logger, router, `/api/triggers_actions_ui`, {
minimumScheduleInterval: '1m',
minimumScheduleInterval: { value: '1m', enforce: false },
});
const [config, handler] = router.get.mock.calls[0];
@ -24,7 +24,7 @@ describe('createConfigRoute', () => {
expect(mockResponse.ok).toBeCalled();
expect(mockResponse.ok.mock.calls[0][0]).toEqual({
body: { minimumScheduleInterval: '1m' },
body: { minimumScheduleInterval: { value: '1m', enforce: false } },
});
});
});

View file

@ -13,13 +13,13 @@ import {
KibanaResponseFactory,
} from 'kibana/server';
import { Logger } from '../../../../../src/core/server';
import { PublicAlertingConfig } from '../../../alerting/server';
import { AlertingRulesConfig } from '../../../alerting/server';
export function createConfigRoute(
logger: Logger,
router: IRouter,
baseRoute: string,
config?: PublicAlertingConfig
config?: AlertingRulesConfig
) {
const path = `${baseRoute}/_config`;
logger.debug(`registering triggers_actions_ui config route GET ${path}`);

View file

@ -162,7 +162,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
'--xpack.alerting.invalidateApiKeysTask.interval="15s"',
'--xpack.alerting.healthCheck.interval="1s"',
'--xpack.alerting.minimumScheduleInterval="1s"',
'--xpack.alerting.rules.minimumScheduleInterval.value="1s"',
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
`--xpack.actions.rejectUnauthorized=${rejectUnauthorized}`,
`--xpack.actions.microsoftGraphApiUrl=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}/api/_actions-FTS-external-service-simulators/exchange/users/test@/sendMail`,

View file

@ -58,7 +58,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'--logging.loggers[2].name=http.server.response',
'--logging.loggers[2].level=all',
`--logging.loggers[2].appenders=${JSON.stringify(['file'])}`,
`--xpack.alerting.minimumScheduleInterval="1s"`,
`--xpack.alerting.rules.minimumScheduleInterval.value="1s"`,
],
},
};

View file

@ -66,7 +66,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
`--elasticsearch.hosts=https://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
`--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
`--xpack.alerting.minimumScheduleInterval="1s"`,
`--xpack.alerting.rules.minimumScheduleInterval.value="1s"`,
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
`--xpack.actions.preconfiguredAlertHistoryEsIndex=false`,
`--xpack.actions.preconfigured=${JSON.stringify({

View file

@ -78,7 +78,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
`--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`,
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
`--xpack.alerting.minimumScheduleInterval="1s"`,
`--xpack.alerting.rules.minimumScheduleInterval.value="1s"`,
'--xpack.eventLog.logEntries=true',
...disabledPlugins
.filter((k) => k !== 'security')

View file

@ -46,7 +46,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'--xpack.ruleRegistry.unsafe.indexUpgrade.enabled=true',
// Without below line, default interval for rules is 1m
// See https://github.com/elastic/kibana/pull/125396 for details
'--xpack.alerting.minimumScheduleInterval=1s',
'--xpack.alerting.rules.minimumScheduleInterval.value=1s',
'--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true',
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'riskyHostsEnabled',