diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts index 4cf3916c0a34a1..0b8d88c035b444 100644 --- a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts @@ -156,8 +156,6 @@ export function createAlertFactory< autoRecoverAlerts, // flappingSettings.enabled is false, as we only want to use this function to get the recovered alerts flappingSettings: DISABLE_FLAPPING_SETTINGS, - // no maintenance window IDs are passed as we only want to use this function to get recovered alerts - maintenanceWindowIds: [], }); return Object.keys(currentRecoveredAlerts ?? {}).map( (alertId: string) => currentRecoveredAlerts[alertId] diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts index 4391fdce06c282..17de8063884faa 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts @@ -11,6 +11,7 @@ import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { AlertsFilter, DEFAULT_FLAPPING_SETTINGS, + MaintenanceWindowStatus, RecoveredActionGroup, RuleAlertData, } from '../types'; @@ -54,8 +55,9 @@ import { Alert } from '../alert/alert'; import { AlertsClient, AlertsClientParams } from './alerts_client'; import { GetSummarizedAlertsParams, - ProcessAndLogAlertsOpts, GetMaintenanceWindowScopedQueryAlertsParams, + ProcessAlertsOpts, + LogAlertsOpts, } from './types'; import { legacyAlertsClientMock } from './legacy_alerts_client.mock'; import { keys, range } from 'lodash'; @@ -74,6 +76,9 @@ import { } from './alerts_client_fixtures'; import { getDataStreamAdapter } from '../alerts_service/lib/data_stream_adapter'; import { MaintenanceWindow } from '../application/maintenance_window/types'; +import { maintenanceWindowsServiceMock } from '../task_runner/maintenance_windows/maintenance_windows_service.mock'; +import { getMockMaintenanceWindow } from '../data/maintenance_window/test_helpers'; +import { KibanaRequest } from '@kbn/core/server'; const date = '2023-03-28T22:27:28.159Z'; const startedAtDate = '2023-03-28T13:00:00.000Z'; @@ -81,6 +86,7 @@ const maxAlerts = 1000; let logger: ReturnType<(typeof loggingSystemMock)['createLogger']>; const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const alertingEventLogger = alertingEventLoggerMock.create(); +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); const ruleType: jest.Mocked = { @@ -226,7 +232,7 @@ const getNewIndexedAlertDoc = (overrides = {}) => ({ [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true], [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['test-id1', 'test-id2'], [ALERT_RULE_CATEGORY]: 'My test rule', [ALERT_RULE_CONSUMER]: 'bar', [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -259,6 +265,7 @@ const getOngoingIndexedAlertDoc = (overrides = {}) => ({ [ALERT_TIME_RANGE]: { gte: '2023-03-28T12:27:28.159Z' }, [ALERT_PREVIOUS_ACTION_GROUP]: 'default', [ALERT_SEVERITY_IMPROVING]: undefined, + [ALERT_MAINTENANCE_WINDOW_IDS]: [], ...overrides, }); @@ -275,6 +282,7 @@ const getRecoveredIndexedAlertDoc = (overrides = {}) => ({ [ALERT_CONSECUTIVE_MATCHES]: 0, [ALERT_PREVIOUS_ACTION_GROUP]: 'default', [ALERT_SEVERITY_IMPROVING]: true, + [ALERT_MAINTENANCE_WINDOW_IDS]: [], ...overrides, }); @@ -287,12 +295,29 @@ const defaultExecutionOpts = { startedAt: null, }; +const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), +} as unknown as KibanaRequest; + const ruleInfo = `for test.rule-type:1 'rule-name'`; const logTags = { tags: ['test.rule-type', '1', 'alerts-client'] }; describe('Alerts Client', () => { let alertsClientParams: AlertsClientParams; - let processAndLogAlertsOpts: ProcessAndLogAlertsOpts; + let processAlertsOpts: ProcessAlertsOpts; + let logAlertsOpts: LogAlertsOpts; beforeAll(() => { jest.useFakeTimers(); @@ -311,22 +336,43 @@ describe('Alerts Client', () => { jest.clearAllMocks(); logger = loggingSystemMock.createLogger(); alertsClientParams = { + alertingEventLogger, logger, elasticsearchClientPromise: Promise.resolve(clusterClient), + request: fakeRequest, ruleType, + maintenanceWindowsService, namespace: 'default', rule: alertRuleData, kibanaVersion: '8.9.0', + spaceId: 'space1', dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts }), }; - processAndLogAlertsOpts = { - eventLogger: alertingEventLogger, + maintenanceWindowsService.getMaintenanceWindows.mockReturnValue({ + maintenanceWindows: [ + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id1', + }, + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id2', + }, + ], + maintenanceWindowsWithoutScopedQueryIds: ['test-id1', 'test-id2'], + }); + processAlertsOpts = { ruleRunMetricsStore, - shouldLogAlerts: false, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], alertDelay: 0, }; + logAlertsOpts = { shouldLogAlerts: false, ruleRunMetricsStore }; }); describe('initializeExecution()', () => { @@ -508,7 +554,8 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -533,6 +580,12 @@ describe('Alerts Client', () => { getNewIndexedAlertDoc({ [ALERT_UUID]: uuid2, [ALERT_INSTANCE_ID]: '2' }), ], }); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should not index new alerts if the activeCount is less than the rule alertDelay', async () => { @@ -547,11 +600,18 @@ describe('Alerts Client', () => { const alertExecutorService = alertsClient.factory(); alertExecutorService.create('1').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); expect(clusterClient.bulk).not.toHaveBeenCalled(); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should update ongoing alerts in existing index', async () => { @@ -588,7 +648,8 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -618,6 +679,12 @@ describe('Alerts Client', () => { getNewIndexedAlertDoc({ [ALERT_UUID]: uuid2, [ALERT_INSTANCE_ID]: '2' }), ], }); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should update unflattened ongoing alerts in existing index', async () => { @@ -654,7 +721,8 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -719,6 +787,12 @@ describe('Alerts Client', () => { getNewIndexedAlertDoc({ [ALERT_UUID]: uuid2, [ALERT_INSTANCE_ID]: '2' }), ], }); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should not update ongoing alerts in existing index when they are not in the processed alerts', async () => { @@ -740,6 +814,7 @@ describe('Alerts Client', () => { .mockReturnValueOnce({ '1': activeAlertObj, // return only the first (tracked) alert }) + .mockReturnValueOnce({}) .mockReturnValueOnce({}); clusterClient.search.mockResolvedValue({ @@ -773,13 +848,15 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); // will be skipped as getProcessedAlerts does not return it - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); - expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledTimes(5); expect(spy).toHaveBeenNthCalledWith(1, 'active'); expect(spy).toHaveBeenNthCalledWith(2, 'recoveredCurrent'); + expect(spy).toHaveBeenNthCalledWith(3, 'new'); expect(logger.error).toHaveBeenCalledWith( `Error writing alert(2) to .alerts-test.alerts-default - alert(2) doesn't exist in active alerts ${ruleInfo}.`, @@ -802,6 +879,12 @@ describe('Alerts Client', () => { getOngoingIndexedAlertDoc({ [ALERT_UUID]: 'abc', [ALERT_CONSECUTIVE_MATCHES]: 0 }), ], }); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should recover recovered alerts in existing index', async () => { @@ -846,7 +929,8 @@ describe('Alerts Client', () => { alertExecutorService.create('2').scheduleActions('default'); alertExecutorService.create('3').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -894,6 +978,12 @@ describe('Alerts Client', () => { getRecoveredIndexedAlertDoc({ [ALERT_UUID]: 'abc' }), ], }); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should recover unflattened recovered alerts in existing index', async () => { @@ -938,7 +1028,8 @@ describe('Alerts Client', () => { alertExecutorService.create('2').scheduleActions('default'); alertExecutorService.create('3').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -1051,6 +1142,12 @@ describe('Alerts Client', () => { }, ], }); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should use startedAt time if provided', async () => { @@ -1096,7 +1193,8 @@ describe('Alerts Client', () => { alertExecutorService.create('2').scheduleActions('default'); alertExecutorService.create('3').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -1160,6 +1258,12 @@ describe('Alerts Client', () => { }), ], }); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should use runTimestamp time if provided', async () => { @@ -1207,7 +1311,8 @@ describe('Alerts Client', () => { alertExecutorService.create('2').scheduleActions('default'); alertExecutorService.create('3').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -1271,6 +1376,12 @@ describe('Alerts Client', () => { }), ], }); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should not try to index if no alerts', async () => { @@ -1282,11 +1393,13 @@ describe('Alerts Client', () => { // Report no alerts - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); expect(clusterClient.bulk).not.toHaveBeenCalled(); + expect(maintenanceWindowsService.getMaintenanceWindows).not.toHaveBeenCalled(); }); test('should log if bulk indexing fails for some alerts', async () => { @@ -1345,7 +1458,8 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -1354,6 +1468,12 @@ describe('Alerts Client', () => { `Error writing alerts ${ruleInfo}: 1 successful, 0 conflicts, 2 errors: Validation Failed: 1: index is missing;2: type is missing;; failed to parse field [process.command_line] of type [wildcard] in document with id 'f0c9805be95fedbc3c99c663f7f02cc15826c122'.`, { tags: ['test.rule-type', '1', 'resolve-alert-conflicts'] } ); + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should log if alert to update belongs to a non-standard index', async () => { @@ -1398,7 +1518,8 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -1432,6 +1553,13 @@ describe('Alerts Client', () => { `Could not update alert abc in partial-.internal.alerts-test.alerts-default-000001. Partial and restored alert indices are not supported ${ruleInfo}.`, logTags ); + + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should log and swallow error if bulk indexing throws error', async () => { @@ -1449,7 +1577,8 @@ describe('Alerts Client', () => { alertExecutorService.create('1').scheduleActions('default'); alertExecutorService.create('2').scheduleActions('default'); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -1458,12 +1587,21 @@ describe('Alerts Client', () => { `Error writing 2 alerts to .alerts-test.alerts-default ${ruleInfo} - fail`, logTags ); + + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); }); test('should not persist alerts if shouldWrite is false', async () => { alertsClientParams = { + alertingEventLogger, logger, elasticsearchClientPromise: Promise.resolve(clusterClient), + maintenanceWindowsService, ruleType: { ...ruleType, alerts: { @@ -1471,10 +1609,12 @@ describe('Alerts Client', () => { shouldWrite: false, }, }, + request: fakeRequest, namespace: 'default', rule: alertRuleData, kibanaVersion: '8.9.0', dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts }), + spaceId: 'space1', }; const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>( alertsClientParams @@ -1490,6 +1630,7 @@ describe('Alerts Client', () => { logTags ); expect(clusterClient.bulk).not.toHaveBeenCalled(); + expect(maintenanceWindowsService.getMaintenanceWindows).not.toHaveBeenCalled(); }); }); @@ -2041,8 +2182,15 @@ describe('Alerts Client', () => { }); }); - describe('updateAlertsMaintenanceWindowIdByScopedQuery', () => { + describe('updatePersistedAlertsWithMaintenanceWindowIds', () => { test('should update alerts with MW ids when provided with maintenance windows', async () => { + maintenanceWindowsService.getMaintenanceWindows.mockReturnValueOnce({ + maintenanceWindows: [ + ...getParamsByUpdateMaintenanceWindowIds.maintenanceWindows, + { id: 'mw3' } as unknown as MaintenanceWindow, + ], + maintenanceWindowsWithoutScopedQueryIds: [], + }); const alertsClient = new AlertsClient(alertsClientParams); const alert1 = new Alert('1'); @@ -2072,11 +2220,8 @@ describe('Alerts Client', () => { // @ts-ignore .mockResolvedValueOnce({}); - // @ts-expect-error - const result = await alertsClient.updateAlertsMaintenanceWindowIdByScopedQuery([ - ...getParamsByUpdateMaintenanceWindowIds.maintenanceWindows, - { id: 'mw3' } as unknown as MaintenanceWindow, - ]); + // @ts-ignore - accessing private function + const result = await alertsClient.updatePersistedAlertsWithMaintenanceWindowIds(); expect(alert1.getMaintenanceWindowIds()).toEqual(['mw3', 'mw1']); expect(alert2.getMaintenanceWindowIds()).toEqual(['mw3', 'mw1']); @@ -2302,7 +2447,8 @@ describe('Alerts Client', () => { payload: { count: 2, url: `https://url2` }, }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -2330,7 +2476,7 @@ describe('Alerts Client', () => { [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true], [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['test-id1', 'test-id2'], [ALERT_RULE_CATEGORY]: 'My test rule', [ALERT_RULE_CONSUMER]: 'bar', [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -2365,7 +2511,7 @@ describe('Alerts Client', () => { [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true], [ALERT_INSTANCE_ID]: '2', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['test-id1', 'test-id2'], [ALERT_RULE_CATEGORY]: 'My test rule', [ALERT_RULE_CONSUMER]: 'bar', [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -2579,7 +2725,8 @@ describe('Alerts Client', () => { payload: { count: 100, url: `https://elastic.co` }, }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -2605,7 +2752,7 @@ describe('Alerts Client', () => { [ALERT_FLAPPING]: false, [ALERT_FLAPPING_HISTORY]: [true], [ALERT_INSTANCE_ID]: '1', - [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['test-id1', 'test-id2'], [ALERT_RULE_CATEGORY]: 'My test rule', [ALERT_RULE_CONSUMER]: 'bar', [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -2679,7 +2826,8 @@ describe('Alerts Client', () => { payload: { count: 100, url: `https://elastic.co` }, }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); @@ -2775,7 +2923,8 @@ describe('Alerts Client', () => { payload: { count: 100, url: `https://elastic.co` }, }); - alertsClient.processAndLogAlerts(processAndLogAlertsOpts); + await alertsClient.processAlerts(processAlertsOpts); + alertsClient.logAlerts(logAlertsOpts); await alertsClient.persistAlerts(); diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 9926ea9ec9039c..1bfa1de9e96d62 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -38,7 +38,6 @@ import type { AlertRule, LogAlertsOpts, ProcessAlertsOpts, SearchResult } from ' import { IAlertsClient, InitializeExecutionOpts, - ProcessAndLogAlertsOpts, TrackedAlerts, ReportedAlert, ReportedAlertData, @@ -62,11 +61,10 @@ import { } from './lib'; import { isValidAlertIndexName } from '../alerts_service'; import { resolveAlertConflicts } from './lib/alert_conflict_resolver'; -import { MaintenanceWindow } from '../application/maintenance_window/types'; import { filterMaintenanceWindows, filterMaintenanceWindowsIds, -} from '../task_runner/get_maintenance_windows'; +} from '../task_runner/maintenance_windows'; // Term queries can take up to 10,000 terms const CHUNK_SIZE = 10000; @@ -77,6 +75,10 @@ export interface AlertsClientParams extends CreateAlertsClientParams { dataStreamAdapter: DataStreamAdapter; } +interface AlertsAffectedByMaintenanceWindows { + alertIds: string[]; + maintenanceWindowIds: string[]; +} export class AlertsClient< AlertData extends RuleAlertData, LegacyState extends AlertInstanceState, @@ -121,7 +123,14 @@ export class AlertsClient< LegacyContext, ActionGroupIds, RecoveryActionGroupId - >({ logger: this.options.logger, ruleType: this.options.ruleType }); + >({ + alertingEventLogger: this.options.alertingEventLogger, + logger: this.options.logger, + maintenanceWindowsService: this.options.maintenanceWindowsService, + request: this.options.request, + ruleType: this.options.ruleType, + spaceId: this.options.spaceId, + }); this.indexTemplateAndPattern = getIndexTemplateAndPattern({ context: this.options.ruleType.alerts?.context!, namespace: this.options.ruleType.alerts?.isSpaceAware @@ -301,45 +310,25 @@ export class AlertsClient< return this.legacyAlertsClient.checkLimitUsage(); } - public processAlerts(opts: ProcessAlertsOpts) { - this.legacyAlertsClient.processAlerts(opts); + public async processAlerts(opts: ProcessAlertsOpts) { + await this.legacyAlertsClient.processAlerts(opts); } public logAlerts(opts: LogAlertsOpts) { this.legacyAlertsClient.logAlerts(opts); } - public processAndLogAlerts(opts: ProcessAndLogAlertsOpts) { - this.legacyAlertsClient.processAndLogAlerts(opts); - } - public getProcessedAlerts( type: 'new' | 'active' | 'activeCurrent' | 'recovered' | 'recoveredCurrent' ) { return this.legacyAlertsClient.getProcessedAlerts(type); } - public async persistAlerts(maintenanceWindows?: MaintenanceWindow[]): Promise<{ - alertIds: string[]; - maintenanceWindowIds: string[]; - } | null> { + public async persistAlerts(): Promise { // Persist alerts first await this.persistAlertsHelper(); - // Try to update the persisted alerts with maintenance windows with a scoped query - let updateAlertsMaintenanceWindowResult = null; - try { - updateAlertsMaintenanceWindowResult = await this.updateAlertsMaintenanceWindowIdByScopedQuery( - maintenanceWindows ?? [] - ); - } catch (e) { - this.options.logger.debug( - `Failed to update alert matched by maintenance window scoped query ${this.ruleInfoMessage}`, - this.logTags - ); - } - - return updateAlertsMaintenanceWindowResult; + return await this.updatePersistedAlertsWithMaintenanceWindowIds(); } public getAlertsToSerialize() { @@ -692,18 +681,39 @@ export class AlertsClient< } } - private async updateAlertsMaintenanceWindowIdByScopedQuery( - maintenanceWindows: MaintenanceWindow[] - ) { + private async updatePersistedAlertsWithMaintenanceWindowIds(): Promise { + // check if there are any alerts + const newAlerts = Object.values(this.legacyAlertsClient.getProcessedAlerts('new')); + const activeAlerts = Object.values(this.legacyAlertsClient.getProcessedAlerts('active')); + const recoveredAlerts = Object.values(this.legacyAlertsClient.getProcessedAlerts('recovered')); + + // return if there are no alerts written + if ( + (!newAlerts.length && !activeAlerts.length && !recoveredAlerts.length) || + !this.options.maintenanceWindowsService + ) { + return { + alertIds: [], + maintenanceWindowIds: [], + }; + } + + const { maintenanceWindows } = + await this.options.maintenanceWindowsService.getMaintenanceWindows({ + eventLogger: this.options.alertingEventLogger, + request: this.options.request, + ruleTypeCategory: this.ruleType.category, + spaceId: this.options.spaceId, + }); + const maintenanceWindowsWithScopedQuery = filterMaintenanceWindows({ - maintenanceWindows, + maintenanceWindows: maintenanceWindows ?? [], withScopedQuery: true, }); const maintenanceWindowsWithoutScopedQueryIds = filterMaintenanceWindowsIds({ - maintenanceWindows, + maintenanceWindows: maintenanceWindows ?? [], withScopedQuery: false, }); - if (maintenanceWindowsWithScopedQuery.length === 0) { return { alertIds: [], @@ -723,8 +733,6 @@ export class AlertsClient< const alertsAffectedByScopedQuery: string[] = []; const appliedMaintenanceWindowIds: string[] = []; - const newAlerts = Object.values(this.getProcessedAlerts('new')); - for (const [scopedQueryMaintenanceWindowId, alertIds] of Object.entries(aggsResult)) { // Go through matched alerts, find the in memory object alertIds.forEach((alertId) => { diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts index 11632a431ebc8f..db341d35ce9713 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts @@ -6,18 +6,21 @@ */ import { loggingSystemMock } from '@kbn/core/server/mocks'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; -import { AlertInstanceContext, RecoveredActionGroup } from '../types'; +import { AlertInstanceContext, MaintenanceWindowStatus, RecoveredActionGroup } from '../types'; import { LegacyAlertsClient } from './legacy_alerts_client'; import { createAlertFactory, getPublicAlertFactory } from '../alert/create_alert_factory'; import { Alert } from '../alert/alert'; -import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock'; import { ruleRunMetricsStoreMock } from '../lib/rule_run_metrics_store.mock'; import { getAlertsForNotification, processAlerts } from '../lib'; import { trimRecoveredAlerts } from '../lib/trim_recovered_alerts'; -import { logAlerts } from '../task_runner/log_alerts'; import { DEFAULT_FLAPPING_SETTINGS } from '../../common/rules_settings'; import { schema } from '@kbn/config-schema'; +import { maintenanceWindowsServiceMock } from '../task_runner/maintenance_windows/maintenance_windows_service.mock'; +import { getMockMaintenanceWindow } from '../data/maintenance_window/test_helpers'; +import { KibanaRequest } from '@kbn/core/server'; +import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock'; +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); const scheduleActions = jest.fn(); const replaceState = jest.fn(() => ({ scheduleActions })); const mockCreateAlert = jest.fn(() => ({ replaceState, scheduleActions })); @@ -81,8 +84,8 @@ jest.mock('../lib/get_alerts_for_notification', () => { jest.mock('../task_runner/log_alerts', () => ({ logAlerts: jest.fn() })); let logger: ReturnType<(typeof loggingSystemMock)['createLogger']>; -const alertingEventLogger = alertingEventLoggerMock.create(); const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); +const alertingEventLogger = alertingEventLoggerMock.create(); const ruleType: jest.Mocked = { id: 'test', @@ -118,6 +121,22 @@ const testAlert2 = { }, }; +const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), +} as unknown as KibanaRequest; + const defaultExecutionOpts = { maxAlerts: 1000, ruleLabel: `test: rule-name`, @@ -138,8 +157,12 @@ describe('Legacy Alerts Client', () => { test('initializeExecution() should create alert factory with given alerts', async () => { const alertsClient = new LegacyAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, + spaceId: 'space1', ruleType, + maintenanceWindowsService, }); await alertsClient.initializeExecution(defaultExecutionOpts); @@ -158,8 +181,12 @@ describe('Legacy Alerts Client', () => { test('factory() should call getPublicAlertFactory on alert factory', async () => { const alertsClient = new LegacyAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, + spaceId: 'space1', ruleType, + maintenanceWindowsService, }); await alertsClient.initializeExecution(defaultExecutionOpts); @@ -170,8 +197,12 @@ describe('Legacy Alerts Client', () => { test('getAlert() should pass through to alert factory function', async () => { const alertsClient = new LegacyAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, + spaceId: 'space1', ruleType, + maintenanceWindowsService, }); await alertsClient.initializeExecution(defaultExecutionOpts); @@ -182,8 +213,12 @@ describe('Legacy Alerts Client', () => { test('checkLimitUsage() should pass through to alert factory function', async () => { const alertsClient = new LegacyAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, + spaceId: 'space1', ruleType, + maintenanceWindowsService, }); await alertsClient.initializeExecution(defaultExecutionOpts); @@ -194,8 +229,12 @@ describe('Legacy Alerts Client', () => { test('hasReachedAlertLimit() should pass through to alert factory function', async () => { const alertsClient = new LegacyAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, + spaceId: 'space1', ruleType, + maintenanceWindowsService, }); await alertsClient.initializeExecution(defaultExecutionOpts); @@ -204,7 +243,26 @@ describe('Legacy Alerts Client', () => { expect(mockCreateAlertFactory.hasReachedAlertLimit).toHaveBeenCalled(); }); - test('processAndLogAlerts() should call processAlerts, trimRecoveredAlerts, getAlertsForNotification and logAlerts and store results', async () => { + test('processAlerts() should call processAlerts, trimRecoveredAlerts and getAlertsForNotifications', async () => { + maintenanceWindowsService.getMaintenanceWindows.mockReturnValue({ + maintenanceWindows: [ + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id1', + }, + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id2', + }, + ], + maintenanceWindowsWithoutScopedQueryIds: ['test-id1', 'test-id2'], + }); (processAlerts as jest.Mock).mockReturnValue({ newAlerts: {}, activeAlerts: { @@ -232,18 +290,19 @@ describe('Legacy Alerts Client', () => { recoveredAlerts: {}, }); const alertsClient = new LegacyAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, + spaceId: 'space1', ruleType, + maintenanceWindowsService, }); await alertsClient.initializeExecution(defaultExecutionOpts); - alertsClient.processAndLogAlerts({ - eventLogger: alertingEventLogger, + await alertsClient.processAlerts({ ruleRunMetricsStore, - shouldLogAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: ['window-id1', 'window-id2'], alertDelay: 5, }); @@ -261,7 +320,6 @@ describe('Legacy Alerts Client', () => { alertLimit: 1000, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: ['window-id1', 'window-id2'], startedAt: null, }); @@ -285,31 +343,126 @@ describe('Legacy Alerts Client', () => { null ); - expect(logAlerts).toHaveBeenCalledWith({ - logger, - alertingEventLogger, - newAlerts: {}, + expect(alertsClient.getProcessedAlerts('active')).toEqual({ + '1': new Alert('1', testAlert1), + '2': new Alert('2', testAlert2), + }); + + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', + }); + }); + + test('processAlerts() should set maintenance windows IDs on new alerts', async () => { + maintenanceWindowsService.getMaintenanceWindows.mockReturnValue({ + maintenanceWindows: [ + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id1', + }, + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id2', + }, + ], + maintenanceWindowsWithoutScopedQueryIds: ['test-id1', 'test-id2'], + }); + (processAlerts as jest.Mock).mockReturnValue({ + newAlerts: { + '1': new Alert('1', testAlert1), + }, activeAlerts: { + '2': new Alert('2', testAlert2), + }, + currentRecoveredAlerts: {}, + recoveredAlerts: {}, + }); + (trimRecoveredAlerts as jest.Mock).mockReturnValue({ + trimmedAlertsRecovered: {}, + earlyRecoveredAlerts: {}, + }); + (getAlertsForNotification as jest.Mock).mockReturnValue({ + newAlerts: { '1': new Alert('1', testAlert1), + }, + activeAlerts: { + '2': new Alert('2', testAlert2), + }, + currentActiveAlerts: { '2': new Alert('2', testAlert2), }, + currentRecoveredAlerts: {}, recoveredAlerts: {}, - ruleLogPrefix: 'test: rule-name', + }); + const alertsClient = new LegacyAlertsClient({ + alertingEventLogger, + logger, + request: fakeRequest, + spaceId: 'space1', + ruleType, + maintenanceWindowsService, + }); + + await alertsClient.initializeExecution({ + ...defaultExecutionOpts, + activeAlertsFromState: { + '2': testAlert2, + }, + }); + + await alertsClient.processAlerts({ ruleRunMetricsStore, - canSetRecoveryContext: false, - shouldPersistAlerts: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, + alertDelay: 5, }); - expect(alertsClient.getProcessedAlerts('active')).toEqual({ - '1': new Alert('1', testAlert1), - '2': new Alert('2', testAlert2), + expect(maintenanceWindowsService.getMaintenanceWindows).toHaveBeenCalledWith({ + eventLogger: alertingEventLogger, + request: fakeRequest, + ruleTypeCategory: 'test', + spaceId: 'space1', }); + + expect(getAlertsForNotification).toHaveBeenCalledWith( + { + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 4, + }, + 'default', + 5, + { + '1': new Alert('1', { + ...testAlert1, + meta: { ...testAlert1.meta, maintenanceWindowIds: ['test-id1', 'test-id2'] }, + }), + }, + { + '2': new Alert('2', testAlert2), + }, + {}, + {}, + null + ); }); test('isTrackedAlert() should return true if alert was active in a previous execution, false otherwise', async () => { const alertsClient = new LegacyAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, + spaceId: 'space1', ruleType, + maintenanceWindowsService, }); await alertsClient.initializeExecution(defaultExecutionOpts); diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts index c63987bea9428e..eb77de7d1918ad 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Logger } from '@kbn/core/server'; +import { KibanaRequest, Logger } from '@kbn/core/server'; import { cloneDeep, keys, merge } from 'lodash'; import { Alert } from '../alert/alert'; import { @@ -21,7 +21,6 @@ import { import { trimRecoveredAlerts } from '../lib/trim_recovered_alerts'; import { logAlerts } from '../task_runner/log_alerts'; import { AlertInstanceContext, AlertInstanceState, WithoutReservedActionGroups } from '../types'; -import { MaintenanceWindow } from '../application/maintenance_window/types'; import { DEFAULT_FLAPPING_SETTINGS, RulesSettingsFlappingProperties, @@ -29,17 +28,22 @@ import { import { IAlertsClient, InitializeExecutionOpts, - ProcessAndLogAlertsOpts, ProcessAlertsOpts, LogAlertsOpts, TrackedAlerts, } from './types'; import { DEFAULT_MAX_ALERTS } from '../config'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; +import { MaintenanceWindowsService } from '../task_runner/maintenance_windows'; +import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event_logger'; export interface LegacyAlertsClientParams { + alertingEventLogger: AlertingEventLogger; logger: Logger; + maintenanceWindowsService?: MaintenanceWindowsService; + request: KibanaRequest; ruleType: UntypedNormalizedRuleType; + spaceId: string; } export class LegacyAlertsClient< @@ -141,9 +145,8 @@ export class LegacyAlertsClient< return !!this.trackedAlerts.active[id]; } - public processAlerts({ + public async processAlerts({ flappingSettings, - maintenanceWindowIds, alertDelay, ruleRunMetricsStore, }: ProcessAlertsOpts) { @@ -160,10 +163,34 @@ export class LegacyAlertsClient< alertLimit: this.maxAlerts, autoRecoverAlerts: this.options.ruleType.autoRecoverAlerts ?? true, flappingSettings, - maintenanceWindowIds, startedAt: this.startedAtString, }); + if (this.options.maintenanceWindowsService) { + // load maintenance windows if there are any any alerts (new, active, recovered) + // this is because we need the MW IDs for any active or recovered alerts that may + // have started during the MW period. + if ( + keys(processedAlertsNew).length > 0 || + keys(processedAlertsActive).length > 0 || + keys(processedAlertsRecovered).length > 0 + ) { + const { maintenanceWindowsWithoutScopedQueryIds } = + await this.options.maintenanceWindowsService.getMaintenanceWindows({ + eventLogger: this.options.alertingEventLogger, + request: this.options.request, + ruleTypeCategory: this.options.ruleType.category, + spaceId: this.options.spaceId, + }); + + for (const id in processedAlertsNew) { + if (Object.hasOwn(processedAlertsNew, id)) { + processedAlertsNew[id].setMaintenanceWindowIds(maintenanceWindowsWithoutScopedQueryIds); + } + } + } + } + const { trimmedAlertsRecovered, earlyRecoveredAlerts } = trimRecoveredAlerts( this.options.logger, processedAlertsRecovered, @@ -190,10 +217,10 @@ export class LegacyAlertsClient< this.processedAlerts.recoveredCurrent = alerts.currentRecoveredAlerts; } - public logAlerts({ eventLogger, ruleRunMetricsStore, shouldLogAlerts }: LogAlertsOpts) { + public logAlerts({ ruleRunMetricsStore, shouldLogAlerts }: LogAlertsOpts) { logAlerts({ logger: this.options.logger, - alertingEventLogger: eventLogger, + alertingEventLogger: this.options.alertingEventLogger, newAlerts: this.processedAlerts.new, activeAlerts: this.processedAlerts.activeCurrent, recoveredAlerts: this.processedAlerts.recoveredCurrent, @@ -204,28 +231,6 @@ export class LegacyAlertsClient< }); } - public processAndLogAlerts({ - eventLogger, - ruleRunMetricsStore, - shouldLogAlerts, - flappingSettings, - maintenanceWindowIds, - alertDelay, - }: ProcessAndLogAlertsOpts) { - this.processAlerts({ - flappingSettings, - maintenanceWindowIds, - alertDelay, - ruleRunMetricsStore, - }); - - this.logAlerts({ - eventLogger, - ruleRunMetricsStore, - shouldLogAlerts, - }); - } - public getProcessedAlerts( type: 'new' | 'active' | 'activeCurrent' | 'recovered' | 'recoveredCurrent' ) { @@ -267,7 +272,7 @@ export class LegacyAlertsClient< return null; } - public async persistAlerts(maintenanceWindows?: MaintenanceWindow[]) { + public async persistAlerts() { return null; } diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/initialize_alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/lib/initialize_alerts_client.test.ts index 779f286686a7f0..c97fb9f31f82a5 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/initialize_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/initialize_alerts_client.test.ts @@ -22,6 +22,8 @@ import { alertsClientMock } from '../alerts_client.mock'; import { UntypedNormalizedRuleType } from '../../rule_type_registry'; import { legacyAlertsClientMock } from '../legacy_alerts_client.mock'; import { initializeAlertsClient, RuleData } from './initialize_alerts_client'; +import { maintenanceWindowsServiceMock } from '../../task_runner/maintenance_windows/maintenance_windows_service.mock'; +import { KibanaRequest } from '@kbn/core/server'; const alertingEventLogger = alertingEventLoggerMock.create(); const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); @@ -29,6 +31,23 @@ const alertsService = alertsServiceMock.create(); const alertsClient = alertsClientMock.create(); const legacyAlertsClient = legacyAlertsClientMock.create(); const logger = loggingSystemMock.create().get(); +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); + +const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), +} as unknown as KibanaRequest; const ruleTypeWithAlerts: jest.Mocked = { ...ruleType, @@ -75,6 +94,8 @@ describe('initializeAlertsClient', () => { context: { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, + maintenanceWindowsService, + request: fakeRequest, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -90,9 +111,13 @@ describe('initializeAlertsClient', () => { }); expect(alertsService.createAlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlerts, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { alertDelay: 0, consumer: 'bar', @@ -128,6 +153,8 @@ describe('initializeAlertsClient', () => { alertsService, context: { alertingEventLogger, + maintenanceWindowsService, + request: fakeRequest, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -143,9 +170,13 @@ describe('initializeAlertsClient', () => { }); expect(alertsService.createAlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlerts, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { alertDelay: 0, consumer: 'bar', @@ -182,6 +213,8 @@ describe('initializeAlertsClient', () => { context: { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, + maintenanceWindowsService, + request: fakeRequest, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -197,9 +230,13 @@ describe('initializeAlertsClient', () => { }); expect(alertsService.createAlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlerts, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { alertDelay: 0, consumer: 'bar', @@ -215,8 +252,12 @@ describe('initializeAlertsClient', () => { }, }); expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlerts, + spaceId: 'default', + maintenanceWindowsService, }); expect(legacyAlertsClient.initializeExecution).toHaveBeenCalledWith({ activeAlertsFromState: {}, @@ -241,6 +282,8 @@ describe('initializeAlertsClient', () => { context: { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, + maintenanceWindowsService, + request: fakeRequest, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -256,9 +299,13 @@ describe('initializeAlertsClient', () => { }); expect(alertsService.createAlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlerts, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { alertDelay: 0, consumer: 'bar', @@ -274,8 +321,12 @@ describe('initializeAlertsClient', () => { }, }); expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlerts, + spaceId: 'default', + maintenanceWindowsService, }); expect(logger.error).toHaveBeenCalledWith( `Error initializing AlertsClient for context test. Using legacy alerts client instead. - fail fail` diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/initialize_alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/lib/initialize_alerts_client.ts index f8c750c0105015..97c31ae7874277 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/initialize_alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/initialize_alerts_client.ts @@ -65,7 +65,14 @@ export const initializeAlertsClient = async < }, } = taskInstance; - const alertsClientParams = { logger, ruleType }; + const alertsClientParams = { + alertingEventLogger: context.alertingEventLogger, + logger, + maintenanceWindowsService: context.maintenanceWindowsService, + request: context.request, + ruleType, + spaceId: context.spaceId, + }; // Create AlertsClient if rule type has registered an alerts context // with the framework. The AlertsClient will handle reading and diff --git a/x-pack/plugins/alerting/server/alerts_client/types.ts b/x-pack/plugins/alerting/server/alerts_client/types.ts index e1b31e54afd10d..d043f41e1e9550 100644 --- a/x-pack/plugins/alerting/server/alerts_client/types.ts +++ b/x-pack/plugins/alerting/server/alerts_client/types.ts @@ -74,16 +74,12 @@ export interface IAlertsClient< initializeExecution(opts: InitializeExecutionOpts): Promise; hasReachedAlertLimit(): boolean; checkLimitUsage(): void; - processAndLogAlerts(opts: ProcessAndLogAlertsOpts): void; processAlerts(opts: ProcessAlertsOpts): void; logAlerts(opts: LogAlertsOpts): void; getProcessedAlerts( type: 'new' | 'active' | 'activeCurrent' | 'recovered' | 'recoveredCurrent' ): Record>; - persistAlerts(maintenanceWindows?: MaintenanceWindow[]): Promise<{ - alertIds: string[]; - maintenanceWindowIds: string[]; - } | null>; + persistAlerts(): Promise<{ alertIds: string[]; maintenanceWindowIds: string[] } | null>; isTrackedAlert(id: string): boolean; getSummarizedAlerts?(params: GetSummarizedAlertsParams): Promise; getAlertsToSerialize(): { @@ -114,13 +110,11 @@ export interface ProcessAndLogAlertsOpts { export interface ProcessAlertsOpts { flappingSettings: RulesSettingsFlappingProperties; - maintenanceWindowIds: string[]; alertDelay: number; ruleRunMetricsStore: RuleRunMetricsStore; } export interface LogAlertsOpts { - eventLogger: AlertingEventLogger; shouldLogAlerts: boolean; ruleRunMetricsStore: RuleRunMetricsStore; } diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts index ccc1bb9ea84273..4b4f632d4cb208 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts @@ -21,12 +21,34 @@ import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { AlertsClient } from '../alerts_client'; import { alertsClientMock } from '../alerts_client/alerts_client.mock'; import { getDataStreamAdapter } from './lib/data_stream_adapter'; +import { maintenanceWindowsServiceMock } from '../task_runner/maintenance_windows/maintenance_windows_service.mock'; +import { KibanaRequest } from '@kbn/core/server'; +import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock'; jest.mock('../alerts_client'); +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); +const alertingEventLogger = alertingEventLoggerMock.create(); + let logger: ReturnType<(typeof loggingSystemMock)['createLogger']>; const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; +const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), +} as unknown as KibanaRequest; + const SimulateTemplateResponse = { template: { aliases: { @@ -1507,9 +1529,13 @@ describe('Alerts Service', () => { ); await alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1526,11 +1552,15 @@ describe('Alerts Service', () => { }); expect(AlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, elasticsearchClientPromise: Promise.resolve(clusterClient), dataStreamAdapter, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1563,9 +1593,13 @@ describe('Alerts Service', () => { async () => alertsService.isInitialized() === true ); const result = await alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1613,9 +1647,13 @@ describe('Alerts Service', () => { expect(clusterClient.indices.create).not.toHaveBeenCalled(); const result = await alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1646,11 +1684,15 @@ describe('Alerts Service', () => { } expect(AlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, elasticsearchClientPromise: Promise.resolve(clusterClient), dataStreamAdapter, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1714,9 +1756,13 @@ describe('Alerts Service', () => { // call createAlertsClient at the same time which will trigger the retries const result = await Promise.all([ alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1732,9 +1778,13 @@ describe('Alerts Service', () => { }, }), alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1765,11 +1815,15 @@ describe('Alerts Service', () => { expect(clusterClient.indices.getAlias).toHaveBeenCalled(); } expect(AlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, elasticsearchClientPromise: Promise.resolve(clusterClient), dataStreamAdapter, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1825,9 +1879,13 @@ describe('Alerts Service', () => { ); const result = await alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1844,11 +1902,15 @@ describe('Alerts Service', () => { }); expect(AlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, elasticsearchClientPromise: Promise.resolve(clusterClient), dataStreamAdapter, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1912,9 +1974,13 @@ describe('Alerts Service', () => { } return await alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -1938,11 +2004,15 @@ describe('Alerts Service', () => { expect(AlertsClient).toHaveBeenCalledTimes(2); expect(AlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger, elasticsearchClientPromise: Promise.resolve(clusterClient), dataStreamAdapter, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -2011,9 +2081,13 @@ describe('Alerts Service', () => { } return await alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -2078,9 +2152,13 @@ describe('Alerts Service', () => { expect(clusterClient.indices.create).not.toHaveBeenCalled(); const result = await alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -2145,9 +2223,13 @@ describe('Alerts Service', () => { expect(clusterClient.indices.create).not.toHaveBeenCalled(); const result = await alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -2210,9 +2292,13 @@ describe('Alerts Service', () => { ); const result = await alertsService.createAlertsClient({ + alertingEventLogger, logger, + request: fakeRequest, ruleType: ruleTypeWithAlertDefinition, + maintenanceWindowsService, namespace: 'default', + spaceId: 'default', rule: { consumer: 'bar', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts index f37481c9ccb863..4f0ead6c542065 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.ts @@ -234,11 +234,15 @@ export class AlertsService implements IAlertsService { ActionGroupIds, RecoveryActionGroupId >({ + alertingEventLogger: opts.alertingEventLogger, logger: this.options.logger, elasticsearchClientPromise: this.options.elasticsearchClientPromise, ruleType: opts.ruleType, + maintenanceWindowsService: opts.maintenanceWindowsService, namespace: opts.namespace, + request: opts.request, rule: opts.rule, + spaceId: opts.spaceId, kibanaVersion: this.options.kibanaVersion, dataStreamAdapter: this.dataStreamAdapter, }); diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/methods/get_active/get_active_maintenance_windows.test.ts b/x-pack/plugins/alerting/server/application/maintenance_window/methods/get_active/get_active_maintenance_windows.test.ts index e40a189efa163b..b26af5a9c2349d 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/methods/get_active/get_active_maintenance_windows.test.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/methods/get_active/get_active_maintenance_windows.test.ts @@ -103,6 +103,90 @@ describe('MaintenanceWindowClient - getActiveMaintenanceWindows', () => { ]); }); + it('should use cacheInterval if provided', async () => { + jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z')); + + savedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [ + { + attributes: getMockMaintenanceWindow({ expirationDate: new Date().toISOString() }), + id: 'test-1', + }, + { + attributes: getMockMaintenanceWindow({ expirationDate: new Date().toISOString() }), + id: 'test-2', + }, + ], + } as unknown as SavedObjectsFindResponse); + + const result = await getActiveMaintenanceWindows(mockContext, 60000); + + const findCallParams = savedObjectsClient.find.mock.calls[0][0]; + + expect(findCallParams.type).toEqual(MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE); + + expect(toElasticsearchQuery(findCallParams.filter)).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "maintenance-window.attributes.events": "2023-02-26T00:00:00.000Z", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "maintenance-window.attributes.events": "2023-02-26T00:01:00.000Z", + }, + }, + ], + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "maintenance-window.attributes.enabled": "true", + }, + }, + ], + }, + }, + ], + }, + } + `); + + expect(result).toEqual([ + expect.objectContaining({ + id: 'test-1', + }), + expect.objectContaining({ + id: 'test-2', + }), + ]); + }); + it('should return empty array if there are no active maintenance windows', async () => { jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z')); diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/methods/get_active/get_active_maintenance_windows.ts b/x-pack/plugins/alerting/server/application/maintenance_window/methods/get_active/get_active_maintenance_windows.ts index 9fc834c6075485..4d42cb0f236885 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/methods/get_active/get_active_maintenance_windows.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/methods/get_active/get_active_maintenance_windows.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { nodeBuilder } from '@kbn/es-query'; +import { KueryNode, nodeBuilder } from '@kbn/es-query'; import type { MaintenanceWindowClientContext } from '../../../../../common'; import type { MaintenanceWindow } from '../../types'; import { transformMaintenanceWindowAttributesToMaintenanceWindow } from '../../transforms'; @@ -23,15 +23,29 @@ export interface MaintenanceWindowAggregationResult { } export async function getActiveMaintenanceWindows( - context: MaintenanceWindowClientContext + context: MaintenanceWindowClientContext, + cacheIntervalMs?: number ): Promise { const { savedObjectsClient, logger } = context; const startDate = new Date(); const startDateISO = startDate.toISOString(); + let eventsKuery: KueryNode; + if (cacheIntervalMs) { + // add offset to startDate + const startDateWithCacheOffset = new Date(startDate.getTime() + cacheIntervalMs); + const startDateWithCacheOffsetISO = startDateWithCacheOffset.toISOString(); + eventsKuery = nodeBuilder.or([ + nodeBuilder.is('maintenance-window.attributes.events', startDateISO), + nodeBuilder.is('maintenance-window.attributes.events', startDateWithCacheOffsetISO), + ]); + } else { + eventsKuery = nodeBuilder.is('maintenance-window.attributes.events', startDateISO); + } + const filter = nodeBuilder.and([ - nodeBuilder.is('maintenance-window.attributes.events', startDateISO), + eventsKuery, nodeBuilder.is('maintenance-window.attributes.enabled', 'true'), ]); diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts index 0b33607a34dd26..9cfc7fe14f8bac 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts @@ -12,8 +12,6 @@ import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; -const maintenanceWindowIds = ['test-id-1', 'test-id-2']; - describe('processAlerts', () => { let clock: sinon.SinonFakeTimers; @@ -60,7 +58,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(newAlerts).toEqual({ '1': newAlert }); @@ -99,7 +96,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(newAlerts).toEqual({ '1': newAlert1, '2': newAlert2 }); @@ -150,7 +146,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], startedAt: '2023-10-03T20:03:08.716Z', }); @@ -168,46 +163,6 @@ describe('processAlerts', () => { expect(newAlert1State.end).not.toBeDefined(); expect(newAlert2State.end).not.toBeDefined(); }); - - test('sets maintenance window IDs in new alert state', () => { - const newAlert1 = new Alert('1'); - const newAlert2 = new Alert('2'); - const existingAlert1 = new Alert('3'); - const existingAlert2 = new Alert('4'); - - const existingAlerts = { - '3': existingAlert1, - '4': existingAlert2, - }; - - const updatedAlerts = { - ...cloneDeep(existingAlerts), - '1': newAlert1, - '2': newAlert2, - }; - - updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); - updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); - updatedAlerts['3'].scheduleActions('default' as never, { foo: '1' }); - updatedAlerts['4'].scheduleActions('default' as never, { foo: '2' }); - - expect(newAlert1.getState()).toStrictEqual({}); - expect(newAlert2.getState()).toStrictEqual({}); - - const { newAlerts } = processAlerts({ - alerts: updatedAlerts, - existingAlerts, - previouslyRecoveredAlerts: {}, - hasReachedAlertLimit: false, - alertLimit: 10, - autoRecoverAlerts: true, - flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds, - }); - - expect(newAlerts['1'].getMaintenanceWindowIds()).toEqual(maintenanceWindowIds); - expect(newAlerts['2'].getMaintenanceWindowIds()).toEqual(maintenanceWindowIds); - }); }); describe('activeAlerts', () => { @@ -238,7 +193,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toEqual({ @@ -277,7 +231,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toEqual({ @@ -326,7 +279,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toEqual({ @@ -385,7 +337,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toEqual({ @@ -451,7 +402,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toEqual({ @@ -513,7 +463,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect( @@ -570,7 +519,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], startedAt: '2023-10-03T20:03:08.716Z', }); @@ -590,37 +538,6 @@ describe('processAlerts', () => { expect(previouslyRecoveredAlert1State.end).not.toBeDefined(); expect(previouslyRecoveredAlert2State.end).not.toBeDefined(); }); - - test('should not set maintenance window IDs for active alerts', () => { - const newAlert = new Alert('1'); - const existingAlert1 = new Alert('2'); - - const existingAlerts = { - '2': existingAlert1, - }; - existingAlerts['2'].replaceState({ start: '1969-12-30T00:00:00.000Z', duration: 33000 }); - - const updatedAlerts = { - ...cloneDeep(existingAlerts), - '1': newAlert, - }; - - updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); - updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); - - const { activeAlerts } = processAlerts({ - alerts: updatedAlerts, - existingAlerts, - previouslyRecoveredAlerts: {}, - hasReachedAlertLimit: false, - alertLimit: 10, - autoRecoverAlerts: true, - flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds, - }); - - expect(activeAlerts['2'].getMaintenanceWindowIds()).toEqual([]); - }); }); describe('recoveredAlerts', () => { @@ -646,7 +563,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'] }); @@ -675,7 +591,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(recoveredAlerts).toEqual({}); @@ -706,7 +621,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] }); @@ -749,7 +663,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], startedAt: '2023-10-03T20:03:08.716Z', }); @@ -790,7 +703,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] }); @@ -830,7 +742,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(recoveredAlerts).toEqual(updatedAlerts); @@ -861,39 +772,10 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: false, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(recoveredAlerts).toEqual({}); }); - - test('should not set maintenance window IDs for recovered alerts', () => { - const activeAlert = new Alert('1'); - const recoveredAlert1 = new Alert('2'); - - const existingAlerts = { - '1': activeAlert, - '2': recoveredAlert1, - }; - existingAlerts['2'].replaceState({ start: '1969-12-30T00:00:00.000Z', duration: 33000 }); - - const updatedAlerts = cloneDeep(existingAlerts); - - updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); - - const { recoveredAlerts } = processAlerts({ - alerts: updatedAlerts, - existingAlerts, - previouslyRecoveredAlerts: {}, - hasReachedAlertLimit: false, - alertLimit: 10, - autoRecoverAlerts: true, - flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds, - }); - - expect(recoveredAlerts['2'].getMaintenanceWindowIds()).toEqual([]); - }); }); describe('when hasReachedAlertLimit is true', () => { @@ -936,7 +818,6 @@ describe('processAlerts', () => { alertLimit: 7, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(recoveredAlerts).toEqual({}); @@ -973,7 +854,6 @@ describe('processAlerts', () => { alertLimit: 7, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toEqual({ @@ -1034,7 +914,6 @@ describe('processAlerts', () => { alertLimit: MAX_ALERTS, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(Object.keys(activeAlerts).length).toEqual(MAX_ALERTS); @@ -1052,68 +931,6 @@ describe('processAlerts', () => { '7': newAlert7, }); }); - - test('should set maintenance window IDs for new alerts when reached alert limit', () => { - const MAX_ALERTS = 7; - const existingAlert1 = new Alert('1'); - const existingAlert2 = new Alert('2'); - const existingAlert3 = new Alert('3'); - const existingAlert4 = new Alert('4'); - const existingAlert5 = new Alert('5'); - const newAlert6 = new Alert('6'); - const newAlert7 = new Alert('7'); - const newAlert8 = new Alert('8'); - const newAlert9 = new Alert('9'); - const newAlert10 = new Alert('10'); - - const existingAlerts = { - '1': existingAlert1, - '2': existingAlert2, - '3': existingAlert3, - '4': existingAlert4, - '5': existingAlert5, - }; - - const updatedAlerts = { - ...cloneDeep(existingAlerts), - '6': newAlert6, - '7': newAlert7, - '8': newAlert8, - '9': newAlert9, - '10': newAlert10, - }; - - updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); - updatedAlerts['2'].scheduleActions('default' as never, { foo: '1' }); - updatedAlerts['3'].scheduleActions('default' as never, { foo: '2' }); - updatedAlerts['4'].scheduleActions('default' as never, { foo: '2' }); - // intentionally not scheduling actions for alert "5" - updatedAlerts['6'].scheduleActions('default' as never, { foo: '2' }); - updatedAlerts['7'].scheduleActions('default' as never, { foo: '2' }); - updatedAlerts['8'].scheduleActions('default' as never, { foo: '2' }); - updatedAlerts['9'].scheduleActions('default' as never, { foo: '2' }); - updatedAlerts['10'].scheduleActions('default' as never, { foo: '2' }); - - const { activeAlerts, newAlerts } = processAlerts({ - alerts: updatedAlerts, - existingAlerts, - previouslyRecoveredAlerts: {}, - hasReachedAlertLimit: true, - alertLimit: MAX_ALERTS, - autoRecoverAlerts: true, - flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds, - }); - - expect(Object.keys(activeAlerts).length).toEqual(MAX_ALERTS); - expect(newAlerts['6'].getMaintenanceWindowIds()).toEqual(maintenanceWindowIds); - expect(newAlerts['7'].getMaintenanceWindowIds()).toEqual(maintenanceWindowIds); - expect(activeAlerts['1'].getMaintenanceWindowIds()).toEqual([]); - expect(activeAlerts['2'].getMaintenanceWindowIds()).toEqual([]); - expect(activeAlerts['3'].getMaintenanceWindowIds()).toEqual([]); - expect(activeAlerts['4'].getMaintenanceWindowIds()).toEqual([]); - expect(activeAlerts['5'].getMaintenanceWindowIds()).toEqual([]); - }); }); describe('updating flappingHistory', () => { @@ -1133,7 +950,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1189,7 +1005,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1231,7 +1046,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1292,7 +1106,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); @@ -1329,7 +1142,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); @@ -1376,7 +1188,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1452,7 +1263,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1494,7 +1304,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1567,7 +1376,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1655,7 +1463,6 @@ describe('processAlerts', () => { alertLimit: 10, autoRecoverAlerts: true, flappingSettings: DISABLE_FLAPPING_SETTINGS, - maintenanceWindowIds: [], }); expect(activeAlerts).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.ts b/x-pack/plugins/alerting/server/lib/process_alerts.ts index 2fef33636441dd..5f145e97f66a57 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.ts @@ -24,7 +24,6 @@ interface ProcessAlertsOpts< autoRecoverAlerts: boolean; startedAt?: string | null; flappingSettings: RulesSettingsFlappingProperties; - maintenanceWindowIds: string[]; } interface ProcessAlertsResult< State extends AlertInstanceState, @@ -52,7 +51,6 @@ export function processAlerts< alertLimit, autoRecoverAlerts, flappingSettings, - maintenanceWindowIds, startedAt, }: ProcessAlertsOpts): ProcessAlertsResult< State, @@ -67,7 +65,6 @@ export function processAlerts< previouslyRecoveredAlerts, alertLimit, flappingSettings, - maintenanceWindowIds, startedAt ) : processAlertsHelper( @@ -76,7 +73,6 @@ export function processAlerts< previouslyRecoveredAlerts, autoRecoverAlerts, flappingSettings, - maintenanceWindowIds, startedAt ); } @@ -92,7 +88,6 @@ function processAlertsHelper< previouslyRecoveredAlerts: Record>, autoRecoverAlerts: boolean, flappingSettings: RulesSettingsFlappingProperties, - maintenanceWindowIds: string[], startedAt?: string | null ): ProcessAlertsResult { const existingAlertIds = new Set(Object.keys(existingAlerts)); @@ -124,7 +119,6 @@ function processAlertsHelper< } updateAlertFlappingHistory(flappingSettings, newAlerts[id], true); } - newAlerts[id].setMaintenanceWindowIds(maintenanceWindowIds); } else { // this alert did exist in previous run // calculate duration to date for active alerts @@ -188,7 +182,6 @@ function processAlertsLimitReached< previouslyRecoveredAlerts: Record>, alertLimit: number, flappingSettings: RulesSettingsFlappingProperties, - maintenanceWindowIds: string[], startedAt?: string | null ): ProcessAlertsResult { const existingAlertIds = new Set(Object.keys(existingAlerts)); @@ -258,8 +251,6 @@ function processAlertsLimitReached< updateAlertFlappingHistory(flappingSettings, newAlerts[id], true); } - newAlerts[id].setMaintenanceWindowIds(maintenanceWindowIds); - if (!hasCapacityForNewAlerts()) { break; } diff --git a/x-pack/plugins/alerting/server/maintenance_window_client/maintenance_window_client.ts b/x-pack/plugins/alerting/server/maintenance_window_client/maintenance_window_client.ts index 60954d6d0a6d33..2c5e6f417ff3da 100644 --- a/x-pack/plugins/alerting/server/maintenance_window_client/maintenance_window_client.ts +++ b/x-pack/plugins/alerting/server/maintenance_window_client/maintenance_window_client.ts @@ -85,6 +85,6 @@ export class MaintenanceWindowClient { public bulkGet = ( params: BulkGetMaintenanceWindowsParams ): Promise => bulkGetMaintenanceWindows(this.context, params); - public getActiveMaintenanceWindows = (): Promise => - getActiveMaintenanceWindows(this.context); + public getActiveMaintenanceWindows = (cacheIntervalMs?: number): Promise => + getActiveMaintenanceWindows(this.context, cacheIntervalMs); } diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index ac4e2f4c4f8b21..33cbccbaf8c0ce 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -168,16 +168,17 @@ const createRuleExecutorServicesMock = < done: jest.fn().mockReturnValue(alertFactoryMockDone), }, alertsClient: publicAlertsClientMock.create(), + getDataViews: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()), + getMaintenanceWindowIds: jest.fn().mockResolvedValue([]), + getSearchSourceClient: jest.fn().mockResolvedValue(searchSourceCommonMock), + ruleMonitoringService: createRuleMonitoringServiceMock(), savedObjectsClient: savedObjectsClientMock.create(), - uiSettingsClient: uiSettingsServiceMock.createClient(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - shouldWriteAlerts: () => true, - shouldStopExecution: () => true, search: createAbortableSearchServiceMock(), - getSearchSourceClient: jest.fn().mockResolvedValue(searchSourceCommonMock), - ruleMonitoringService: createRuleMonitoringServiceMock(), share: createShareStartMock(), - getDataViews: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()), + shouldStopExecution: () => true, + shouldWriteAlerts: () => true, + uiSettingsClient: uiSettingsServiceMock.createClient(), }; }; export type RuleExecutorServicesMock = ReturnType; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 5619b266c85bd4..ba1eff19ff4266 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -116,6 +116,7 @@ import { ConnectorAdapter, ConnectorAdapterParams } from './connector_adapters/t import { DataStreamAdapter, getDataStreamAdapter } from './alerts_service/lib/data_stream_adapter'; import { createGetAlertIndicesAliasFn, GetAlertIndicesAlias } from './lib'; import { BackfillClient } from './backfill_client/backfill_client'; +import { MaintenanceWindowsService } from './task_runner/maintenance_windows'; export const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -605,10 +606,14 @@ export class AlertingPlugin { encryptedSavedObjectsClient, eventLogger: this.eventLogger!, executionContext: core.executionContext, - getMaintenanceWindowClientWithRequest, getRulesClientWithRequest, kibanaBaseUrl: this.kibanaBaseUrl, logger, + maintenanceWindowsService: new MaintenanceWindowsService({ + cacheInterval: this.config.rulesSettings.cacheInterval, + getMaintenanceWindowClientWithRequest, + logger, + }), maxAlerts: this.config.rules.run.alerts.max, maxEphemeralActionsPerRule: this.config.maxEphemeralActionsPerAlert, ruleTypeRegistry: this.ruleTypeRegistry!, 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 f90420ba850362..59e7e9d548f5cb 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 @@ -30,7 +30,6 @@ import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/us import { AdHocTaskRunner } from './ad_hoc_task_runner'; 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 { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { @@ -94,6 +93,7 @@ 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'; +import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock'; const UUID = '5f6aa57d-3e22-484e-bae8-cbed868f4d28'; @@ -142,7 +142,7 @@ const dataViewsMock = { const elasticsearchService = elasticsearchServiceMock.createInternalStart(); const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); -const maintenanceWindowClient = maintenanceWindowClientMock.create(); +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); const rulesClient = rulesClientMock.create(); const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); const rulesSettingsService = rulesSettingsServiceMock.create(); @@ -165,7 +165,7 @@ const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType encryptedSavedObjectsClient, eventLogger: eventLoggerMock.create(), executionContext: executionContextServiceMock.createInternalStartContract(), - getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + maintenanceWindowsService, getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), kibanaBaseUrl: 'https://localhost:5601', logger, @@ -459,7 +459,6 @@ describe('Ad Hoc Task Runner', () => { expect(call.rule.ruleTypeName).toBe('My test rule'); expect(call.rule.actions).toEqual([]); expect(call.flappingSettings).toEqual(DEFAULT_FLAPPING_SETTINGS); - expect(call.maintenanceWindowIds).toBe(undefined); expect(clusterClient.bulk).toHaveBeenCalledWith({ index: '.alerts-test.alerts-default', diff --git a/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.ts b/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.ts index 6c6ad31fb6d89c..d126151030672b 100644 --- a/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.ts @@ -180,6 +180,7 @@ export class AdHocTaskRunner implements CancellableTask { const ruleTypeRunnerContext = { alertingEventLogger: this.alertingEventLogger, namespace: this.context.spaceIdToNamespace(adHocRunData.spaceId), + request: fakeRequest, ruleId: rule.id, ruleLogPrefix: ruleLabel, ruleRunMetricsStore, diff --git a/x-pack/plugins/alerting/server/task_runner/get_maintenance_windows.test.ts b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/get_maintenance_windows.test.ts similarity index 90% rename from x-pack/plugins/alerting/server/task_runner/get_maintenance_windows.test.ts rename to x-pack/plugins/alerting/server/task_runner/maintenance_windows/get_maintenance_windows.test.ts index f8a0508346a242..a6479fba828f4a 100644 --- a/x-pack/plugins/alerting/server/task_runner/get_maintenance_windows.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/get_maintenance_windows.test.ts @@ -7,19 +7,19 @@ import { CoreKibanaRequest } from '@kbn/core-http-router-server-internal'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; -import { maintenanceWindowCategoryIdTypes } from '../application/maintenance_window/constants'; -import { getMockMaintenanceWindow } from '../data/maintenance_window/test_helpers'; -import { maintenanceWindowClientMock } from '../maintenance_window_client.mock'; -import { MaintenanceWindowStatus } from '../types'; -import { MaintenanceWindow } from '../application/maintenance_window/types'; -import { mockedRawRuleSO, mockedRule } from './fixtures'; +import { maintenanceWindowCategoryIdTypes } from '../../application/maintenance_window/constants'; +import { getMockMaintenanceWindow } from '../../data/maintenance_window/test_helpers'; +import { maintenanceWindowClientMock } from '../../maintenance_window_client.mock'; +import { MaintenanceWindowStatus } from '../../types'; +import { MaintenanceWindow } from '../../application/maintenance_window/types'; +import { mockedRawRuleSO, mockedRule } from '../fixtures'; import { filterMaintenanceWindows, filterMaintenanceWindowsIds, getMaintenanceWindows, } from './get_maintenance_windows'; -import { getFakeKibanaRequest } from './rule_loader'; -import { TaskRunnerContext } from './types'; +import { getFakeKibanaRequest } from '../rule_loader'; +import { TaskRunnerContext } from '../types'; import { FilterStateStore } from '@kbn/es-query'; const logger = loggingSystemMock.create().get(); @@ -64,8 +64,8 @@ describe('getMaintenanceWindows', () => { ); expect( await getMaintenanceWindows({ - context, fakeRequest, + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), logger, ruleTypeId, ruleTypeCategory: 'observability', @@ -98,8 +98,8 @@ describe('getMaintenanceWindows', () => { ); expect( await getMaintenanceWindows({ - context, fakeRequest, + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), logger, ruleTypeId, ruleTypeCategory: 'observability', @@ -139,8 +139,8 @@ describe('getMaintenanceWindows', () => { ); expect( await getMaintenanceWindows({ - context, fakeRequest, + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), logger, ruleTypeId, ruleTypeCategory: 'observability', @@ -153,8 +153,8 @@ describe('getMaintenanceWindows', () => { maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce([]); expect( await getMaintenanceWindows({ - context, fakeRequest, + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), logger, ruleTypeId, ruleTypeCategory: 'observability', @@ -169,8 +169,8 @@ describe('getMaintenanceWindows', () => { }); expect( await getMaintenanceWindows({ - context, fakeRequest, + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), logger, ruleTypeId, ruleTypeCategory: 'observability', diff --git a/x-pack/plugins/alerting/server/task_runner/get_maintenance_windows.ts b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/get_maintenance_windows.ts similarity index 52% rename from x-pack/plugins/alerting/server/task_runner/get_maintenance_windows.ts rename to x-pack/plugins/alerting/server/task_runner/maintenance_windows/get_maintenance_windows.ts index 9fbdd235f5d455..ffc880f5475aea 100644 --- a/x-pack/plugins/alerting/server/task_runner/get_maintenance_windows.ts +++ b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/get_maintenance_windows.ts @@ -6,16 +6,17 @@ */ import { KibanaRequest, Logger } from '@kbn/core/server'; -import { MaintenanceWindow } from '../application/maintenance_window/types'; -import { TaskRunnerContext } from './types'; +import { MaintenanceWindow } from '../../application/maintenance_window/types'; +import { MaintenanceWindowClientApi } from '../../types'; +import { withAlertingSpan } from '../lib'; interface GetMaintenanceWindowsOpts { - context: TaskRunnerContext; fakeRequest: KibanaRequest; + getMaintenanceWindowClientWithRequest(request: KibanaRequest): MaintenanceWindowClientApi; logger: Logger; + ruleId: string; ruleTypeId: string; ruleTypeCategory: string; - ruleId: string; } interface FilterMaintenanceWindowsOpts { @@ -55,29 +56,38 @@ export const filterMaintenanceWindowsIds = ({ export const getMaintenanceWindows = async ( opts: GetMaintenanceWindowsOpts ): Promise => { - const { context, fakeRequest, logger, ruleTypeId, ruleId, ruleTypeCategory } = opts; - const maintenanceWindowClient = context.getMaintenanceWindowClientWithRequest(fakeRequest); - - let activeMaintenanceWindows: MaintenanceWindow[] = []; - try { - activeMaintenanceWindows = await maintenanceWindowClient.getActiveMaintenanceWindows(); - } catch (err) { - logger.error( - `error getting active maintenance window for ${ruleTypeId}:${ruleId} ${err.message}` - ); - } + return await withAlertingSpan('alerting:load-maintenance-windows', async () => { + const { + getMaintenanceWindowClientWithRequest, + fakeRequest, + logger, + ruleTypeId, + ruleId, + ruleTypeCategory, + } = opts; + const maintenanceWindowClient = getMaintenanceWindowClientWithRequest(fakeRequest); - const maintenanceWindows = activeMaintenanceWindows.filter(({ categoryIds }) => { - // If category IDs array doesn't exist: allow all - if (!Array.isArray(categoryIds)) { - return true; - } - // If category IDs array exist: check category - if ((categoryIds as string[]).includes(ruleTypeCategory)) { - return true; + let activeMaintenanceWindows: MaintenanceWindow[] = []; + try { + activeMaintenanceWindows = await maintenanceWindowClient.getActiveMaintenanceWindows(); + } catch (err) { + logger.error( + `error getting active maintenance window for ${ruleTypeId}:${ruleId} ${err.message}` + ); } - return false; - }); - return maintenanceWindows; + const maintenanceWindows = activeMaintenanceWindows.filter(({ categoryIds }) => { + // If category IDs array doesn't exist: allow all + if (!Array.isArray(categoryIds)) { + return true; + } + // If category IDs array exist: check category + if ((categoryIds as string[]).includes(ruleTypeCategory)) { + return true; + } + return false; + }); + + return maintenanceWindows; + }); }; diff --git a/x-pack/plugins/alerting/server/task_runner/maintenance_windows/index.ts b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/index.ts new file mode 100644 index 00000000000000..4e8a32cf208ba6 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { filterMaintenanceWindows, filterMaintenanceWindowsIds } from './get_maintenance_windows'; +export { MaintenanceWindowsService } from './maintenance_windows_service'; diff --git a/x-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.mock.ts b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.mock.ts new file mode 100644 index 00000000000000..6ee652d693669e --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.mock.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +const createMaintenanceWindowsServiceMock = () => { + return jest.fn().mockImplementation(() => { + return { + getMaintenanceWindows: jest.fn(), + }; + }); +}; + +export const maintenanceWindowsServiceMock = { + create: createMaintenanceWindowsServiceMock(), +}; diff --git a/x-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.test.ts b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.test.ts new file mode 100644 index 00000000000000..e768eb8ec6efe7 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.test.ts @@ -0,0 +1,425 @@ +/* + * 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 { KibanaRequest } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { alertingEventLoggerMock } from '../../lib/alerting_event_logger/alerting_event_logger.mock'; +import { MaintenanceWindowsService } from './maintenance_windows_service'; +import { maintenanceWindowClientMock } from '../../maintenance_window_client.mock'; +import { getMockMaintenanceWindow } from '../../data/maintenance_window/test_helpers'; +import { MaintenanceWindowStatus } from '../../../common'; +import { MaintenanceWindowCategoryIds } from '../../../common/routes/maintenance_window/shared'; +import { FilterStateStore } from '@kbn/es-query'; + +const alertingEventLogger = alertingEventLoggerMock.create(); +const logger = loggingSystemMock.createLogger(); +const maintenanceWindowClient = maintenanceWindowClientMock.create(); +let fakeTimer: sinon.SinonFakeTimers; + +const maintenanceWindows = [ + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id1', + }, + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id2', + }, +]; + +const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), +} as unknown as KibanaRequest; + +describe('MaintenanceWindowsService', () => { + beforeAll(() => { + fakeTimer = sinon.useFakeTimers(new Date('2023-02-27T08:15:00.000Z')); + }); + + beforeEach(() => { + fakeTimer.reset(); + jest.clearAllMocks(); + }); + + afterAll(() => fakeTimer.restore()); + + test('should load maintenance windows if none in cache', async () => { + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(maintenanceWindows); + const maintenanceWindowsService = new MaintenanceWindowsService({ + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + logger, + }); + // @ts-ignore - accessing private variable + expect(maintenanceWindowsService.windows.get('default')).toBeUndefined(); + + const windows = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalledTimes(1); + + // @ts-ignore - accessing private variable + expect(maintenanceWindowsService.windows.get('default')).toEqual({ + lastUpdated: 1677485700000, + activeMaintenanceWindows: maintenanceWindows, + }); + + expect(windows.maintenanceWindows).toEqual(maintenanceWindows); + expect(windows.maintenanceWindowsWithoutScopedQueryIds).toEqual(['test-id1', 'test-id2']); + + expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith([ + 'test-id1', + 'test-id2', + ]); + }); + + test('should return empty arrays if fetch settings errors and nothing in cache', async () => { + maintenanceWindowClient.getActiveMaintenanceWindows.mockImplementationOnce(() => { + throw new Error('Test error'); + }); + const maintenanceWindowsService = new MaintenanceWindowsService({ + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + logger, + }); + // @ts-ignore - accessing private variable + expect(maintenanceWindowsService.windows.get('default')).toBeUndefined(); + + const windows = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalledTimes(1); + + // @ts-ignore - accessing private variable + expect(maintenanceWindowsService.windows.get('default')).toBeUndefined(); + + expect(windows.maintenanceWindows).toEqual([]); + expect(windows.maintenanceWindowsWithoutScopedQueryIds).toEqual([]); + + expect(alertingEventLogger.setMaintenanceWindowIds).not.toHaveBeenCalled(); + }); + + test('should fetch maintenance windows per space', async () => { + const newSpaceMW = [ + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-new-space-id2', + }, + ]; + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(maintenanceWindows); + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(newSpaceMW); + const maintenanceWindowsService = new MaintenanceWindowsService({ + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + logger, + }); + // @ts-ignore - accessing private variable + expect(maintenanceWindowsService.windows.get('default')).toBeUndefined(); + // @ts-ignore - accessing private variable + expect(maintenanceWindowsService.windows.get('new-space')).toBeUndefined(); + + const windowsDefault = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + const windowsNewSpace = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'new-space', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalledTimes(2); + + // @ts-ignore - accessing private variable + expect(maintenanceWindowsService.windows.get('default')).toEqual({ + lastUpdated: 1677485700000, + activeMaintenanceWindows: maintenanceWindows, + }); + // @ts-ignore - accessing private variable + expect(maintenanceWindowsService.windows.get('new-space')).toEqual({ + lastUpdated: 1677485700000, + activeMaintenanceWindows: newSpaceMW, + }); + + expect(windowsDefault.maintenanceWindows).toEqual(maintenanceWindows); + expect(windowsDefault.maintenanceWindowsWithoutScopedQueryIds).toEqual([ + 'test-id1', + 'test-id2', + ]); + + expect(windowsNewSpace.maintenanceWindows).toEqual(newSpaceMW); + expect(windowsNewSpace.maintenanceWindowsWithoutScopedQueryIds).toEqual(['test-new-space-id2']); + + expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith([ + 'test-id1', + 'test-id2', + ]); + }); + + test('should use cached windows if cache has not expired', async () => { + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(maintenanceWindows); + const maintenanceWindowsService = new MaintenanceWindowsService({ + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + logger, + }); + + const windows1 = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + fakeTimer.tick(30000); + const windows2 = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalledTimes(1); + + expect(windows1.maintenanceWindows).toEqual(maintenanceWindows); + expect(windows1.maintenanceWindowsWithoutScopedQueryIds).toEqual(['test-id1', 'test-id2']); + expect(windows1).toEqual(windows2); + + expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith([ + 'test-id1', + 'test-id2', + ]); + }); + + test('should refetch windows if cache has expired', async () => { + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(maintenanceWindows); + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce([ + maintenanceWindows[0], + ]); + const maintenanceWindowsService = new MaintenanceWindowsService({ + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + logger, + }); + + const windows1 = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + fakeTimer.tick(61000); + const windows2 = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalledTimes(2); + + expect(windows1.maintenanceWindows).toEqual(maintenanceWindows); + expect(windows1.maintenanceWindowsWithoutScopedQueryIds).toEqual(['test-id1', 'test-id2']); + expect(windows2.maintenanceWindows).toEqual([maintenanceWindows[0]]); + expect(windows2.maintenanceWindowsWithoutScopedQueryIds).toEqual(['test-id1']); + + expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith([ + 'test-id1', + 'test-id2', + ]); + }); + + test('should return cached windows if refetching throws an error', async () => { + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(maintenanceWindows); + maintenanceWindowClient.getActiveMaintenanceWindows.mockImplementationOnce(() => { + throw new Error('Test error'); + }); + const maintenanceWindowsService = new MaintenanceWindowsService({ + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + logger, + }); + + const windows1 = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + fakeTimer.tick(61000); + const windows2 = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'rule-category', + }); + + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalledTimes(2); + + expect(windows1.maintenanceWindows).toEqual(maintenanceWindows); + expect(windows1.maintenanceWindowsWithoutScopedQueryIds).toEqual(['test-id1', 'test-id2']); + expect(windows1).toEqual(windows2); + + expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith([ + 'test-id1', + 'test-id2', + ]); + }); + + test('should filter by rule category', async () => { + const mw = [ + { + ...maintenanceWindows[0], + categoryIds: ['observability', 'management'] as MaintenanceWindowCategoryIds, + }, + maintenanceWindows[1], + ]; + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(mw); + const maintenanceWindowsService = new MaintenanceWindowsService({ + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + logger, + }); + + const windows = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'securitySolution', + }); + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalledTimes(1); + + expect(windows.maintenanceWindows).toEqual([maintenanceWindows[1]]); + expect(windows.maintenanceWindowsWithoutScopedQueryIds).toEqual(['test-id2']); + + expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith(['test-id2']); + }); + + test('should not call alertingEventLogger.setMaintenanceWindowIds if all maintenance windows have scoped queries', async () => { + const mw = maintenanceWindows.map((window) => ({ + ...window, + scopedQuery: { + kql: "_id: '1234'", + filters: [ + { + meta: { + disabled: false, + negate: false, + alias: null, + key: 'kibana.alert.action_group', + field: 'kibana.alert.action_group', + params: { + query: 'test', + }, + type: 'phrase', + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + query: { + match_phrase: { + 'kibana.alert.action_group': 'test', + }, + }, + }, + ], + }, + })); + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(mw); + const maintenanceWindowsService = new MaintenanceWindowsService({ + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + logger, + }); + + const windows = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'securitySolution', + }); + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalledTimes(1); + + expect(windows.maintenanceWindows).toEqual(mw); + expect(windows.maintenanceWindowsWithoutScopedQueryIds).toEqual([]); + + expect(alertingEventLogger.setMaintenanceWindowIds).not.toHaveBeenCalled(); + }); + + test('should filter active maintenance windows on current time', async () => { + const mw = [ + { + ...getMockMaintenanceWindow(), + events: [ + { + gte: '2023-02-27T00:00:00.000Z', + lte: '2023-02-28T00:00:00.000Z', + }, + { + gte: '2023-03-01T00:00:00.000Z', + lte: '2023-03-02T00:00:00.000Z', + }, + ], + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id1', + }, + { + ...getMockMaintenanceWindow(), + events: [ + { + // maintenance window starts one minute in the future + gte: '2023-02-27T08:16:00.000Z', + lte: '2023-02-28T00:00:00.000Z', + }, + ], + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id2', + }, + ]; + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(mw); + const maintenanceWindowsService = new MaintenanceWindowsService({ + getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), + logger, + }); + + const windows = await maintenanceWindowsService.getMaintenanceWindows({ + request: fakeRequest, + spaceId: 'default', + eventLogger: alertingEventLogger, + ruleTypeCategory: 'securitySolution', + }); + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalledTimes(1); + + expect(windows.maintenanceWindows).toEqual([mw[0]]); + expect(windows.maintenanceWindowsWithoutScopedQueryIds).toEqual(['test-id1']); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.ts b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.ts new file mode 100644 index 00000000000000..4f1b62a61f88f7 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.ts @@ -0,0 +1,147 @@ +/* + * 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 { MaintenanceWindow } from '../../application/maintenance_window/types'; +import { filterMaintenanceWindowsIds } from './get_maintenance_windows'; +import { MaintenanceWindowClientApi } from '../../types'; +import { AlertingEventLogger } from '../../lib/alerting_event_logger/alerting_event_logger'; +import { withAlertingSpan } from '../lib'; + +export const DEFAULT_CACHE_INTERVAL_MS = 60000; // 1 minute cache + +interface MaintenanceWindowServiceOpts { + cacheInterval?: number; + getMaintenanceWindowClientWithRequest(request: KibanaRequest): MaintenanceWindowClientApi; + logger: Logger; +} + +interface MaintenanceWindowData { + maintenanceWindows: MaintenanceWindow[]; + maintenanceWindowsWithoutScopedQueryIds: string[]; +} + +interface LoadMaintenanceWindowsOpts { + request: KibanaRequest; + spaceId: string; +} + +type GetMaintenanceWindowsOpts = LoadMaintenanceWindowsOpts & { + eventLogger: AlertingEventLogger; + ruleTypeCategory: string; +}; + +interface LastUpdatedWindows { + lastUpdated: number; + activeMaintenanceWindows: MaintenanceWindow[]; +} + +export class MaintenanceWindowsService { + private cacheIntervalMs = DEFAULT_CACHE_INTERVAL_MS; + + private windows: Map = new Map(); + + constructor(private readonly options: MaintenanceWindowServiceOpts) { + if (options.cacheInterval) { + this.cacheIntervalMs = options.cacheInterval; + } + } + + public async getMaintenanceWindows( + opts: GetMaintenanceWindowsOpts + ): Promise { + const activeMaintenanceWindows = await this.loadMaintenanceWindows({ + request: opts.request, + spaceId: opts.spaceId, + }); + + // Filter maintenance windows on current time + const now = Date.now(); + const currentlyActiveMaintenanceWindows = activeMaintenanceWindows.filter((mw) => { + return mw.events.some((event) => { + return new Date(event.gte).getTime() <= now && new Date(event.lte).getTime; + }); + }); + + // Only look at maintenance windows for this rule category + const maintenanceWindows = currentlyActiveMaintenanceWindows.filter(({ categoryIds }) => { + // If category IDs array doesn't exist: allow all + if (!Array.isArray(categoryIds)) { + return true; + } + // If category IDs array exist: check category + if ((categoryIds as string[]).includes(opts.ruleTypeCategory)) { + return true; + } + return false; + }); + + // Set the event log MW Id field the first time with MWs without scoped queries + const maintenanceWindowsWithoutScopedQueryIds = filterMaintenanceWindowsIds({ + maintenanceWindows, + withScopedQuery: false, + }); + + if (maintenanceWindowsWithoutScopedQueryIds.length) { + opts.eventLogger.setMaintenanceWindowIds(maintenanceWindowsWithoutScopedQueryIds); + } + + return { maintenanceWindows, maintenanceWindowsWithoutScopedQueryIds }; + } + + private async loadMaintenanceWindows( + opts: LoadMaintenanceWindowsOpts + ): Promise { + const now = Date.now(); + if (this.windows.has(opts.spaceId)) { + const windowsFromLastUpdate = this.windows.get(opts.spaceId)!; + const lastUpdated = new Date(windowsFromLastUpdate.lastUpdated).getTime(); + + if (now - lastUpdated >= this.cacheIntervalMs) { + // cache expired, refetch settings + try { + return await this.fetchMaintenanceWindows(opts.request, opts.spaceId, now); + } catch (err) { + // return cached settings on error + this.options.logger.debug( + `Failed to fetch maintenance windows after cache expiration, using cached windows: ${err.message}` + ); + return windowsFromLastUpdate.activeMaintenanceWindows; + } + } else { + return windowsFromLastUpdate.activeMaintenanceWindows; + } + } else { + // nothing in cache, fetch settings + try { + return await this.fetchMaintenanceWindows(opts.request, opts.spaceId, now); + } catch (err) { + // return default settings on error + this.options.logger.debug(`Failed to fetch initial maintenance windows: ${err.message}`); + return []; + } + } + } + + private async fetchMaintenanceWindows( + request: KibanaRequest, + spaceId: string, + now: number + ): Promise { + return await withAlertingSpan('alerting:load-maintenance-windows', async () => { + const maintenanceWindowClient = this.options.getMaintenanceWindowClientWithRequest(request); + const activeMaintenanceWindows = await maintenanceWindowClient.getActiveMaintenanceWindows( + this.cacheIntervalMs + ); + this.windows.set(spaceId, { + lastUpdated: now, + activeMaintenanceWindows, + }); + return activeMaintenanceWindows; + }); + } +} diff --git a/x-pack/plugins/alerting/server/task_runner/rule_type_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/rule_type_runner.test.ts index e839887a6122c9..1d218e50f927b0 100644 --- a/x-pack/plugins/alerting/server/task_runner/rule_type_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/rule_type_runner.test.ts @@ -29,11 +29,14 @@ import { TaskStatus, } from '@kbn/task-manager-plugin/server'; import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running'; +import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock'; +import { KibanaRequest } from '@kbn/core/server'; const alertingEventLogger = alertingEventLoggerMock.create(); const alertsClient = alertsClientMock.create(); const dataViews = dataViewPluginMocks.createStartContract(); const logger = loggingSystemMock.create().get(); +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); const publicRuleMonitoringService = publicRuleMonitoringServiceMock.create(); const publicRuleResultService = publicRuleResultServiceMock.create(); const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); @@ -43,6 +46,22 @@ const wrappedScopedClusterClient = wrappedScopedClusterClientMock.create(); const getDataViews = jest.fn().mockResolvedValue(dataViews); const getWrappedSearchSourceClient = jest.fn(); +const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), +} as unknown as KibanaRequest; + const timer = new TaskRunnerTimer({ logger }); const ruleType: jest.Mocked< NormalizedRuleType<{}, {}, { foo: string }, {}, {}, 'default', 'recovered', {}> @@ -162,8 +181,10 @@ describe('RuleTypeRunner', () => { const { state, error, stackTrace } = await ruleTypeRunner.run({ context: { alertingEventLogger, + maintenanceWindowsService, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + request: fakeRequest, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -193,6 +214,7 @@ describe('RuleTypeRunner', () => { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), getDataViews: expect.any(Function), + getMaintenanceWindowIds: expect.any(Function), ruleMonitoringService: publicRuleMonitoringService, ruleResultService: publicRuleResultService, savedObjectsClient, @@ -247,14 +269,12 @@ describe('RuleTypeRunner', () => { expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled(); expect(alertsClient.processAlerts).toHaveBeenCalledWith({ flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], alertDelay: 0, ruleRunMetricsStore, }); - expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]); + expect(alertsClient.persistAlerts).toHaveBeenCalled(); expect(alertingEventLogger.setMaintenanceWindowIds).not.toHaveBeenCalled(); expect(alertsClient.logAlerts).toHaveBeenCalledWith({ - eventLogger: alertingEventLogger, ruleRunMetricsStore, shouldLogAlerts: true, }); @@ -269,6 +289,8 @@ describe('RuleTypeRunner', () => { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + request: fakeRequest, + maintenanceWindowsService, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -298,6 +320,7 @@ describe('RuleTypeRunner', () => { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), getDataViews: expect.any(Function), + getMaintenanceWindowIds: expect.any(Function), ruleMonitoringService: publicRuleMonitoringService, ruleResultService: publicRuleResultService, savedObjectsClient, @@ -352,14 +375,12 @@ describe('RuleTypeRunner', () => { expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled(); expect(alertsClient.processAlerts).toHaveBeenCalledWith({ flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], alertDelay: 0, ruleRunMetricsStore, }); - expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]); + expect(alertsClient.persistAlerts).toHaveBeenCalled(); expect(alertingEventLogger.setMaintenanceWindowIds).not.toHaveBeenCalled(); expect(alertsClient.logAlerts).toHaveBeenCalledWith({ - eventLogger: alertingEventLogger, ruleRunMetricsStore, shouldLogAlerts: true, }); @@ -377,6 +398,8 @@ describe('RuleTypeRunner', () => { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + maintenanceWindowsService, + request: fakeRequest, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -413,14 +436,12 @@ describe('RuleTypeRunner', () => { expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled(); expect(alertsClient.processAlerts).toHaveBeenCalledWith({ flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], alertDelay: 0, ruleRunMetricsStore, }); - expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]); + expect(alertsClient.persistAlerts).toHaveBeenCalled(); expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith(['abc']); expect(alertsClient.logAlerts).toHaveBeenCalledWith({ - eventLogger: alertingEventLogger, ruleRunMetricsStore, shouldLogAlerts: true, }); @@ -438,6 +459,8 @@ describe('RuleTypeRunner', () => { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + request: fakeRequest, + maintenanceWindowsService, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -467,6 +490,7 @@ describe('RuleTypeRunner', () => { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), getDataViews: expect.any(Function), + getMaintenanceWindowIds: expect.any(Function), ruleMonitoringService: publicRuleMonitoringService, ruleResultService: publicRuleResultService, savedObjectsClient, @@ -537,6 +561,8 @@ describe('RuleTypeRunner', () => { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + maintenanceWindowsService, + request: fakeRequest, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -566,6 +592,7 @@ describe('RuleTypeRunner', () => { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), getDataViews: expect.any(Function), + getMaintenanceWindowIds: expect.any(Function), ruleMonitoringService: publicRuleMonitoringService, ruleResultService: publicRuleResultService, savedObjectsClient, @@ -636,6 +663,8 @@ describe('RuleTypeRunner', () => { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + maintenanceWindowsService, + request: fakeRequest, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -671,7 +700,9 @@ describe('RuleTypeRunner', () => { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + request: fakeRequest, ruleId: RULE_ID, + maintenanceWindowsService, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, spaceId: 'default', @@ -700,6 +731,7 @@ describe('RuleTypeRunner', () => { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), getDataViews: expect.any(Function), + getMaintenanceWindowIds: expect.any(Function), ruleMonitoringService: publicRuleMonitoringService, ruleResultService: publicRuleResultService, savedObjectsClient, @@ -758,13 +790,11 @@ describe('RuleTypeRunner', () => { expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled(); expect(alertsClient.processAlerts).toHaveBeenCalledWith({ flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], alertDelay: 0, ruleRunMetricsStore, }); - expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]); + expect(alertsClient.persistAlerts).toHaveBeenCalled(); expect(alertsClient.logAlerts).toHaveBeenCalledWith({ - eventLogger: alertingEventLogger, ruleRunMetricsStore, shouldLogAlerts: true, }); @@ -783,7 +813,9 @@ describe('RuleTypeRunner', () => { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + request: fakeRequest, ruleId: RULE_ID, + maintenanceWindowsService, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, spaceId: 'default', @@ -812,6 +844,7 @@ describe('RuleTypeRunner', () => { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), getDataViews: expect.any(Function), + getMaintenanceWindowIds: expect.any(Function), ruleMonitoringService: publicRuleMonitoringService, ruleResultService: publicRuleResultService, savedObjectsClient, @@ -870,13 +903,11 @@ describe('RuleTypeRunner', () => { expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled(); expect(alertsClient.processAlerts).toHaveBeenCalledWith({ flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], alertDelay: 0, ruleRunMetricsStore, }); - expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]); + expect(alertsClient.persistAlerts).toHaveBeenCalled(); expect(alertsClient.logAlerts).toHaveBeenCalledWith({ - eventLogger: alertingEventLogger, ruleRunMetricsStore, shouldLogAlerts: true, }); @@ -895,6 +926,8 @@ describe('RuleTypeRunner', () => { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + request: fakeRequest, + maintenanceWindowsService, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -925,6 +958,7 @@ describe('RuleTypeRunner', () => { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), getDataViews: expect.any(Function), + getMaintenanceWindowIds: expect.any(Function), ruleMonitoringService: publicRuleMonitoringService, ruleResultService: publicRuleResultService, savedObjectsClient, @@ -976,7 +1010,6 @@ describe('RuleTypeRunner', () => { expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled(); expect(alertsClient.processAlerts).toHaveBeenCalledWith({ flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], alertDelay: 0, ruleRunMetricsStore, }); @@ -996,8 +1029,10 @@ describe('RuleTypeRunner', () => { context: { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, + request: fakeRequest, queryDelaySec: 0, ruleId: RULE_ID, + maintenanceWindowsService, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, spaceId: 'default', @@ -1027,6 +1062,7 @@ describe('RuleTypeRunner', () => { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), getDataViews: expect.any(Function), + getMaintenanceWindowIds: expect.any(Function), ruleMonitoringService: publicRuleMonitoringService, ruleResultService: publicRuleResultService, savedObjectsClient, @@ -1078,11 +1114,10 @@ describe('RuleTypeRunner', () => { expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled(); expect(alertsClient.processAlerts).toHaveBeenCalledWith({ flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], alertDelay: 0, ruleRunMetricsStore, }); - expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]); + expect(alertsClient.persistAlerts).toHaveBeenCalled(); expect(alertsClient.logAlerts).not.toHaveBeenCalled(); }); @@ -1099,6 +1134,8 @@ describe('RuleTypeRunner', () => { alertingEventLogger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySec: 0, + request: fakeRequest, + maintenanceWindowsService, ruleId: RULE_ID, ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`, ruleRunMetricsStore, @@ -1128,6 +1165,7 @@ describe('RuleTypeRunner', () => { services: { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), + getMaintenanceWindowIds: expect.any(Function), getDataViews: expect.any(Function), ruleMonitoringService: publicRuleMonitoringService, ruleResultService: publicRuleResultService, @@ -1180,13 +1218,11 @@ describe('RuleTypeRunner', () => { expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled(); expect(alertsClient.processAlerts).toHaveBeenCalledWith({ flappingSettings: DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: [], alertDelay: 0, ruleRunMetricsStore, }); - expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]); + expect(alertsClient.persistAlerts).toHaveBeenCalled(); expect(alertsClient.logAlerts).toHaveBeenCalledWith({ - eventLogger: alertingEventLogger, ruleRunMetricsStore, shouldLogAlerts: true, }); diff --git a/x-pack/plugins/alerting/server/task_runner/rule_type_runner.ts b/x-pack/plugins/alerting/server/task_runner/rule_type_runner.ts index 301057d405e9af..c10871613ea20e 100644 --- a/x-pack/plugins/alerting/server/task_runner/rule_type_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/rule_type_runner.ts @@ -15,7 +15,6 @@ import { } from '@kbn/task-manager-plugin/server'; import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running'; import { IAlertsClient } from '../alerts_client/types'; -import { MaintenanceWindow } from '../application/maintenance_window/types'; import { ErrorWithReason } from '../lib'; import { getTimeRange } from '../lib/get_time_range'; import { NormalizedRuleType } from '../rule_type_registry'; @@ -89,8 +88,6 @@ interface RunOpts< nowDate?: string ) => { dateStart: string; dateEnd: string }; }; - maintenanceWindows?: MaintenanceWindow[]; - maintenanceWindowsWithoutScopedQueryIds?: string[]; rule: RuleData; ruleType: NormalizedRuleType< Params, @@ -147,8 +144,6 @@ export class RuleTypeRunner< alertsClient, executionId, executorServices, - maintenanceWindows = [], - maintenanceWindowsWithoutScopedQueryIds = [], rule, ruleType, startedAt, @@ -221,6 +216,27 @@ export class RuleTypeRunner< services: { alertFactory: alertsClient.factory(), alertsClient: alertsClient.client(), + getDataViews: executorServices.getDataViews, + getMaintenanceWindowIds: async () => { + if (context.maintenanceWindowsService) { + const { maintenanceWindowsWithoutScopedQueryIds } = + await context.maintenanceWindowsService.getMaintenanceWindows({ + eventLogger: context.alertingEventLogger, + request: context.request, + ruleTypeCategory: ruleType.category, + spaceId: context.spaceId, + }); + return maintenanceWindowsWithoutScopedQueryIds ?? []; + } + return []; + }, + getSearchSourceClient: async () => { + if (!wrappedSearchSourceClient) { + wrappedSearchSourceClient = + await executorServices.getWrappedSearchSourceClient(); + } + return wrappedSearchSourceClient.searchSourceClient; + }, ruleMonitoringService: executorServices.ruleMonitoringService, ruleResultService: executorServices.ruleResultService, savedObjectsClient: executorServices.savedObjectsClient, @@ -230,14 +246,6 @@ export class RuleTypeRunner< shouldWriteAlerts: () => this.shouldLogAndScheduleActionsForAlerts(ruleType.cancelAlertsOnRuleTimeout), uiSettingsClient: executorServices.uiSettingsClient, - getDataViews: executorServices.getDataViews, - getSearchSourceClient: async () => { - if (!wrappedSearchSourceClient) { - wrappedSearchSourceClient = - await executorServices.getWrappedSearchSourceClient(); - } - return wrappedSearchSourceClient.searchSourceClient; - }, }, params: validatedParams, state: ruleTypeState as RuleState, @@ -270,10 +278,6 @@ export class RuleTypeRunner< }, logger: this.options.logger, flappingSettings: context.flappingSettings ?? DEFAULT_FLAPPING_SETTINGS, - // passed in so the rule registry knows about maintenance windows - ...(maintenanceWindowsWithoutScopedQueryIds.length - ? { maintenanceWindowIds: maintenanceWindowsWithoutScopedQueryIds } - : {}), getTimeRange: (timeWindow) => getTimeRange({ logger: this.options.logger, @@ -333,9 +337,8 @@ export class RuleTypeRunner< await withAlertingSpan('alerting:process-alerts', () => this.options.timer.runWithTimer(TaskRunnerTimerSpan.ProcessAlerts, async () => { - alertsClient.processAlerts({ + await alertsClient.processAlerts({ flappingSettings: context.flappingSettings ?? DEFAULT_FLAPPING_SETTINGS, - maintenanceWindowIds: maintenanceWindowsWithoutScopedQueryIds, alertDelay: alertDelay?.active ?? 0, ruleRunMetricsStore: context.ruleRunMetricsStore, }); @@ -344,9 +347,7 @@ export class RuleTypeRunner< await withAlertingSpan('alerting:index-alerts-as-data', () => this.options.timer.runWithTimer(TaskRunnerTimerSpan.PersistAlerts, async () => { - const updateAlertsMaintenanceWindowResult = await alertsClient.persistAlerts( - maintenanceWindows - ); + const updateAlertsMaintenanceWindowResult = await alertsClient.persistAlerts(); // Set the event log MW ids again, this time including the ids that matched alerts with // scoped query @@ -362,7 +363,6 @@ export class RuleTypeRunner< ); alertsClient.logAlerts({ - eventLogger: context.alertingEventLogger, ruleRunMetricsStore: context.ruleRunMetricsStore, shouldLogAlerts: this.shouldLogAndScheduleActionsForAlerts( ruleType.cancelAlertsOnRuleTimeout 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 833393504fdeb1..438ffb3685e2a3 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 @@ -83,12 +83,10 @@ 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 { maintenanceWindowClientMock } from '../maintenance_window_client.mock'; import { alertsServiceMock } from '../alerts_service/alerts_service.mock'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; import { getMockMaintenanceWindow } from '../data/maintenance_window/test_helpers'; import { alertsClientMock } from '../alerts_client/alerts_client.mock'; -import { MaintenanceWindow } from '../application/maintenance_window/types'; import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running'; import { RuleResultService } from '../monitoring/rule_result_service'; @@ -97,6 +95,8 @@ 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'; +import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock'; +import { MaintenanceWindow } from '../application/maintenance_window/types'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -107,7 +107,6 @@ jest.mock('../lib/wrap_scoped_cluster_client', () => ({ })); jest.mock('../lib/alerting_event_logger/alerting_event_logger'); - jest.mock('../monitoring/rule_result_service'); jest.spyOn(getExecutorServicesModule, 'getExecutorServices'); @@ -120,6 +119,7 @@ const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const alertingEventLogger = alertingEventLoggerMock.create(); const alertsClient = alertsClientMock.create(); const ruleResultService = ruleResultServiceMock.create(); +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); describe('Task Runner', () => { let mockedTaskInstance: ConcreteTaskInstance; @@ -157,7 +157,6 @@ describe('Task Runner', () => { getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), } as DataViewsServerPluginStart; const alertsService = alertsServiceMock.create(); - const maintenanceWindowClient = maintenanceWindowClientMock.create(); const connectorAdapterRegistry = new ConnectorAdapterRegistry(); const rulesSettingsService = rulesSettingsServiceMock.create(); @@ -181,12 +180,10 @@ describe('Task Runner', () => { encryptedSavedObjectsClient, eventLogger: eventLoggerMock.create(), executionContext: executionContextServiceMock.createInternalStartContract(), - getMaintenanceWindowClientWithRequest: jest - .fn() - .mockReturnValue(maintenanceWindowClientMock.create()), getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), kibanaBaseUrl: 'https://localhost:5601', logger, + maintenanceWindowsService, maxAlerts: 1000, maxEphemeralActionsPerRule: 10, ruleTypeRegistry, @@ -235,7 +232,6 @@ describe('Task Runner', () => { }); savedObjectsService.getScopedClient.mockReturnValue(services.savedObjectsClient); elasticsearchService.client.asScoped.mockReturnValue(services.scopedClusterClient); - maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValue([]); taskRunnerFactoryInitializerParams.getRulesClientWithRequest.mockReturnValue(rulesClient); taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( actionsClient @@ -247,13 +243,14 @@ describe('Task Runner', () => { flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS, }); + maintenanceWindowsService.getMaintenanceWindows.mockResolvedValue({ + maintenanceWindows: [], + maintenanceWindowsWithoutScopedQueryIds: [], + }); ruleTypeRegistry.get.mockReturnValue(ruleType); taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => fn() ); - taskRunnerFactoryInitializerParams.getMaintenanceWindowClientWithRequest.mockReturnValue( - maintenanceWindowClient - ); mockedRuleTypeSavedObject.monitoring!.run.history = []; mockedRuleTypeSavedObject.monitoring!.run.calculated_metrics.success_ratio = 0; @@ -646,6 +643,21 @@ describe('Task Runner', () => { ); test('skips alert notification if there are active maintenance windows', async () => { + const mw = [ + { + ...getMockMaintenanceWindow(), + id: 'test-id-1', + } as MaintenanceWindow, + { + ...getMockMaintenanceWindow(), + id: 'test-id-2', + } as MaintenanceWindow, + ]; + const maintenanceWindowIds = ['test-id-1', 'test-id-2']; + maintenanceWindowsService.getMaintenanceWindows.mockResolvedValue({ + maintenanceWindows: mw, + maintenanceWindowsWithoutScopedQueryIds: maintenanceWindowIds, + }); taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); ruleType.executor.mockImplementation( @@ -672,29 +684,16 @@ describe('Task Runner', () => { }); expect(AlertingEventLogger).toHaveBeenCalledTimes(1); rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce([ - { - ...getMockMaintenanceWindow(), - id: 'test-id-1', - } as MaintenanceWindow, - { - ...getMockMaintenanceWindow(), - id: 'test-id-2', - } as MaintenanceWindow, - ]); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); await taskRunner.run(); expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); - const maintenanceWindowIds = ['test-id-1', 'test-id-2']; - testAlertingEventLogCalls({ activeAlerts: 1, newAlerts: 1, status: 'active', logAlert: 2, - maintenanceWindowIds, }); expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( 1, @@ -719,6 +718,18 @@ describe('Task Runner', () => { }); test('skips alert notification if active maintenance window contains the rule type category', async () => { + const mw = [ + { + ...getMockMaintenanceWindow(), + categoryIds: ['test'] as unknown as MaintenanceWindow['categoryIds'], + id: 'test-id-1', + } as MaintenanceWindow, + ]; + const maintenanceWindowIds = ['test-id-1']; + maintenanceWindowsService.getMaintenanceWindows.mockResolvedValue({ + maintenanceWindows: mw, + maintenanceWindowsWithoutScopedQueryIds: maintenanceWindowIds, + }); taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); ruleType.executor.mockImplementation( @@ -745,26 +756,16 @@ describe('Task Runner', () => { }); expect(AlertingEventLogger).toHaveBeenCalledTimes(1); rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce([ - { - ...getMockMaintenanceWindow(), - categoryIds: ['test'] as unknown as MaintenanceWindow['categoryIds'], - id: 'test-id-1', - } as MaintenanceWindow, - ]); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); await taskRunner.run(); expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); - const maintenanceWindowIds = ['test-id-1']; - testAlertingEventLogCalls({ activeAlerts: 1, newAlerts: 1, status: 'active', logAlert: 2, - maintenanceWindowIds, }); expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( 1, @@ -788,6 +789,10 @@ describe('Task Runner', () => { }); test('allows alert notification if active maintenance window does not contain the rule type category', async () => { + maintenanceWindowsService.getMaintenanceWindows.mockResolvedValue({ + maintenanceWindows: [], + maintenanceWindowsWithoutScopedQueryIds: [], + }); taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); ruleType.executor.mockImplementation( @@ -814,13 +819,6 @@ describe('Task Runner', () => { }); expect(AlertingEventLogger).toHaveBeenCalledTimes(1); rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce([ - { - ...getMockMaintenanceWindow(), - categoryIds: ['something-else'] as unknown as MaintenanceWindow['categoryIds'], - id: 'test-id-1', - } as MaintenanceWindow, - ]); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); await taskRunner.run(); @@ -855,67 +853,6 @@ describe('Task Runner', () => { expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); - test('should update alerts with maintenance window if scoped query matches said alerts', async () => { - taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); - taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); - - ruleType.executor.mockImplementation(async () => { - return { state: {} }; - }); - - const mockMaintenanceWindows = [ - { - ...getMockMaintenanceWindow(), - categoryIds: ['test'] as unknown as MaintenanceWindow['categoryIds'], - id: 'test-id-1', - } as unknown as MaintenanceWindow, - { - ...getMockMaintenanceWindow(), - categoryIds: ['test'] as unknown as MaintenanceWindow['categoryIds'], - scopedQuery: { - kql: "kibana.alert.rule.name: 'test123'", - filters: [], - dsl: '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"kibana.alert.rule.name":"test123"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}', - }, - id: 'test-id-2', - } as unknown as MaintenanceWindow, - ]; - - maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce( - mockMaintenanceWindows - ); - - alertsClient.persistAlerts.mockResolvedValue({ - alertIds: [], - maintenanceWindowIds: ['test-id-1', 'test-id-2'], - }); - - alertsService.createAlertsClient.mockImplementation(() => alertsClient); - - const taskRunner = new TaskRunner({ - ruleType, - taskInstance: mockedTaskInstance, - context: taskRunnerFactoryInitializerParams, - inMemoryMetrics, - internalSavedObjectsRepository, - }); - - expect(AlertingEventLogger).toHaveBeenCalledTimes(1); - rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); - await taskRunner.run(); - expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); - - expect(alertsClient.persistAlerts).toHaveBeenLastCalledWith(mockMaintenanceWindows); - - expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith(['test-id-1']); - expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith([ - 'test-id-1', - 'test-id-2', - ]); - }); - test.each(ephemeralTestParams)( 'skips firing actions for active alert if alert is muted %s', async (nameExtension, customTaskRunnerFactoryInitializerParams, enqueueFunction) => { 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 936ff2bc647978..e01dd73df7e583 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -64,8 +64,6 @@ import { RuleMonitoringService } from '../monitoring/rule_monitoring_service'; import { lastRunToRaw } from '../lib/last_run_status'; import { RuleRunningHandler } from './rule_running_handler'; import { RuleResultService } from '../monitoring/rule_result_service'; -import { MaintenanceWindow } from '../application/maintenance_window/types'; -import { filterMaintenanceWindowsIds, getMaintenanceWindows } from './get_maintenance_windows'; import { RuleTypeRunner } from './rule_type_runner'; import { initializeAlertsClient } from '../alerts_client'; import { createTaskRunnerLogger, withAlertingSpan, processRunResults } from './lib'; @@ -136,8 +134,6 @@ export class TaskRunner< private ruleMonitoring: RuleMonitoringService; private ruleRunning: RuleRunningHandler; private ruleResult: RuleResultService; - private maintenanceWindows: MaintenanceWindow[] = []; - private maintenanceWindowsWithoutScopedQueryIds: string[] = []; private ruleTypeRunner: RuleTypeRunner< Params, ExtractedParams, @@ -299,8 +295,10 @@ export class TaskRunner< const ruleTypeRunnerContext = { alertingEventLogger: this.alertingEventLogger, flappingSettings, + maintenanceWindowsService: this.context.maintenanceWindowsService, namespace: this.context.spaceIdToNamespace(spaceId), queryDelaySec: queryDelaySettings.delay, + request: fakeRequest, ruleId, ruleLogPrefix: ruleLabel, ruleRunMetricsStore, @@ -359,8 +357,6 @@ export class TaskRunner< alertsClient, executionId: this.executionId, executorServices, - maintenanceWindows: this.maintenanceWindows, - maintenanceWindowsWithoutScopedQueryIds: this.maintenanceWindowsWithoutScopedQueryIds, rule, ruleType: this.ruleType, startedAt: this.taskInstance.startedAt!, @@ -526,30 +522,6 @@ export class TaskRunner< // Set rule monitoring data this.ruleMonitoring.setMonitoring(runRuleParams.rule.monitoring); - // Load the maintenance windows - this.maintenanceWindows = await withAlertingSpan('alerting:load-maintenance-windows', () => - getMaintenanceWindows({ - context: this.context, - fakeRequest: runRuleParams.fakeRequest, - logger: this.logger, - ruleTypeId: this.ruleType.id, - ruleId, - ruleTypeCategory: this.ruleType.category, - }) - ); - - // Set the event log MW Id field the first time with MWs without scoped queries - this.maintenanceWindowsWithoutScopedQueryIds = filterMaintenanceWindowsIds({ - maintenanceWindows: this.maintenanceWindows, - withScopedQuery: false, - }); - - if (this.maintenanceWindowsWithoutScopedQueryIds.length) { - this.alertingEventLogger.setMaintenanceWindowIds( - this.maintenanceWindowsWithoutScopedQueryIds - ); - } - (async () => { try { await runRuleParams.rulesClient.clearExpiredSnoozes({ 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 cee8a2ea3b345d..c116230016e9be 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,7 @@ import { AlertInstanceContext, Rule, RuleAlertData, + MaintenanceWindowStatus, DEFAULT_FLAPPING_SETTINGS, DEFAULT_QUERY_DELAY_SETTINGS, } from '../types'; @@ -57,7 +58,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 { maintenanceWindowClientMock } from '../maintenance_window_client.mock'; import { alertsServiceMock } from '../alerts_service/alerts_service.mock'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { alertsClientMock } from '../alerts_client/alerts_client.mock'; @@ -104,6 +104,8 @@ import { import { backfillClientMock } from '../backfill_client/backfill_client.mock'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; import { createTaskRunnerLogger } from './lib'; +import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock'; +import { getMockMaintenanceWindow } from '../data/maintenance_window/test_helpers'; import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock'; jest.mock('uuid', () => ({ @@ -124,6 +126,7 @@ const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const alertingEventLogger = alertingEventLoggerMock.create(); const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); const ruleTypeWithAlerts: jest.Mocked = { ...ruleType, @@ -181,7 +184,6 @@ describe('Task Runner', () => { const mockAlertsClient = alertsClientMock.create(); const mockLegacyAlertsClient = legacyAlertsClientMock.create(); const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); - const maintenanceWindowClient = maintenanceWindowClientMock.create(); const connectorAdapterRegistry = new ConnectorAdapterRegistry(); const elasticsearchAndSOAvailability$ = new Subject(); @@ -205,10 +207,10 @@ describe('Task Runner', () => { encryptedSavedObjectsClient, eventLogger: eventLoggerMock.create(), executionContext: executionContextServiceMock.createInternalStartContract(), - getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient), getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), kibanaBaseUrl: 'https://localhost:5601', logger, + maintenanceWindowsService, maxAlerts: 1000, maxEphemeralActionsPerRule: 10, ruleTypeRegistry, @@ -236,7 +238,6 @@ describe('Task Runner', () => { }); savedObjectsService.getScopedClient.mockReturnValue(services.savedObjectsClient); elasticsearchService.client.asScoped.mockReturnValue(services.scopedClusterClient); - maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValue([]); taskRunnerFactoryInitializerParams.getRulesClientWithRequest.mockReturnValue(rulesClient); taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( actionsClient @@ -252,14 +253,31 @@ describe('Task Runner', () => { flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS, }); - taskRunnerFactoryInitializerParams.getMaintenanceWindowClientWithRequest.mockReturnValue( - maintenanceWindowClient - ); mockedRuleTypeSavedObject.monitoring!.run.history = []; mockedRuleTypeSavedObject.monitoring!.run.calculated_metrics.success_ratio = 0; alertingEventLogger.getStartAndDuration.mockImplementation(() => ({ start: new Date() })); (AlertingEventLogger as jest.Mock).mockImplementation(() => alertingEventLogger); + + maintenanceWindowsService.getMaintenanceWindows.mockReturnValue({ + maintenanceWindows: [ + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id1', + }, + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id2', + }, + ], + maintenanceWindowsWithoutScopedQueryIds: ['test-id1', 'test-id2'], + }); logger.get.mockImplementation(() => logger); ruleType.executor.mockResolvedValue({ state: {} }); }); @@ -309,8 +327,12 @@ describe('Task Runner', () => { await taskRunner.run(); expect(mockAlertsService.createAlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, logger: taskRunnerLogger, + maintenanceWindowsService, + request: expect.any(Object), ruleType: ruleTypeWithAlerts, + spaceId: 'default', namespace: 'default', rule: { alertDelay: 0, @@ -588,7 +610,7 @@ describe('Task Runner', () => { [ALERT_FLAPPING_HISTORY]: [true], [ALERT_INSTANCE_ID]: '1', [ALERT_SEVERITY_IMPROVING]: false, - [ALERT_MAINTENANCE_WINDOW_IDS]: [], + [ALERT_MAINTENANCE_WINDOW_IDS]: ['test-id1', 'test-id2'], [ALERT_RULE_CATEGORY]: 'My test rule', [ALERT_RULE_CONSUMER]: 'bar', [ALERT_RULE_EXECUTION_UUID]: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -666,8 +688,12 @@ describe('Task Runner', () => { { tags: ['1', 'test'] } ); expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, + request: expect.any(Object), logger: taskRunnerLogger, ruleType: ruleTypeWithAlerts, + maintenanceWindowsService, + spaceId: 'default', }); testCorrectAlertsClientUsed({ @@ -753,8 +779,12 @@ describe('Task Runner', () => { expect(mockAlertsService.createAlertsClient).not.toHaveBeenCalled(); expect(logger.error).not.toHaveBeenCalled(); expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({ + alertingEventLogger, + request: expect.any(Object), + spaceId: 'default', logger: taskRunnerLogger, ruleType: ruleTypeWithAlerts, + maintenanceWindowsService, }); testCorrectAlertsClientUsed({ @@ -844,12 +874,10 @@ describe('Task Runner', () => { lookBackWindow: 20, statusChangeThreshold: 4, }, - maintenanceWindowIds: [], ruleRunMetricsStore, }); expect(alertsClientToUse.logAlerts).toHaveBeenCalledWith({ - eventLogger: alertingEventLogger, ruleRunMetricsStore, shouldLogAlerts: true, }); 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 b9fb6284c3911c..3a6a9547fb9021 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 @@ -56,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 { maintenanceWindowClientMock } from '../maintenance_window_client.mock'; import { alertsServiceMock } from '../alerts_service/alerts_service.mock'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; @@ -64,6 +63,7 @@ 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'; +import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -85,6 +85,7 @@ const dataViewsMock = { getScriptedFieldsEnabled: jest.fn().mockReturnValue(true), } as DataViewsServerPluginStart; const alertsService = alertsServiceMock.create(); +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); describe('Task Runner Cancel', () => { let mockedTaskInstance: ConcreteTaskInstance; @@ -140,12 +141,10 @@ describe('Task Runner Cancel', () => { encryptedSavedObjectsClient, eventLogger: eventLoggerMock.create(), executionContext: executionContextServiceMock.createInternalStartContract(), - getMaintenanceWindowClientWithRequest: jest - .fn() - .mockReturnValue(maintenanceWindowClientMock.create()), getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), kibanaBaseUrl: 'https://localhost:5601', logger, + maintenanceWindowsService, maxAlerts: 1000, maxEphemeralActionsPerRule: 10, ruleTypeRegistry, @@ -187,11 +186,11 @@ describe('Task Runner Cancel', () => { flappingSettings: DEFAULT_FLAPPING_SETTINGS, queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS, }); - taskRunnerFactoryInitializerParams.getMaintenanceWindowClientWithRequest.mockReturnValue( - maintenanceWindowClientMock.create() - ); rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); - + maintenanceWindowsService.getMaintenanceWindows.mockReturnValue({ + maintenanceWindows: [], + maintenanceWindowsWithoutScopedQueryIds: [], + }); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); 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 d2e863ef865b26..855e8ee2050f84 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,17 +28,18 @@ 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 { 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'; +import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock'; const inMemoryMetrics = inMemoryMetricsMock.create(); const backfillClient = backfillClientMock.create(); const rulesSettingsService = rulesSettingsServiceMock.create(); +const maintenanceWindowsService = maintenanceWindowsServiceMock.create(); const executionContext = executionContextServiceMock.createSetupContract(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); @@ -117,12 +118,10 @@ describe('Task Runner Factory', () => { encryptedSavedObjectsClient: encryptedSavedObjectsPlugin.getClient(), eventLogger: eventLoggerMock.create(), executionContext, - getMaintenanceWindowClientWithRequest: jest - .fn() - .mockReturnValue(maintenanceWindowClientMock.create()), getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), kibanaBaseUrl: 'https://localhost:5601', logger: loggingSystemMock.create().get(), + maintenanceWindowsService, maxAlerts: 1000, maxEphemeralActionsPerRule: 10, ruleTypeRegistry: ruleTypeRegistryMock.create(), diff --git a/x-pack/plugins/alerting/server/task_runner/types.ts b/x-pack/plugins/alerting/server/task_runner/types.ts index 18bf53cdc60b91..8598987e12f9dd 100644 --- a/x-pack/plugins/alerting/server/task_runner/types.ts +++ b/x-pack/plugins/alerting/server/task_runner/types.ts @@ -45,7 +45,6 @@ import { ActionsConfigMap } from '../lib/get_actions_config_map'; import { NormalizedRuleType } from '../rule_type_registry'; import { CombinedSummarizedAlerts, - MaintenanceWindowClientApi, RawRule, RulesClientApi, RuleTypeRegistry, @@ -57,6 +56,7 @@ import { BackfillClient } from '../backfill_client/backfill_client'; import { ElasticsearchError } from '../lib'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; import { RulesSettingsService } from '../rules_settings'; +import { MaintenanceWindowsService } from './maintenance_windows'; export interface RuleTaskRunResult { state: RuleTaskState; @@ -140,8 +140,10 @@ export type Executable< export interface RuleTypeRunnerContext { alertingEventLogger: AlertingEventLogger; flappingSettings?: RulesSettingsFlappingProperties; + maintenanceWindowsService?: MaintenanceWindowsService; namespace?: string; queryDelaySec?: number; + request: KibanaRequest; ruleId: string; ruleLogPrefix: string; ruleRunMetricsStore: RuleRunMetricsStore; @@ -167,10 +169,10 @@ export interface TaskRunnerContext { encryptedSavedObjectsClient: EncryptedSavedObjectsClient; eventLogger: IEventLogger; executionContext: ExecutionContextStart; - getMaintenanceWindowClientWithRequest(request: KibanaRequest): MaintenanceWindowClientApi; getRulesClientWithRequest(request: KibanaRequest): RulesClientApi; kibanaBaseUrl: string | undefined; logger: Logger; + maintenanceWindowsService: MaintenanceWindowsService; maxAlerts: number; maxEphemeralActionsPerRule: number; ruleTypeRegistry: RuleTypeRegistry; diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 58404268efaa8f..b543177c16cfd3 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -102,27 +102,28 @@ export interface RuleExecutorServices< ActionGroupIds extends string = never, AlertData extends RuleAlertData = RuleAlertData > { - savedObjectsClient: SavedObjectsClientContract; - uiSettingsClient: IUiSettingsClient; - scopedClusterClient: IScopedClusterClient; + /** + * Only available when framework alerts are enabled and rule + * type has registered alert context with the framework with shouldWrite set to true + */ + alertsClient: PublicAlertsClient | null; /** * Deprecate alertFactory and remove when all rules are onboarded to * the alertsClient * @deprecated */ alertFactory: PublicAlertFactory; - /** - * Only available when framework alerts are enabled and rule - * type has registered alert context with the framework with shouldWrite set to true - */ - alertsClient: PublicAlertsClient | null; - shouldWriteAlerts: () => boolean; - shouldStopExecution: () => boolean; - ruleMonitoringService?: PublicRuleMonitoringService; - share: SharePluginStart; - ruleResultService?: PublicRuleResultService; getDataViews: () => Promise; + getMaintenanceWindowIds: () => Promise; getSearchSourceClient: () => Promise; + ruleMonitoringService?: PublicRuleMonitoringService; + ruleResultService?: PublicRuleResultService; + savedObjectsClient: SavedObjectsClientContract; + scopedClusterClient: IScopedClusterClient; + share: SharePluginStart; + shouldStopExecution: () => boolean; + shouldWriteAlerts: () => boolean; + uiSettingsClient: IUiSettingsClient; } export interface RuleExecutorOptions< @@ -145,7 +146,6 @@ export interface RuleExecutorOptions< state: State; namespace?: string; flappingSettings: RulesSettingsFlappingProperties; - maintenanceWindowIds?: string[]; getTimeRange: (timeWindow?: string) => GetTimeRangeResult; } diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts index 32090cf88390c8..6b6c85f9120f6a 100644 --- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/executor.test.ts @@ -181,6 +181,7 @@ describe('BurnRateRuleExecutor', () => { shouldStopExecution: jest.fn(), share: {} as SharePluginStart, getDataViews: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()), + getMaintenanceWindowIds: jest.fn().mockResolvedValue([]), }; }); diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index 46cbe2eb1eb29c..b895c49c14a5fe 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -1130,6 +1130,7 @@ describe('createLifecycleExecutor', () => { it('updates documents with maintenance window ids for newly firing alerts', async () => { const logger = loggerMock.create(); const ruleDataClientMock = createRuleDataClientMock(); + const executor = createLifecycleExecutor( logger, ruleDataClientMock @@ -1151,7 +1152,6 @@ describe('createLifecycleExecutor', () => { params: {}, state: { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }, logger, - maintenanceWindowIds, }) ); @@ -1288,7 +1288,6 @@ describe('createLifecycleExecutor', () => { trackedAlertsRecovered: {}, }, logger, - maintenanceWindowIds, }) ); @@ -1420,7 +1419,6 @@ describe('createLifecycleExecutor', () => { trackedAlertsRecovered: {}, }, logger, - maintenanceWindowIds, }) ); diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 4bd3b912ae67df..cdbdf56fabc514 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -130,10 +130,9 @@ export const createLifecycleExecutor = > ): Promise<{ state: WrappedLifecycleRuleState }> => { const { - services: { alertFactory, shouldWriteAlerts }, + services: { alertFactory, getMaintenanceWindowIds, shouldWriteAlerts }, state: previousState, flappingSettings, - maintenanceWindowIds, rule, } = options; @@ -217,6 +216,11 @@ export const createLifecycleExecutor = `[Rule Registry] Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStates.length} previous)` ); + // load maintenance window ids if there are new alerts + const maintenanceWindowIds: string[] = allAlertIds.length + ? await getMaintenanceWindowIds() + : []; + interface TrackedAlertData { indexName: string; fields: Partial; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index f8bcf1393f1e16..6dbc33b6664976 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -137,6 +137,7 @@ function createRule(shouldWriteAlerts: boolean = true) { savedObjectsClient: {} as any, scopedClusterClient: {} as any, search: {} as any, + getMaintenanceWindowIds: async () => [], getSearchSourceClient: async () => ({} as ISearchStartSearchSource), shouldStopExecution: () => false, shouldWriteAlerts: () => shouldWriteAlerts, diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts index 854cebc9b0bf9c..78c6238ce78860 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts @@ -51,7 +51,7 @@ export type BackendAlertWithSuppressionFields870 = Omit< export const ALERT_GROUP_INDEX = `${ALERT_NAMESPACE}.group.index` as const; -const augmentAlerts = ({ +const augmentAlerts = async ({ alerts, options, kibanaVersion, @@ -65,6 +65,9 @@ const augmentAlerts = ({ intendedTimestamp: Date | undefined; }) => { const commonRuleFields = getCommonAlertFields(options); + const maintenanceWindowIds: string[] = + alerts.length > 0 ? await options.services.getMaintenanceWindowIds() : []; + const currentDate = new Date(); const timestampOverrideOrCurrent = currentTimeOverride ?? currentDate; return alerts.map((alert) => { @@ -78,8 +81,8 @@ const augmentAlerts = ({ ? intendedTimestamp : timestampOverrideOrCurrent, [VERSION]: kibanaVersion, - ...(options?.maintenanceWindowIds?.length - ? { [ALERT_MAINTENANCE_WINDOW_IDS]: options.maintenanceWindowIds } + ...(maintenanceWindowIds.length + ? { [ALERT_MAINTENANCE_WINDOW_IDS]: maintenanceWindowIds } : {}), ...commonRuleFields, ...alert._source, @@ -311,7 +314,7 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper intendedTimestamp = options.startedAt; } - const augmentedAlerts = augmentAlerts({ + const augmentedAlerts = await augmentAlerts({ alerts: enrichedAlerts, options, kibanaVersion: ruleDataClient.kibanaVersion, @@ -575,7 +578,7 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper alertsWereTruncated = true; } - const augmentedAlerts = augmentAlerts({ + const augmentedAlerts = await augmentAlerts({ alerts: enrichedAlerts, options, kibanaVersion: ruleDataClient.kibanaVersion, diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts index 21ecfc40efeff8..357703c7b75219 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts @@ -39,7 +39,6 @@ export const createDefaultAlertExecutorOptions = < startedAt = new Date(), updatedAt = new Date(), shouldWriteAlerts = true, - maintenanceWindowIds, }: { alertId?: string; ruleName?: string; @@ -50,7 +49,6 @@ export const createDefaultAlertExecutorOptions = < startedAt?: Date; updatedAt?: Date; shouldWriteAlerts?: boolean; - maintenanceWindowIds?: string[]; }): RuleExecutorOptions => ({ startedAt, startedAtOverridden: false, @@ -78,17 +76,18 @@ export const createDefaultAlertExecutorOptions = < params, spaceId: 'SPACE_ID', services: { - alertsClient: null, alertFactory: alertsMock.createRuleExecutorServices() .alertFactory, + alertsClient: null, + getDataViews: async () => dataViewPluginMocks.createStartContract(), + getMaintenanceWindowIds: async () => ['test-id-1', 'test-id-2'], + getSearchSourceClient: async () => searchSourceCommonMock, savedObjectsClient: savedObjectsClientMock.create(), - uiSettingsClient: uiSettingsServiceMock.createClient(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - shouldWriteAlerts: () => shouldWriteAlerts, - shouldStopExecution: () => false, - getSearchSourceClient: async () => searchSourceCommonMock, share: {} as SharePluginStart, - getDataViews: async () => dataViewPluginMocks.createStartContract(), + shouldStopExecution: () => false, + shouldWriteAlerts: () => shouldWriteAlerts, + uiSettingsClient: uiSettingsServiceMock.createClient(), }, state, previousStartedAt: null, @@ -96,7 +95,6 @@ export const createDefaultAlertExecutorOptions = < executionId: 'b33f65d7-6e8b-4aae-8d20-c93613deb33f', logger, flappingSettings: DEFAULT_FLAPPING_SETTINGS, - ...(maintenanceWindowIds ? { maintenanceWindowIds } : {}), getTimeRange: () => { const date = new Date(Date.now()).toISOString(); return { dateStart: date, dateEnd: date }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 50542592aa1d1e..3b3656c82f06d4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -300,6 +300,7 @@ export const previewRulesRoute = ( abortController, searchSourceClient, }), + getMaintenanceWindowIds: async () => [], uiSettingsClient: coreContext.uiSettings.client, getDataViews: async () => dataViewsService, share, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index 5a30b5980b87e2..c3b748f5d18289 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -105,6 +105,7 @@ export const createRuleTypeMocks = ( alertWithPersistence: jest.fn(), logger: loggerMock, shouldWriteAlerts: () => true, + getMaintenanceWindowIds: jest.fn().mockResolvedValue([]), getDataViews: jest.fn().mockResolvedValue({ createDataViewLazy: jest.fn().mockResolvedValue({ getFields: jest.fn().mockResolvedValue({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create_test_data.ts index 8e6663ff1c295e..febba47484e224 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create_test_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create_test_data.ts @@ -15,6 +15,8 @@ export const END_DATE = '2020-01-01T00:00:00Z'; export const DOCUMENT_SOURCE = 'queryDataEndpointTests'; export const DOCUMENT_REFERENCE = '-na-'; +export const TEST_CACHE_EXPIRATION_TIME = 10000; + export async function createEsDocuments( es: Client, esTestIndexTool: ESTestIndexTool, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts index 5afc94fd00dc92..e25d64e5091019 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts @@ -8,6 +8,7 @@ import moment from 'moment'; import expect from '@kbn/expect'; import { get } from 'lodash'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; import { ES_TEST_INDEX_NAME, ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; @@ -21,6 +22,7 @@ import { resetRulesSettings, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { TEST_CACHE_EXPIRATION_TIME } from '../create_test_data'; const InstanceActions = new Set([ 'new-instance', @@ -1682,6 +1684,9 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .expect(200); objectRemover.add(space.id, window3.id, 'rules/maintenance_window', 'alerting', true); + // wait so cache expires + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); + const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -1742,13 +1747,21 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); }); - const actionsToCheck = [ - 'new-instance', - 'active-instance', - 'recovered-instance', - 'execute', - ]; + const executeEvents = events.filter((event) => event?.event?.action === 'execute'); + // the first execute event should not have any maintenance window ids because there were no alerts during the + // first execution + for (let i = 0; i < executeEvents.length; i++) { + if (i === 0) { + expect(executeEvents[i]?.kibana?.alert?.maintenance_window_ids).to.be(undefined); + } else { + const alertMaintenanceWindowIds = + executeEvents[i]?.kibana?.alert?.maintenance_window_ids?.sort(); + expect(alertMaintenanceWindowIds).eql([window1.id, window2.id].sort()); + } + } + + const actionsToCheck = ['new-instance', 'active-instance', 'recovered-instance']; events.forEach((event) => { if (actionsToCheck.includes(event?.event?.action || '')) { const alertMaintenanceWindowIds = @@ -1775,6 +1788,9 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .expect(200); objectRemover.add(space.id, window.id, 'rules/maintenance_window', 'alerting', true); + // wait so cache expires + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); + const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -1857,6 +1873,9 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); it('should generate expected events with a alertDelay', async () => { + // wait so cache expires so maintenance window from previous test will be cleared + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); + const ACTIVE_PATH = 'kibana.alert.rule.execution.metrics.alert_counts.active'; const NEW_PATH = 'kibana.alert.rule.execution.metrics.alert_counts.new'; const RECOVERED_PATH = 'kibana.alert.rule.execution.metrics.alert_counts.recovered'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts index 7637675d5afcde..86444543bde73e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { omit } from 'lodash'; - +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { Spaces } from '../../../scenarios'; import { getUrlPrefix, @@ -17,6 +17,7 @@ import { getEventLog, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { TEST_CACHE_EXPIRATION_TIME } from '../create_test_data'; // eslint-disable-next-line import/no-default-export export default function createGetAlertSummaryTests({ getService }: FtrProviderContext) { @@ -308,6 +309,9 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo true ); + // wait so cache expires + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); + // pattern of when the rule should fire const pattern = { alertA: [true, true, true, true], @@ -384,6 +388,9 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo describe('legacy', function () { this.tags('skipFIPS'); it('handles multi-alert status', async () => { + // wait so cache expires + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); + // pattern of when the alert should fire const pattern = { alertA: [true, true, true, true], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/test_helpers.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/test_helpers.ts index 8c3438f7152a5f..33b389d15823d3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/test_helpers.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/test_helpers.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import type { RetryService } from '@kbn/ftr-common-functional-services'; import type { IValidatedEvent } from '@kbn/event-log-plugin/server'; import type { Agent as SuperTestAgent } from 'supertest'; @@ -13,6 +14,7 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; import { Spaces } from '../../../scenarios'; +import { TEST_CACHE_EXPIRATION_TIME } from '../create_test_data'; export const createRule = async ({ actionId, @@ -109,6 +111,9 @@ export const createMaintenanceWindow = async ({ .expect(200); objectRemover.add(Spaces.space1.id, window.id, 'rules/maintenance_window', 'alerting', true); + + // wait so cache expires + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); return window; }; @@ -128,12 +133,15 @@ export const finishMaintenanceWindow = async ({ id: string; supertest: SuperTestAgent; }) => { - return supertest + await supertest .post( `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/maintenance_window/${id}/_finish` ) .set('kbn-xsrf', 'foo') .expect(200); + + // wait so cache expires + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); }; export const getRuleEvents = async ({ 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 c0c52ddc73b5a6..8833800f8215f0 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 @@ -48,6 +48,7 @@ import { ObjectRemover, TaskManagerDoc, } from '../../../../../common/lib'; +import { TEST_CACHE_EXPIRATION_TIME } from '../../create_test_data'; // eslint-disable-next-line import/no-default-export export default function createAlertsAsDataInstallResourcesTest({ getService }: FtrProviderContext) { @@ -89,7 +90,7 @@ 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); + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); 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 de1f4351e0fa88..bf2dd0e5706c55 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 @@ -20,6 +20,7 @@ import { ObjectRemover, TaskManagerDoc, } from '../../../../../common/lib'; +import { TEST_CACHE_EXPIRATION_TIME } from '../../create_test_data'; // eslint-disable-next-line import/no-default-export export default function createAlertsAsDataFlappingTest({ getService }: FtrProviderContext) { @@ -59,7 +60,7 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid }) .expect(200); // wait so cache expires - await setTimeoutAsync(10000); + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); const pattern = { alertA: [true, false, false, true, false, true, false, true, false].concat( @@ -192,7 +193,7 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid }) .expect(200); // wait so cache expires - await setTimeoutAsync(10000); + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); const pattern = { alertA: [true, false, false, true, false, true, false, true, false, true].concat( @@ -322,7 +323,7 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid }) .expect(200); // wait so cache expires - await setTimeoutAsync(10000); + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); const pattern = { alertA: [true, false, true, false, false, false, false, false, false], @@ -382,7 +383,7 @@ export default function createAlertsAsDataFlappingTest({ getService }: FtrProvid }) .expect(200); // wait so cache expires - await setTimeoutAsync(10000); + await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); const pattern = { alertA: [true, false, false, true, false, true, false, true, false].concat( diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts index 415e3e165cff35..9f6eebedd7e7c5 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts @@ -200,6 +200,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid services: { alertFactory: getMockAlertFactory(), shouldWriteAlerts: sinon.stub().returns(true), + getMaintenanceWindowIds: async () => [], }, flappingSettings: { enabled: false,