[8.x] [Response Ops][Alerting] Only load maintenance windows when there are alerts during rule execution and caching loaded maintenance windows (#192573) (#194191)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Response Ops][Alerting] Only load maintenance windows when there are
alerts during rule execution and caching loaded maintenance windows
(#192573)](https://github.com/elastic/kibana/pull/192573)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Ying
Mao","email":"ying.mao@elastic.co"},"sourceCommit":{"committedDate":"2024-09-26T12:59:36Z","message":"[Response
Ops][Alerting] Only load maintenance windows when there are alerts
during rule execution and caching loaded maintenance windows
(#192573)\n\nResolves
https://github.com/elastic/kibana/issues/184324\r\n\r\n##
Summary\r\n\r\nThis PR moves the loading of maintenance windows further
down in rule\r\nexecution so maintenance windows are only loaded when a
rule execution\r\ngenerates alerts. Also caches maintenance windows per
space to reduce\r\nthe number of requests.\r\n\r\n## To Verify\r\n\r\n1.
Add some logging
to\r\nx-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.ts\r\nto
indicate when windows are being fetched and when they're
returning\r\nfrom the cache.\r\n2. Create and run some rules in
different spaces with and without alerts\r\nto see that the maintenance
windows are only loaded when there are\r\nalerts and that the windows
are returned from the cache when the cache\r\nhas not
expired.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"93414a672c2767b035110fa2d811cc040af57727","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Alerting","release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-management","v8.16.0"],"number":192573,"url":"https://github.com/elastic/kibana/pull/192573","mergeCommit":{"message":"[Response
Ops][Alerting] Only load maintenance windows when there are alerts
during rule execution and caching loaded maintenance windows
(#192573)\n\nResolves
https://github.com/elastic/kibana/issues/184324\r\n\r\n##
Summary\r\n\r\nThis PR moves the loading of maintenance windows further
down in rule\r\nexecution so maintenance windows are only loaded when a
rule execution\r\ngenerates alerts. Also caches maintenance windows per
space to reduce\r\nthe number of requests.\r\n\r\n## To Verify\r\n\r\n1.
Add some logging
to\r\nx-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.ts\r\nto
indicate when windows are being fetched and when they're
returning\r\nfrom the cache.\r\n2. Create and run some rules in
different spaces with and without alerts\r\nto see that the maintenance
windows are only loaded when there are\r\nalerts and that the windows
are returned from the cache when the cache\r\nhas not
expired.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"93414a672c2767b035110fa2d811cc040af57727"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/192573","number":192573,"mergeCommit":{"message":"[Response
Ops][Alerting] Only load maintenance windows when there are alerts
during rule execution and caching loaded maintenance windows
(#192573)\n\nResolves
https://github.com/elastic/kibana/issues/184324\r\n\r\n##
Summary\r\n\r\nThis PR moves the loading of maintenance windows further
down in rule\r\nexecution so maintenance windows are only loaded when a
rule execution\r\ngenerates alerts. Also caches maintenance windows per
space to reduce\r\nthe number of requests.\r\n\r\n## To Verify\r\n\r\n1.
Add some logging
to\r\nx-pack/plugins/alerting/server/task_runner/maintenance_windows/maintenance_windows_service.ts\r\nto
indicate when windows are being fetched and when they're
returning\r\nfrom the cache.\r\n2. Create and run some rules in
different spaces with and without alerts\r\nto see that the maintenance
windows are only loaded when there are\r\nalerts and that the windows
are returned from the cache when the cache\r\nhas not
expired.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"93414a672c2767b035110fa2d811cc040af57727"}},{"branch":"8.x","label":"v8.16.0","labelRegex":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Ying Mao 2024-09-26 14:15:58 -04:00 committed by GitHub
parent 4a3d831695
commit 3358772208
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1627 additions and 642 deletions

View file

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

View file

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

View file

@ -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<AlertsAffectedByMaintenanceWindows> {
// 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<AlertsAffectedByMaintenanceWindows> {
// 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) => {

View file

@ -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<UntypedNormalizedRuleType> = {
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: {},
activeAlerts: {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
recoveredAlerts: {},
ruleLogPrefix: 'test: rule-name',
ruleRunMetricsStore,
canSetRecoveryContext: false,
shouldPersistAlerts: true,
});
expect(alertsClient.getProcessedAlerts('active')).toEqual({
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('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<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
},
activeAlerts: {
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
currentRecoveredAlerts: {},
recoveredAlerts: {},
});
(trimRecoveredAlerts as jest.Mock).mockReturnValue({
trimmedAlertsRecovered: {},
earlyRecoveredAlerts: {},
});
(getAlertsForNotification as jest.Mock).mockReturnValue({
newAlerts: {
'1': new Alert<AlertInstanceContext, AlertInstanceContext>('1', testAlert1),
},
activeAlerts: {
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
currentActiveAlerts: {
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('2', testAlert2),
},
currentRecoveredAlerts: {},
recoveredAlerts: {},
});
const alertsClient = new LegacyAlertsClient({
alertingEventLogger,
logger,
request: fakeRequest,
spaceId: 'space1',
ruleType,
maintenanceWindowsService,
});
await alertsClient.initializeExecution({
...defaultExecutionOpts,
activeAlertsFromState: {
'2': testAlert2,
},
});
await alertsClient.processAlerts({
ruleRunMetricsStore,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
alertDelay: 5,
});
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<AlertInstanceContext, AlertInstanceContext>('1', {
...testAlert1,
meta: { ...testAlert1.meta, maintenanceWindowIds: ['test-id1', 'test-id2'] },
}),
},
{
'2': new Alert<AlertInstanceContext, AlertInstanceContext>('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);

View file

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

View file

@ -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<UntypedNormalizedRuleType> = {
...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`

View file

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

View file

@ -74,16 +74,12 @@ export interface IAlertsClient<
initializeExecution(opts: InitializeExecutionOpts): Promise<void>;
hasReachedAlertLimit(): boolean;
checkLimitUsage(): void;
processAndLogAlerts(opts: ProcessAndLogAlertsOpts): void;
processAlerts(opts: ProcessAlertsOpts): void;
logAlerts(opts: LogAlertsOpts): void;
getProcessedAlerts(
type: 'new' | 'active' | 'activeCurrent' | 'recovered' | 'recoveredCurrent'
): Record<string, LegacyAlert<State, Context, ActionGroupIds | RecoveryActionGroupId>>;
persistAlerts(maintenanceWindows?: MaintenanceWindow[]): Promise<{
alertIds: string[];
maintenanceWindowIds: string[];
} | null>;
persistAlerts(): Promise<{ alertIds: string[]; maintenanceWindowIds: string[] } | null>;
isTrackedAlert(id: string): boolean;
getSummarizedAlerts?(params: GetSummarizedAlertsParams): Promise<SummarizedAlerts>;
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;
}

View file

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

View file

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

View file

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

View file

@ -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<MaintenanceWindow[]> {
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'),
]);

View file

@ -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<AlertInstanceState, AlertInstanceContext>('1');
const newAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('2');
const existingAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('3');
const existingAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('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<AlertInstanceState, AlertInstanceContext>('1');
const existingAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('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<AlertInstanceState, AlertInstanceContext>('1');
const recoveredAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('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<AlertInstanceState, AlertInstanceContext>('1');
const existingAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('2');
const existingAlert3 = new Alert<AlertInstanceState, AlertInstanceContext>('3');
const existingAlert4 = new Alert<AlertInstanceState, AlertInstanceContext>('4');
const existingAlert5 = new Alert<AlertInstanceState, AlertInstanceContext>('5');
const newAlert6 = new Alert<AlertInstanceState, AlertInstanceContext>('6');
const newAlert7 = new Alert<AlertInstanceState, AlertInstanceContext>('7');
const newAlert8 = new Alert<AlertInstanceState, AlertInstanceContext>('8');
const newAlert9 = new Alert<AlertInstanceState, AlertInstanceContext>('9');
const newAlert10 = new Alert<AlertInstanceState, AlertInstanceContext>('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(`

View file

@ -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<State, Context>): 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<string, Alert<State, Context>>,
autoRecoverAlerts: boolean,
flappingSettings: RulesSettingsFlappingProperties,
maintenanceWindowIds: string[],
startedAt?: string | null
): ProcessAlertsResult<State, Context, ActionGroupIds, RecoveryActionGroupId> {
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<string, Alert<State, Context>>,
alertLimit: number,
flappingSettings: RulesSettingsFlappingProperties,
maintenanceWindowIds: string[],
startedAt?: string | null
): ProcessAlertsResult<State, Context, ActionGroupIds, RecoveryActionGroupId> {
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;
}

View file

@ -85,6 +85,6 @@ export class MaintenanceWindowClient {
public bulkGet = (
params: BulkGetMaintenanceWindowsParams
): Promise<BulkGetMaintenanceWindowsResult> => bulkGetMaintenanceWindows(this.context, params);
public getActiveMaintenanceWindows = (): Promise<MaintenanceWindow[]> =>
getActiveMaintenanceWindows(this.context);
public getActiveMaintenanceWindows = (cacheIntervalMs?: number): Promise<MaintenanceWindow[]> =>
getActiveMaintenanceWindows(this.context, cacheIntervalMs);
}

View file

@ -168,16 +168,17 @@ const createRuleExecutorServicesMock = <
done: jest.fn().mockReturnValue(alertFactoryMockDone),
},
alertsClient: publicAlertsClientMock.create(),
savedObjectsClient: savedObjectsClientMock.create(),
uiSettingsClient: uiSettingsServiceMock.createClient(),
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
shouldWriteAlerts: () => true,
shouldStopExecution: () => true,
search: createAbortableSearchServiceMock(),
getDataViews: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()),
getMaintenanceWindowIds: jest.fn().mockResolvedValue([]),
getSearchSourceClient: jest.fn().mockResolvedValue(searchSourceCommonMock),
ruleMonitoringService: createRuleMonitoringServiceMock(),
savedObjectsClient: savedObjectsClientMock.create(),
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
search: createAbortableSearchServiceMock(),
share: createShareStartMock(),
getDataViews: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()),
shouldStopExecution: () => true,
shouldWriteAlerts: () => true,
uiSettingsClient: uiSettingsServiceMock.createClient(),
};
};
export type RuleExecutorServicesMock = ReturnType<typeof createRuleExecutorServicesMock>;

View file

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

View file

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

View file

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

View file

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

View file

@ -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<MaintenanceWindow[]> => {
const { context, fakeRequest, logger, ruleTypeId, ruleId, ruleTypeCategory } = opts;
const maintenanceWindowClient = context.getMaintenanceWindowClientWithRequest(fakeRequest);
return await withAlertingSpan('alerting:load-maintenance-windows', async () => {
const {
getMaintenanceWindowClientWithRequest,
fakeRequest,
logger,
ruleTypeId,
ruleId,
ruleTypeCategory,
} = opts;
const maintenanceWindowClient = getMaintenanceWindowClientWithRequest(fakeRequest);
let activeMaintenanceWindows: MaintenanceWindow[] = [];
try {
activeMaintenanceWindows = await maintenanceWindowClient.getActiveMaintenanceWindows();
} catch (err) {
logger.error(
`error getting active maintenance window for ${ruleTypeId}:${ruleId} ${err.message}`
);
}
let activeMaintenanceWindows: MaintenanceWindow[] = [];
try {
activeMaintenanceWindows = await maintenanceWindowClient.getActiveMaintenanceWindows();
} catch (err) {
logger.error(
`error getting active maintenance window for ${ruleTypeId}:${ruleId} ${err.message}`
);
}
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;
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;
});
return maintenanceWindows;
};

View file

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

View file

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

View file

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

View file

@ -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<string, LastUpdatedWindows> = new Map();
constructor(private readonly options: MaintenanceWindowServiceOpts) {
if (options.cacheInterval) {
this.cacheIntervalMs = options.cacheInterval;
}
}
public async getMaintenanceWindows(
opts: GetMaintenanceWindowsOpts
): Promise<MaintenanceWindowData> {
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<MaintenanceWindow[]> {
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<MaintenanceWindow[]> {
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;
});
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<UntypedNormalizedRuleType> = {
...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<boolean>();
@ -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,
});

View file

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

View file

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

View file

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

View file

@ -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<AlertData, State, Context, ActionGroupIds> | null;
/**
* Deprecate alertFactory and remove when all rules are onboarded to
* the alertsClient
* @deprecated
*/
alertFactory: PublicAlertFactory<State, Context, ActionGroupIds>;
/**
* Only available when framework alerts are enabled and rule
* type has registered alert context with the framework with shouldWrite set to true
*/
alertsClient: PublicAlertsClient<AlertData, State, Context, ActionGroupIds> | null;
shouldWriteAlerts: () => boolean;
shouldStopExecution: () => boolean;
ruleMonitoringService?: PublicRuleMonitoringService;
share: SharePluginStart;
ruleResultService?: PublicRuleResultService;
getDataViews: () => Promise<DataViewsContract>;
getMaintenanceWindowIds: () => Promise<string[]>;
getSearchSourceClient: () => Promise<ISearchStartSearchSource>;
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;
}

