diff --git a/x-pack/plugins/alerting/server/config.test.ts b/x-pack/plugins/alerting/server/config.test.ts index 9e2d2d089ac62..fe5657bc7f0ea 100644 --- a/x-pack/plugins/alerting/server/config.test.ts +++ b/x-pack/plugins/alerting/server/config.test.ts @@ -37,6 +37,9 @@ describe('config validation', () => { }, }, }, + "rulesSettings": Object { + "cacheInterval": 60000, + }, } `); }); diff --git a/x-pack/plugins/alerting/server/config.ts b/x-pack/plugins/alerting/server/config.ts index ccacc6440b03a..cbe1328bdef5f 100644 --- a/x-pack/plugins/alerting/server/config.ts +++ b/x-pack/plugins/alerting/server/config.ts @@ -7,6 +7,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { validateDurationSchema, parseDuration } from './lib'; +import { DEFAULT_CACHE_INTERVAL_MS } from './rules_settings'; export const DEFAULT_MAX_ALERTS = 1000; const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000; @@ -74,6 +75,9 @@ export const configSchema = schema.object({ enableFrameworkAlerts: schema.boolean({ defaultValue: true }), cancelAlertsOnRuleTimeout: schema.boolean({ defaultValue: true }), rules: rulesSchema, + rulesSettings: schema.object({ + cacheInterval: schema.number({ defaultValue: DEFAULT_CACHE_INTERVAL_MS }), + }), }); export type AlertingConfig = TypeOf; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 447dab2b94715..a7595eb4c5bac 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -66,7 +66,11 @@ import { ServerlessPluginSetup } from '@kbn/serverless/server'; import { RuleTypeRegistry } from './rule_type_registry'; import { TaskRunnerFactory } from './task_runner'; import { RulesClientFactory } from './rules_client_factory'; -import { RulesSettingsClientFactory } from './rules_settings_client_factory'; +import { + RulesSettingsClientFactory, + RulesSettingsService, + getRulesSettingsFeature, +} from './rules_settings'; import { MaintenanceWindowClientFactory } from './maintenance_window_client_factory'; import { ILicenseState, LicenseState } from './lib/license_state'; import { AlertingRequestHandlerContext, ALERTING_FEATURE_ID, RuleAlertData } from './types'; @@ -106,7 +110,6 @@ import { type InitializationPromise, errorResult, } from './alerts_service'; -import { getRulesSettingsFeature } from './rules_settings_feature'; import { maintenanceWindowFeature } from './maintenance_window_feature'; import { ConnectorAdapterRegistry } from './connector_adapters/connector_adapter_registry'; import { ConnectorAdapter, ConnectorAdapterParams } from './connector_adapters/types'; @@ -588,33 +591,38 @@ export class AlertingPlugin { }; taskRunnerFactory.initialize({ - logger, + actionsConfigMap: getActionsConfigMap(this.config.rules.run.actions), + actionsPlugin: plugins.actions, + alertsService: this.alertsService, + backfillClient: this.backfillClient!, + basePathService: core.http.basePath, + cancelAlertsOnRuleTimeout: this.config.cancelAlertsOnRuleTimeout, + connectorAdapterRegistry: this.connectorAdapterRegistry, data: plugins.data, - share: plugins.share, dataViews: plugins.dataViews, - savedObjects: core.savedObjects, - uiSettings: core.uiSettings, elasticsearch: core.elasticsearch, - getRulesClientWithRequest, - spaceIdToNamespace, - actionsPlugin: plugins.actions, encryptedSavedObjectsClient, - basePathService: core.http.basePath, eventLogger: this.eventLogger!, executionContext: core.executionContext, - ruleTypeRegistry: this.ruleTypeRegistry!, - alertsService: this.alertsService, + getMaintenanceWindowClientWithRequest, + getRulesClientWithRequest, kibanaBaseUrl: this.kibanaBaseUrl, - supportsEphemeralTasks: plugins.taskManager.supportsEphemeralTasks(), - maxEphemeralActionsPerRule: this.config.maxEphemeralActionsPerAlert, - cancelAlertsOnRuleTimeout: this.config.cancelAlertsOnRuleTimeout, + logger, maxAlerts: this.config.rules.run.alerts.max, - actionsConfigMap: getActionsConfigMap(this.config.rules.run.actions), + maxEphemeralActionsPerRule: this.config.maxEphemeralActionsPerAlert, + ruleTypeRegistry: this.ruleTypeRegistry!, + rulesSettingsService: new RulesSettingsService({ + cacheInterval: this.config.rulesSettings.cacheInterval, + getRulesSettingsClientWithRequest, + isServerless: !!plugins.serverless, + logger, + }), + savedObjects: core.savedObjects, + share: plugins.share, + spaceIdToNamespace, + supportsEphemeralTasks: plugins.taskManager.supportsEphemeralTasks(), + uiSettings: core.uiSettings, usageCounter: this.usageCounter, - getRulesSettingsClientWithRequest, - getMaintenanceWindowClientWithRequest, - backfillClient: this.backfillClient!, - connectorAdapterRegistry: this.connectorAdapterRegistry, }); this.eventLogService!.registerSavedObjectProvider(RULE_SAVED_OBJECT_TYPE, (request) => { diff --git a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts index cab10e242cdad..b2f25d349ec7f 100644 --- a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts @@ -12,7 +12,10 @@ import { httpServerMock } from '@kbn/core/server/mocks'; import { actionsClientMock } from '@kbn/actions-plugin/server/mocks'; import type { ActionsClientMock } from '@kbn/actions-plugin/server/mocks'; import { rulesClientMock, RulesClientMock } from '../rules_client.mock'; -import { rulesSettingsClientMock, RulesSettingsClientMock } from '../rules_settings_client.mock'; +import { + rulesSettingsClientMock, + RulesSettingsClientMock, +} from '../rules_settings/rules_settings_client.mock'; import { maintenanceWindowClientMock, MaintenanceWindowClientMock, diff --git a/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts b/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts index 80354da80b784..b1353a6e328db 100644 --- a/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts @@ -8,7 +8,10 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { rulesSettingsClientMock, RulesSettingsClientMock } from '../rules_settings_client.mock'; +import { + rulesSettingsClientMock, + RulesSettingsClientMock, +} from '../rules_settings/rules_settings_client.mock'; import { getFlappingSettingsRoute } from './get_flapping_settings'; let rulesSettingsClient: RulesSettingsClientMock; diff --git a/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.test.ts b/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.test.ts index 4102aa80b29af..e025fcf84a25b 100644 --- a/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.test.ts +++ b/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.test.ts @@ -11,7 +11,7 @@ import { mockHandlerArguments } from '../../../_mock_handler_arguments'; import { rulesSettingsClientMock, RulesSettingsClientMock, -} from '../../../../rules_settings_client.mock'; +} from '../../../../rules_settings/rules_settings_client.mock'; import { getQueryDelaySettingsRoute } from './get_query_delay_settings'; let rulesSettingsClient: RulesSettingsClientMock; diff --git a/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.test.ts b/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.test.ts index 8a506809131ab..34d3c8dfefe08 100644 --- a/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.test.ts +++ b/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.test.ts @@ -11,7 +11,7 @@ import { mockHandlerArguments } from '../../../_mock_handler_arguments'; import { rulesSettingsClientMock, RulesSettingsClientMock, -} from '../../../../rules_settings_client.mock'; +} from '../../../../rules_settings/rules_settings_client.mock'; import { updateQueryDelaySettingsRoute } from './update_query_delay_settings'; let rulesSettingsClient: RulesSettingsClientMock; diff --git a/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts b/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts index 84fb238b8509b..81ea7dd087b16 100644 --- a/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts @@ -8,7 +8,10 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { rulesSettingsClientMock, RulesSettingsClientMock } from '../rules_settings_client.mock'; +import { + rulesSettingsClientMock, + RulesSettingsClientMock, +} from '../rules_settings/rules_settings_client.mock'; import { updateFlappingSettingsRoute } from './update_flapping_settings'; let rulesSettingsClient: RulesSettingsClientMock; diff --git a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts b/x-pack/plugins/alerting/server/rules_settings/flapping/rules_settings_flapping_client.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts rename to x-pack/plugins/alerting/server/rules_settings/flapping/rules_settings_flapping_client.test.ts diff --git a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts b/x-pack/plugins/alerting/server/rules_settings/flapping/rules_settings_flapping_client.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts rename to x-pack/plugins/alerting/server/rules_settings/flapping/rules_settings_flapping_client.ts diff --git a/x-pack/plugins/alerting/server/rules_settings_client/index.ts b/x-pack/plugins/alerting/server/rules_settings/index.ts similarity index 75% rename from x-pack/plugins/alerting/server/rules_settings_client/index.ts rename to x-pack/plugins/alerting/server/rules_settings/index.ts index fcbf30b0bcb6c..d1b0525bd3754 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client/index.ts +++ b/x-pack/plugins/alerting/server/rules_settings/index.ts @@ -8,3 +8,6 @@ export * from './rules_settings_client'; export * from './flapping/rules_settings_flapping_client'; export * from './query_delay/rules_settings_query_delay_client'; +export * from './rules_settings_client_factory'; +export * from './rules_settings_feature'; +export * from './rules_settings_service'; diff --git a/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts b/x-pack/plugins/alerting/server/rules_settings/query_delay/rules_settings_query_delay_client.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts rename to x-pack/plugins/alerting/server/rules_settings/query_delay/rules_settings_query_delay_client.test.ts diff --git a/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts b/x-pack/plugins/alerting/server/rules_settings/query_delay/rules_settings_query_delay_client.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts rename to x-pack/plugins/alerting/server/rules_settings/query_delay/rules_settings_query_delay_client.ts diff --git a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_client.mock.ts similarity index 98% rename from x-pack/plugins/alerting/server/rules_settings_client.mock.ts rename to x-pack/plugins/alerting/server/rules_settings/rules_settings_client.mock.ts index 5e3479b10e0ab..cc7601a56fcd1 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_settings/rules_settings_client.mock.ts @@ -11,7 +11,7 @@ import { RulesSettingsQueryDelayClientApi, DEFAULT_FLAPPING_SETTINGS, DEFAULT_QUERY_DELAY_SETTINGS, -} from './types'; +} from '../types'; export type RulesSettingsClientMock = jest.Mocked; export type RulesSettingsFlappingClientMock = jest.Mocked; diff --git a/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.test.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_client.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.test.ts rename to x-pack/plugins/alerting/server/rules_settings/rules_settings_client.test.ts diff --git a/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_client.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.ts rename to x-pack/plugins/alerting/server/rules_settings/rules_settings_client.ts diff --git a/x-pack/plugins/alerting/server/rules_settings_client_factory.test.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_client_factory.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/rules_settings_client_factory.test.ts rename to x-pack/plugins/alerting/server/rules_settings/rules_settings_client_factory.test.ts index f107fbac3a541..8942fb31acd32 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/rules_settings/rules_settings_client_factory.test.ts @@ -18,7 +18,7 @@ import { } from '@kbn/core/server/mocks'; import { AuthenticatedUser } from '@kbn/security-plugin/common'; import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; -import { RULES_SETTINGS_SAVED_OBJECT_TYPE } from '../common'; +import { RULES_SETTINGS_SAVED_OBJECT_TYPE } from '../../common'; jest.mock('./rules_settings_client'); diff --git a/x-pack/plugins/alerting/server/rules_settings_client_factory.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_client_factory.ts similarity index 97% rename from x-pack/plugins/alerting/server/rules_settings_client_factory.ts rename to x-pack/plugins/alerting/server/rules_settings/rules_settings_client_factory.ts index 707c38b508c1a..a55529f995783 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client_factory.ts +++ b/x-pack/plugins/alerting/server/rules_settings/rules_settings_client_factory.ts @@ -13,7 +13,7 @@ import { SecurityServiceStart, } from '@kbn/core/server'; import { RulesSettingsClient } from './rules_settings_client'; -import { RULES_SETTINGS_SAVED_OBJECT_TYPE } from '../common'; +import { RULES_SETTINGS_SAVED_OBJECT_TYPE } from '../../common'; export interface RulesSettingsClientFactoryOpts { logger: Logger; diff --git a/x-pack/plugins/alerting/server/rules_settings_feature.test.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_feature.test.ts rename to x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.test.ts diff --git a/x-pack/plugins/alerting/server/rules_settings_feature.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.ts similarity index 99% rename from x-pack/plugins/alerting/server/rules_settings_feature.ts rename to x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.ts index ed346121ecde8..de01cec619dff 100644 --- a/x-pack/plugins/alerting/server/rules_settings_feature.ts +++ b/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.ts @@ -16,7 +16,7 @@ import { RULES_SETTINGS_SAVED_OBJECT_TYPE, ALL_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID, READ_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID, -} from '../common'; +} from '../../common'; export function getRulesSettingsFeature(isServerless: boolean): KibanaFeatureConfig { const settings = { diff --git a/x-pack/plugins/alerting/server/rules_settings/rules_settings_service.mock.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_service.mock.ts new file mode 100644 index 0000000000000..d4fea3bcc0da0 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_settings/rules_settings_service.mock.ts @@ -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 { DEFAULT_FLAPPING_SETTINGS, DEFAULT_QUERY_DELAY_SETTINGS } from '../types'; + +const createRulesSettingsServiceMock = () => { + return jest.fn().mockImplementation(() => { + return { + getSettings: jest.fn().mockReturnValue({ + queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + }), + }; + }); +}; + +export const rulesSettingsServiceMock = { + create: createRulesSettingsServiceMock(), +}; diff --git a/x-pack/plugins/alerting/server/rules_settings/rules_settings_service.test.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_service.test.ts new file mode 100644 index 0000000000000..7a42b2d9b92cc --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_settings/rules_settings_service.test.ts @@ -0,0 +1,241 @@ +/* + * 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 sinon from 'sinon'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { KibanaRequest } from '@kbn/core/server'; +import { rulesSettingsClientMock } from './rules_settings_client.mock'; +import { RulesSettingsService } from './rules_settings_service'; +import { DEFAULT_QUERY_DELAY_SETTINGS, DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS } from '../types'; + +const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), +} as unknown as KibanaRequest; +let fakeTimer: sinon.SinonFakeTimers; + +const logger = loggingSystemMock.create().get(); + +describe('RulesSettingsService', () => { + beforeAll(() => { + fakeTimer = sinon.useFakeTimers(new Date('2023-02-27T08:15:00.000Z')); + }); + beforeEach(() => { + fakeTimer.reset(); + }); + afterAll(() => fakeTimer.restore()); + + for (const isServerless of [false, true]) { + const label = isServerless ? 'serverless' : 'non-serverless'; + describe(`getSettings in ${label}`, () => { + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + test('should fetch settings if none in cache', async () => { + const rulesSettingsClient = rulesSettingsClientMock.create(); + const rulesSettingsService = new RulesSettingsService({ + isServerless, + logger, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClient), + }); + // @ts-ignore - accessing private variable + expect(rulesSettingsService.settings.get('default')).toBeUndefined(); + + const settings = await rulesSettingsService.getSettings(fakeRequest, 'default'); + expect(rulesSettingsClient.queryDelay().get).toHaveBeenCalledTimes(1); + expect(rulesSettingsClient.flapping().get).toHaveBeenCalledTimes(1); + + // @ts-ignore - accessing private variable + expect(rulesSettingsService.settings.get('default')).toEqual({ + lastUpdated: 1677485700000, + queryDelaySettings: { delay: 0 }, + flappingSettings: { enabled: true, lookBackWindow: 20, statusChangeThreshold: 4 }, + }); + + expect(settings.queryDelaySettings).toEqual({ delay: 0 }); + expect(settings.flappingSettings).toEqual(getFlappingSettings(20, 4)); + }); + + test('should return defaults if fetch settings errors and nothing in cache', async () => { + const rulesSettingsClient = rulesSettingsClientMock.create(); + (rulesSettingsClient.queryDelay().get as jest.Mock).mockImplementationOnce(() => { + throw new Error('no!'); + }); + + const rulesSettingsService = new RulesSettingsService({ + isServerless, + logger, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClient), + }); + + // @ts-ignore - accessing private variable + expect(rulesSettingsService.settings.get('default')).toBeUndefined(); + + const settings = await rulesSettingsService.getSettings(fakeRequest, 'default'); + expect(rulesSettingsClient.queryDelay().get).toHaveBeenCalledTimes(1); + expect(rulesSettingsClient.flapping().get).toHaveBeenCalledTimes(0); + + // @ts-ignore - accessing private variable + expect(rulesSettingsService.settings.get('default')).toBeUndefined(); + + expect(settings.queryDelaySettings).toEqual( + isServerless ? DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS : DEFAULT_QUERY_DELAY_SETTINGS + ); + expect(settings.flappingSettings).toEqual(getFlappingSettings(20, 4)); + + expect(logger.debug).toHaveBeenCalledWith( + `Failed to fetch initial rules settings, using default settings: no!` + ); + }); + + test('should fetch settings per space', async () => { + const rulesSettingsClient = rulesSettingsClientMock.create(); + (rulesSettingsClient.queryDelay().get as jest.Mock).mockResolvedValueOnce({ delay: 13 }); + (rulesSettingsClient.flapping().get as jest.Mock).mockResolvedValueOnce( + getFlappingSettings(45, 2) + ); + const rulesSettingsService = new RulesSettingsService({ + isServerless, + logger, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClient), + }); + // @ts-ignore - accessing private variable + expect(rulesSettingsService.settings.get('default')).toBeUndefined(); + + // @ts-ignore - accessing private variable + expect(rulesSettingsService.settings.get('new-space')).toBeUndefined(); + + const settingsDefault = await rulesSettingsService.getSettings(fakeRequest, 'default'); + const settingsNewSpace = await rulesSettingsService.getSettings(fakeRequest, 'new-space'); + expect(rulesSettingsClient.queryDelay().get).toHaveBeenCalledTimes(2); + expect(rulesSettingsClient.flapping().get).toHaveBeenCalledTimes(2); + + // @ts-ignore - accessing private variable + expect(rulesSettingsService.settings.get('default')).toEqual({ + lastUpdated: 1677485700000, + queryDelaySettings: { delay: 13 }, + flappingSettings: getFlappingSettings(45, 2), + }); + + // @ts-ignore - accessing private variable + expect(rulesSettingsService.settings.get('new-space')).toEqual({ + lastUpdated: 1677485700000, + queryDelaySettings: { delay: 0 }, + flappingSettings: getFlappingSettings(20, 4), + }); + + expect(settingsNewSpace.queryDelaySettings).toEqual({ delay: 0 }); + expect(settingsNewSpace.flappingSettings).toEqual(getFlappingSettings(20, 4)); + + expect(settingsDefault.queryDelaySettings).toEqual({ delay: 13 }); + expect(settingsDefault.flappingSettings).toEqual(getFlappingSettings(45, 2)); + }); + + test('should use cached settings if cache has not expired', async () => { + const rulesSettingsClient = rulesSettingsClientMock.create(); + (rulesSettingsClient.queryDelay().get as jest.Mock).mockResolvedValueOnce({ delay: 5 }); + (rulesSettingsClient.flapping().get as jest.Mock).mockResolvedValueOnce( + getFlappingSettings(30, 3) + ); + const rulesSettingsService = new RulesSettingsService({ + isServerless, + logger, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClient), + }); + + const settings1 = await rulesSettingsService.getSettings(fakeRequest, 'default'); + fakeTimer.tick(30000); + const settings2 = await rulesSettingsService.getSettings(fakeRequest, 'default'); + + expect(rulesSettingsClient.queryDelay().get).toHaveBeenCalledTimes(1); + expect(rulesSettingsClient.flapping().get).toHaveBeenCalledTimes(1); + + expect(settings1.queryDelaySettings).toEqual({ delay: 5 }); + expect(settings1.flappingSettings).toEqual(getFlappingSettings(30, 3)); + expect(settings1).toEqual(settings2); + }); + + test('should refetch settings if cache has expired', async () => { + const rulesSettingsClient = rulesSettingsClientMock.create(); + (rulesSettingsClient.queryDelay().get as jest.Mock).mockResolvedValueOnce({ delay: 5 }); + (rulesSettingsClient.flapping().get as jest.Mock).mockResolvedValueOnce( + getFlappingSettings(30, 3) + ); + (rulesSettingsClient.queryDelay().get as jest.Mock).mockResolvedValueOnce({ delay: 21 }); + (rulesSettingsClient.flapping().get as jest.Mock).mockResolvedValueOnce( + getFlappingSettings(11, 44) + ); + const rulesSettingsService = new RulesSettingsService({ + isServerless, + logger, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClient), + }); + + const settings1 = await rulesSettingsService.getSettings(fakeRequest, 'default'); + fakeTimer.tick(61000); + const settings2 = await rulesSettingsService.getSettings(fakeRequest, 'default'); + + expect(rulesSettingsClient.queryDelay().get).toHaveBeenCalledTimes(2); + expect(rulesSettingsClient.flapping().get).toHaveBeenCalledTimes(2); + + expect(settings1.queryDelaySettings).toEqual({ delay: 5 }); + expect(settings1.flappingSettings).toEqual(getFlappingSettings(30, 3)); + expect(settings2.queryDelaySettings).toEqual({ delay: 21 }); + expect(settings2.flappingSettings).toEqual(getFlappingSettings(11, 44)); + }); + + test('should return cached settings if refetching throws an error', async () => { + const rulesSettingsClient = rulesSettingsClientMock.create(); + (rulesSettingsClient.queryDelay().get as jest.Mock).mockResolvedValueOnce({ delay: 13 }); + (rulesSettingsClient.flapping().get as jest.Mock).mockResolvedValueOnce( + getFlappingSettings(11, 44) + ); + (rulesSettingsClient.queryDelay().get as jest.Mock).mockImplementationOnce(() => { + throw new Error('no!'); + }); + const rulesSettingsService = new RulesSettingsService({ + isServerless, + logger, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClient), + }); + + const settings1 = await rulesSettingsService.getSettings(fakeRequest, 'default'); + fakeTimer.tick(61000); + const settings2 = await rulesSettingsService.getSettings(fakeRequest, 'default'); + + expect(rulesSettingsClient.queryDelay().get).toHaveBeenCalledTimes(2); + + expect(settings1.queryDelaySettings).toEqual({ delay: 13 }); + expect(settings1.flappingSettings).toEqual(getFlappingSettings(11, 44)); + expect(settings1).toEqual(settings2); + + expect(logger.debug).toHaveBeenCalledWith( + `Failed to fetch rules settings after cache expiration, using cached settings: no!` + ); + }); + }); + } +}); + +const getFlappingSettings = (lookback: number, threshold: number) => ({ + enabled: true, + lookBackWindow: lookback, + statusChangeThreshold: threshold, +}); diff --git a/x-pack/plugins/alerting/server/rules_settings/rules_settings_service.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_service.ts new file mode 100644 index 0000000000000..2dd264e5d9a69 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_settings/rules_settings_service.ts @@ -0,0 +1,112 @@ +/* + * 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 { KibanaRequest, Logger } from '@kbn/core/server'; +import { + DEFAULT_FLAPPING_SETTINGS, + DEFAULT_QUERY_DELAY_SETTINGS, + DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS, + RulesSettingsClientApi, + RulesSettingsFlappingProperties, + RulesSettingsQueryDelayProperties, +} from '../types'; +import { withAlertingSpan } from '../task_runner/lib'; + +export const DEFAULT_CACHE_INTERVAL_MS = 60000; // 1 minute cache + +export interface RulesSettingsServiceConstructorOptions { + readonly isServerless: boolean; + cacheInterval?: number; + logger: Logger; + getRulesSettingsClientWithRequest(request: KibanaRequest): RulesSettingsClientApi; +} + +interface Settings { + queryDelaySettings: RulesSettingsQueryDelayProperties; + flappingSettings: RulesSettingsFlappingProperties; +} + +type LastUpdatedSettings = Settings & { lastUpdated: number }; + +export class RulesSettingsService { + private cacheIntervalMs = DEFAULT_CACHE_INTERVAL_MS; + private defaultQueryDelaySettings: RulesSettingsQueryDelayProperties; + private settings: Map = new Map(); + + constructor(private readonly options: RulesSettingsServiceConstructorOptions) { + if (options.cacheInterval) { + this.cacheIntervalMs = options.cacheInterval; + } + this.defaultQueryDelaySettings = options.isServerless + ? DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS + : DEFAULT_QUERY_DELAY_SETTINGS; + } + + public async getSettings(request: KibanaRequest, spaceId: string): Promise { + const now = Date.now(); + if (this.settings.has(spaceId)) { + const settingsFromLastUpdate = this.settings.get(spaceId)!; + const lastUpdated = new Date(settingsFromLastUpdate.lastUpdated).getTime(); + const currentFlappingSettings = settingsFromLastUpdate.flappingSettings; + const currentQueryDelaySettings = settingsFromLastUpdate.queryDelaySettings; + + if (now - lastUpdated >= this.cacheIntervalMs) { + // cache expired, refetch settings + try { + return await this.fetchSettings(request, spaceId, now); + } catch (err) { + // return cached settings on error + this.options.logger.debug( + `Failed to fetch rules settings after cache expiration, using cached settings: ${err.message}` + ); + return { + queryDelaySettings: currentQueryDelaySettings, + flappingSettings: currentFlappingSettings, + }; + } + } else { + return { + queryDelaySettings: currentQueryDelaySettings, + flappingSettings: currentFlappingSettings, + }; + } + } else { + // nothing in cache, fetch settings + try { + return await this.fetchSettings(request, spaceId, now); + } catch (err) { + // return default settings on error + this.options.logger.debug( + `Failed to fetch initial rules settings, using default settings: ${err.message}` + ); + return { + queryDelaySettings: this.defaultQueryDelaySettings, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + }; + } + } + } + + private async fetchSettings( + request: KibanaRequest, + spaceId: string, + now: number + ): Promise { + const rulesSettingsClient = this.options.getRulesSettingsClientWithRequest(request); + const queryDelaySettings = await withAlertingSpan('alerting:get-query-delay-settings', () => + rulesSettingsClient.queryDelay().get() + ); + + const flappingSettings = await withAlertingSpan('alerting:get-flapping-settings', () => + rulesSettingsClient.flapping().get() + ); + + this.settings.set(spaceId, { lastUpdated: now, queryDelaySettings, flappingSettings }); + + return { flappingSettings, queryDelaySettings }; + } +} diff --git a/x-pack/plugins/alerting/server/rules_settings_client/schemas/flapping_schema.ts b/x-pack/plugins/alerting/server/rules_settings/schemas/flapping_schema.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_client/schemas/flapping_schema.ts rename to x-pack/plugins/alerting/server/rules_settings/schemas/flapping_schema.ts diff --git a/x-pack/plugins/alerting/server/rules_settings_client/schemas/index.ts b/x-pack/plugins/alerting/server/rules_settings/schemas/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_client/schemas/index.ts rename to x-pack/plugins/alerting/server/rules_settings/schemas/index.ts diff --git a/x-pack/plugins/alerting/server/rules_settings_client/schemas/query_delay_schema.ts b/x-pack/plugins/alerting/server/rules_settings/schemas/query_delay_schema.ts similarity index 100% rename from x-pack/plugins/alerting/server/rules_settings_client/schemas/query_delay_schema.ts rename to x-pack/plugins/alerting/server/rules_settings/schemas/query_delay_schema.ts diff --git a/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.test.ts index 9b65f6613baaf..f90420ba85036 100644 --- a/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.test.ts @@ -32,7 +32,6 @@ import { TaskRunnerContext } from './types'; import { backfillClientMock } from '../backfill_client/backfill_client.mock'; import { maintenanceWindowClientMock } from '../maintenance_window_client.mock'; import { rulesClientMock } from '../rules_client.mock'; -import { rulesSettingsClientMock } from '../rules_settings_client.mock'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { AlertingEventLogger, @@ -94,6 +93,7 @@ import { validateRuleTypeParams } from '../lib/validate_rule_type_params'; import { ruleRunMetricsStoreMock } from '../lib/rule_run_metrics_store.mock'; import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; +import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock'; const UUID = '5f6aa57d-3e22-484e-bae8-cbed868f4d28'; @@ -145,17 +145,14 @@ const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); const maintenanceWindowClient = maintenanceWindowClientMock.create(); const rulesClient = rulesClientMock.create(); const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); +const rulesSettingsService = rulesSettingsServiceMock.create(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); const services = alertsMock.createRuleExecutorServices(); const uiSettingsService = uiSettingsServiceMock.createStartContract(); const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { - actionsConfigMap: { - default: { - max: 10000, - }, - }, + actionsConfigMap: { default: { max: 1000 } }, actionsPlugin: actionsMock.createStart(), alertsService, backfillClient, @@ -170,12 +167,12 @@ const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType executionContext: executionContextServiceMock.createInternalStartContract(), getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), - getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), kibanaBaseUrl: 'https://localhost:5601', logger, maxAlerts: 1000, maxEphemeralActionsPerRule: 10, ruleTypeRegistry, + rulesSettingsService, savedObjects: savedObjectsService, share: {} as SharePluginStart, spaceIdToNamespace: jest.fn().mockReturnValue(undefined), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index c6cec600090ba..833393504fdeb 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -17,6 +17,8 @@ import { Rule, RuleAction, RuleAlertData, + DEFAULT_FLAPPING_SETTINGS, + DEFAULT_QUERY_DELAY_SETTINGS, } from '../types'; import { ConcreteTaskInstance, @@ -81,7 +83,6 @@ import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_e import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; -import { rulesSettingsClientMock } from '../rules_settings_client.mock'; import { maintenanceWindowClientMock } from '../maintenance_window_client.mock'; import { alertsServiceMock } from '../alerts_service/alerts_service.mock'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; @@ -95,6 +96,7 @@ import { ruleResultServiceMock } from '../monitoring/rule_result_service.mock'; import { backfillClientMock } from '../backfill_client/backfill_client.mock'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import * as getExecutorServicesModule from './get_executor_services'; +import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -157,6 +159,7 @@ describe('Task Runner', () => { const alertsService = alertsServiceMock.create(); const maintenanceWindowClient = maintenanceWindowClientMock.create(); const connectorAdapterRegistry = new ConnectorAdapterRegistry(); + const rulesSettingsService = rulesSettingsServiceMock.create(); type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -165,37 +168,35 @@ describe('Task Runner', () => { }; const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { + actionsConfigMap: { default: { max: 1000 } }, + actionsPlugin: actionsMock.createStart(), + alertsService, + backfillClient, + basePathService: httpServiceMock.createBasePath(), + cancelAlertsOnRuleTimeout: true, + connectorAdapterRegistry, data: dataPlugin, dataViews: dataViewsMock, - savedObjects: savedObjectsService, - share: {} as SharePluginStart, - uiSettings: uiSettingsService, elasticsearch: elasticsearchService, - actionsPlugin: actionsMock.createStart(), - getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), encryptedSavedObjectsClient, - logger, - backfillClient, - executionContext: executionContextServiceMock.createInternalStartContract(), - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), - ruleTypeRegistry, - alertsService, + executionContext: executionContextServiceMock.createInternalStartContract(), + getMaintenanceWindowClientWithRequest: jest + .fn() + .mockReturnValue(maintenanceWindowClientMock.create()), + getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), kibanaBaseUrl: 'https://localhost:5601', - supportsEphemeralTasks: false, - maxEphemeralActionsPerRule: 10, + logger, maxAlerts: 1000, - cancelAlertsOnRuleTimeout: true, + maxEphemeralActionsPerRule: 10, + ruleTypeRegistry, + rulesSettingsService, + savedObjects: savedObjectsService, + share: {} as SharePluginStart, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + supportsEphemeralTasks: false, + uiSettings: uiSettingsService, usageCounter: mockUsageCounter, - actionsConfigMap: { - default: { - max: 10000, - }, - }, - getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), - getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), - connectorAdapterRegistry, }; const ephemeralTestParams: Array< @@ -242,13 +243,14 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation( (actionTypeId, actionId, params) => params ); + rulesSettingsService.getSettings.mockResolvedValue({ + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS, + }); ruleTypeRegistry.get.mockReturnValue(ruleType); taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => fn() ); - taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue( - rulesSettingsClientMock.create() - ); taskRunnerFactoryInitializerParams.getMaintenanceWindowClientWithRequest.mockReturnValue( maintenanceWindowClient ); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 409427bb34ca2..936ff2bc64797 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -291,21 +291,16 @@ export class TaskRunner< state: { previousStartedAt }, } = this.taskInstance; - const rulesSettingsClient = this.context.getRulesSettingsClientWithRequest(fakeRequest); + const { queryDelaySettings, flappingSettings } = + await this.context.rulesSettingsService.getSettings(fakeRequest, spaceId); const ruleRunMetricsStore = new RuleRunMetricsStore(); const ruleLabel = `${this.ruleType.id}:${ruleId}: '${rule.name}'`; - const queryDelay = await withAlertingSpan('alerting:get-query-delay-settings', () => - rulesSettingsClient.queryDelay().get() - ); - const flappingSettings = await withAlertingSpan('alerting:get-flapping-settings', () => - rulesSettingsClient.flapping().get() - ); const ruleTypeRunnerContext = { alertingEventLogger: this.alertingEventLogger, flappingSettings, namespace: this.context.spaceIdToNamespace(spaceId), - queryDelaySec: queryDelay.delay, + queryDelaySec: queryDelaySettings.delay, ruleId, ruleLogPrefix: ruleLabel, ruleRunMetricsStore, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts index 851bdddaed62a..cee8a2ea3b345 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts @@ -15,6 +15,8 @@ import { AlertInstanceContext, Rule, RuleAlertData, + DEFAULT_FLAPPING_SETTINGS, + DEFAULT_QUERY_DELAY_SETTINGS, } from '../types'; import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; import { TaskRunnerContext } from './types'; @@ -55,7 +57,6 @@ import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_e import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; -import { rulesSettingsClientMock } from '../rules_settings_client.mock'; import { maintenanceWindowClientMock } from '../maintenance_window_client.mock'; import { alertsServiceMock } from '../alerts_service/alerts_service.mock'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; @@ -103,6 +104,7 @@ import { import { backfillClientMock } from '../backfill_client/backfill_client.mock'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; import { createTaskRunnerLogger } from './lib'; +import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -161,6 +163,7 @@ describe('Task Runner', () => { const backfillClient = backfillClientMock.create(); const services = alertsMock.createRuleExecutorServices(); const actionsClient = actionsClientMock.create(); + const rulesSettingsService = rulesSettingsServiceMock.create(); const rulesClient = rulesClientMock.create(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); @@ -189,39 +192,33 @@ describe('Task Runner', () => { }; const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { + actionsConfigMap: { default: { max: 1000 } }, + actionsPlugin: actionsMock.createStart(), + alertsService: mockAlertsService, + backfillClient, + basePathService: httpServiceMock.createBasePath(), + cancelAlertsOnRuleTimeout: true, + connectorAdapterRegistry, data: dataPlugin, dataViews: dataViewsMock, - savedObjects: savedObjectsService, - share: {} as SharePluginStart, - uiSettings: uiSettingsService, elasticsearch: elasticsearchService, - actionsPlugin: actionsMock.createStart(), - getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), encryptedSavedObjectsClient, - logger, - executionContext: executionContextServiceMock.createInternalStartContract(), - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), - backfillClient, - ruleTypeRegistry, - alertsService: mockAlertsService, + executionContext: executionContextServiceMock.createInternalStartContract(), + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), kibanaBaseUrl: 'https://localhost:5601', - supportsEphemeralTasks: false, - maxEphemeralActionsPerRule: 10, + logger, maxAlerts: 1000, - cancelAlertsOnRuleTimeout: true, + maxEphemeralActionsPerRule: 10, + ruleTypeRegistry, + rulesSettingsService, + savedObjects: savedObjectsService, + share: {} as SharePluginStart, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + supportsEphemeralTasks: false, + uiSettings: uiSettingsService, usageCounter: mockUsageCounter, - actionsConfigMap: { - default: { - max: 10000, - }, - }, - getRulesSettingsClientWithRequest: jest - .fn() - .mockReturnValue(rulesSettingsClientMock.create()), - getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), - connectorAdapterRegistry, }; describe(`using ${label} for alert indices`, () => { @@ -251,9 +248,10 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation( (ctx, fn) => fn() ); - taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue( - rulesSettingsClientMock.create() - ); + rulesSettingsService.getSettings.mockResolvedValue({ + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS, + }); taskRunnerFactoryInitializerParams.getMaintenanceWindowClientWithRequest.mockReturnValue( maintenanceWindowClient ); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index fdc05d208aba1..b9fb6284c3911 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -15,6 +15,8 @@ import { AlertInstanceContext, Rule, RuleAlertData, + DEFAULT_FLAPPING_SETTINGS, + DEFAULT_QUERY_DELAY_SETTINGS, } from '../types'; import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; import { TaskRunner } from './task_runner'; @@ -54,7 +56,6 @@ import { EVENT_LOG_ACTIONS } from '../plugin'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import { rulesSettingsClientMock } from '../rules_settings_client.mock'; import { maintenanceWindowClientMock } from '../maintenance_window_client.mock'; import { alertsServiceMock } from '../alerts_service/alerts_service.mock'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; @@ -62,6 +63,7 @@ import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; import { TaskRunnerContext } from './types'; import { backfillClientMock } from '../backfill_client/backfill_client.mock'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; +import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -116,6 +118,7 @@ describe('Task Runner Cancel', () => { const dataPlugin = dataPluginMock.createStartContract(); const inMemoryMetrics = inMemoryMetricsMock.create(); const connectorAdapterRegistry = new ConnectorAdapterRegistry(); + const rulesSettingsService = rulesSettingsServiceMock.create(); type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -124,39 +127,35 @@ describe('Task Runner Cancel', () => { }; const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { + actionsConfigMap: { default: { max: 1000 } }, + actionsPlugin: actionsMock.createStart(), + alertsService, + backfillClient, + basePathService: httpServiceMock.createBasePath(), + cancelAlertsOnRuleTimeout: true, + connectorAdapterRegistry, data: dataPlugin, dataViews: dataViewsMock, - savedObjects: savedObjectsService, - share: {} as SharePluginStart, - uiSettings: uiSettingsService, elasticsearch: elasticsearchService, - actionsPlugin: actionsMock.createStart(), - getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), encryptedSavedObjectsClient, - logger, - executionContext: executionContextServiceMock.createInternalStartContract(), - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), - backfillClient, - ruleTypeRegistry, - alertsService, - kibanaBaseUrl: 'https://localhost:5601', - supportsEphemeralTasks: false, - maxEphemeralActionsPerRule: 10, - maxAlerts: 1000, - cancelAlertsOnRuleTimeout: true, - usageCounter: mockUsageCounter, - actionsConfigMap: { - default: { - max: 1000, - }, - }, - getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), + executionContext: executionContextServiceMock.createInternalStartContract(), getMaintenanceWindowClientWithRequest: jest .fn() .mockReturnValue(maintenanceWindowClientMock.create()), - connectorAdapterRegistry, + getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), + kibanaBaseUrl: 'https://localhost:5601', + logger, + maxAlerts: 1000, + maxEphemeralActionsPerRule: 10, + ruleTypeRegistry, + rulesSettingsService, + savedObjects: savedObjectsService, + share: {} as SharePluginStart, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + supportsEphemeralTasks: false, + uiSettings: uiSettingsService, + usageCounter: mockUsageCounter, }; beforeEach(() => { @@ -184,9 +183,10 @@ describe('Task Runner Cancel', () => { taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => fn() ); - taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue( - rulesSettingsClientMock.create() - ); + rulesSettingsService.getSettings.mockResolvedValue({ + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS, + }); taskRunnerFactoryInitializerParams.getMaintenanceWindowClientWithRequest.mockReturnValue( maintenanceWindowClientMock.create() ); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index a29d9f3c0ad91..d2e863ef865b2 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -28,16 +28,17 @@ import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import { rulesSettingsClientMock } from '../rules_settings_client.mock'; import { maintenanceWindowClientMock } from '../maintenance_window_client.mock'; import { alertsServiceMock } from '../alerts_service/alerts_service.mock'; import { schema } from '@kbn/config-schema'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; import { TaskRunnerContext } from './types'; import { backfillClientMock } from '../backfill_client/backfill_client.mock'; +import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock'; const inMemoryMetrics = inMemoryMetricsMock.create(); const backfillClient = backfillClientMock.create(); +const rulesSettingsService = rulesSettingsServiceMock.create(); const executionContext = executionContextServiceMock.createSetupContract(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); @@ -103,39 +104,35 @@ describe('Task Runner Factory', () => { const connectorAdapterRegistry = new ConnectorAdapterRegistry(); const taskRunnerFactoryInitializerParams: jest.Mocked = { + actionsConfigMap: { default: { max: 1000 } }, + actionsPlugin: actionsMock.createStart(), + alertsService: mockAlertService, backfillClient, + basePathService: httpServiceMock.createBasePath(), + cancelAlertsOnRuleTimeout: true, + connectorAdapterRegistry, data: dataPlugin, dataViews: dataViewsMock, - savedObjects: savedObjectsService, - share: {} as SharePluginStart, - uiSettings: uiSettingsService, elasticsearch: elasticsearchService, - getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), - actionsPlugin: actionsMock.createStart(), encryptedSavedObjectsClient: encryptedSavedObjectsPlugin.getClient(), - logger: loggingSystemMock.create().get(), - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), - ruleTypeRegistry: ruleTypeRegistryMock.create(), - alertsService: mockAlertService, - kibanaBaseUrl: 'https://localhost:5601', - supportsEphemeralTasks: true, - maxEphemeralActionsPerRule: 10, - maxAlerts: 1000, - cancelAlertsOnRuleTimeout: true, executionContext, - usageCounter: mockUsageCounter, - actionsConfigMap: { - default: { - max: 1000, - }, - }, - getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), getMaintenanceWindowClientWithRequest: jest .fn() .mockReturnValue(maintenanceWindowClientMock.create()), - connectorAdapterRegistry, + getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), + kibanaBaseUrl: 'https://localhost:5601', + logger: loggingSystemMock.create().get(), + maxAlerts: 1000, + maxEphemeralActionsPerRule: 10, + ruleTypeRegistry: ruleTypeRegistryMock.create(), + rulesSettingsService, + savedObjects: savedObjectsService, + share: {} as SharePluginStart, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + supportsEphemeralTasks: true, + uiSettings: uiSettingsService, + usageCounter: mockUsageCounter, }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/task_runner/types.ts b/x-pack/plugins/alerting/server/task_runner/types.ts index 9d40c186bcead..18bf53cdc60b9 100644 --- a/x-pack/plugins/alerting/server/task_runner/types.ts +++ b/x-pack/plugins/alerting/server/task_runner/types.ts @@ -48,7 +48,6 @@ import { MaintenanceWindowClientApi, RawRule, RulesClientApi, - RulesSettingsClientApi, RuleTypeRegistry, SpaceIdToNamespaceFunction, } from '../types'; @@ -57,6 +56,7 @@ import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event import { BackfillClient } from '../backfill_client/backfill_client'; import { ElasticsearchError } from '../lib'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; +import { RulesSettingsService } from '../rules_settings'; export interface RuleTaskRunResult { state: RuleTaskState; @@ -160,6 +160,7 @@ export interface TaskRunnerContext { backfillClient: BackfillClient; basePathService: IBasePath; cancelAlertsOnRuleTimeout: boolean; + connectorAdapterRegistry: ConnectorAdapterRegistry; data: DataPluginStart; dataViews: DataViewsPluginStart; elasticsearch: ElasticsearchServiceStart; @@ -168,17 +169,16 @@ export interface TaskRunnerContext { executionContext: ExecutionContextStart; getMaintenanceWindowClientWithRequest(request: KibanaRequest): MaintenanceWindowClientApi; getRulesClientWithRequest(request: KibanaRequest): RulesClientApi; - getRulesSettingsClientWithRequest(request: KibanaRequest): RulesSettingsClientApi; kibanaBaseUrl: string | undefined; logger: Logger; maxAlerts: number; maxEphemeralActionsPerRule: number; ruleTypeRegistry: RuleTypeRegistry; + rulesSettingsService: RulesSettingsService; savedObjects: SavedObjectsServiceStart; share: SharePluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; supportsEphemeralTasks: boolean; uiSettings: UiSettingsServiceStart; usageCounter?: UsageCounter; - connectorAdapterRegistry: ConnectorAdapterRegistry; } diff --git a/x-pack/plugins/alerting/server/test_utils/index.ts b/x-pack/plugins/alerting/server/test_utils/index.ts index 9985f43348eb0..036454f2f80c6 100644 --- a/x-pack/plugins/alerting/server/test_utils/index.ts +++ b/x-pack/plugins/alerting/server/test_utils/index.ts @@ -71,5 +71,6 @@ export function generateAlertingConfig(): AlertingConfig { }, }, }, + rulesSettings: { cacheInterval: 60000 }, }; } diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 3df6f241061c3..58404268efaa8 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -36,7 +36,7 @@ import { RulesSettingsClient, RulesSettingsFlappingClient, RulesSettingsQueryDelayClient, -} from './rules_settings_client'; +} from './rules_settings'; import { MaintenanceWindowClient } from './maintenance_window_client'; export * from '../common'; import { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 80439bc1cb489..1faadc6041634 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -208,6 +208,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) { id: 'test.capped', max: '1' }, ])}`, `--xpack.alerting.enableFrameworkAlerts=true`, + `--xpack.alerting.rulesSettings.cacheInterval=10000`, `--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`, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data.ts index 5ce7ffcd18ef5..c0c52ddc73b5a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IValidatedEvent } from '@kbn/event-log-plugin/server'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import type { Alert } from '@kbn/alerts-as-data-utils'; import { omit } from 'lodash'; import { @@ -87,6 +88,8 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F it(`should write alert docs during rule execution with flapping.enabled: ${enableFlapping}`, async () => { await setFlappingSettings(enableFlapping); + // wait so cache expires + await setTimeoutAsync(10000); const pattern = { alertA: [true, true, true], // stays active across executions diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_flapping.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_flapping.ts index 6dce47b54ebce..de1f4351e0fa8 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_flapping.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_flapping.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Alert } from '@kbn/alerts-as-data-utils'; import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { ALERT_FLAPPING, ALERT_FLAPPING_HISTORY, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { Spaces } from '../../../../scenarios'; @@ -57,6 +58,8 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid status_change_threshold: 4, }) .expect(200); + // wait so cache expires + await setTimeoutAsync(10000); const pattern = { alertA: [true, false, false, true, false, true, false, true, false].concat( @@ -188,6 +191,8 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid status_change_threshold: 4, }) .expect(200); + // wait so cache expires + await setTimeoutAsync(10000); const pattern = { alertA: [true, false, false, true, false, true, false, true, false, true].concat( @@ -316,6 +321,8 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid status_change_threshold: 3, }) .expect(200); + // wait so cache expires + await setTimeoutAsync(10000); const pattern = { alertA: [true, false, true, false, false, false, false, false, false], @@ -374,6 +381,8 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid status_change_threshold: 5, }) .expect(200); + // wait so cache expires + await setTimeoutAsync(10000); const pattern = { alertA: [true, false, false, true, false, true, false, true, false].concat(