View file

@ -181,6 +181,7 @@ describe('BurnRateRuleExecutor', () => {
shouldStopExecution: jest.fn(),
share: {} as SharePluginStart,
getDataViews: jest.fn().mockResolvedValue(dataViewPluginMocks.createStartContract()),
getMaintenanceWindowIds: jest.fn().mockResolvedValue([]),
};
});

View file

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

View file

@ -130,10 +130,9 @@ export const createLifecycleExecutor =
>
): Promise<{ state: WrappedLifecycleRuleState<State> }> => {
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<ParsedTechnicalFields & ParsedExperimentalFields>;

View file

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

View file

@ -51,7 +51,7 @@ export type BackendAlertWithSuppressionFields870<T> = Omit<
export const ALERT_GROUP_INDEX = `${ALERT_NAMESPACE}.group.index` as const;
const augmentAlerts = <T>({
const augmentAlerts = async <T>({
alerts,
options,
kibanaVersion,
@ -65,6 +65,9 @@ const augmentAlerts = <T>({
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 = <T>({
? 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,

View file

@ -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<Params, State, InstanceState, InstanceContext, ActionGroupIds> => ({
startedAt,
startedAtOverridden: false,
@ -78,17 +76,18 @@ export const createDefaultAlertExecutorOptions = <
params,
spaceId: 'SPACE_ID',
services: {
alertsClient: null,
alertFactory: alertsMock.createRuleExecutorServices<InstanceState, InstanceContext>()
.alertFactory,
savedObjectsClient: savedObjectsClientMock.create(),
uiSettingsClient: uiSettingsServiceMock.createClient(),
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
shouldWriteAlerts: () => shouldWriteAlerts,
shouldStopExecution: () => false,
getSearchSourceClient: async () => searchSourceCommonMock,
share: {} as SharePluginStart,
alertsClient: null,
getDataViews: async () => dataViewPluginMocks.createStartContract(),
getMaintenanceWindowIds: async () => ['test-id-1', 'test-id-2'],
getSearchSourceClient: async () => searchSourceCommonMock,
savedObjectsClient: savedObjectsClientMock.create(),
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
share: {} as SharePluginStart,
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 };

View file

@ -300,6 +300,7 @@ export const previewRulesRoute = (
abortController,
searchSourceClient,
}),
getMaintenanceWindowIds: async () => [],
uiSettingsClient: coreContext.uiSettings.client,
getDataViews: async () => dataViewsService,
share,

View file

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

View file

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

View file

@ -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<string | undefined>([
'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';

View file

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

View file

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

View file

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

View file

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

View file

@ -200,6 +200,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid
services: {
alertFactory: getMockAlertFactory(),
shouldWriteAlerts: sinon.stub().returns(true),
getMaintenanceWindowIds: async () => [],
},
flappingSettings: {
enabled: false,