mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Response Ops][Alerting] Refactor alerting task runner in order to reuse certain portions. (#178044)
## Summary To support the creation of a backfill rule run task runner, I would like to extract out certain parts of the alerting task runner for reuse, specifically some items in the `runRule` function that initialize the alerts client, call the rule type executors and call the alerts client to process and persist alerts. While doing this, I also moved around some other stuff in task runner. Descriptions below: This PR refactors the following in task runner: - in the `run()` function, consolidated everything that occurs before calling `this.runRule()` into the `prepareToRun()` function - in this commit8c64963757
- in the `run()` function, consolidated everything that occurs after calling `this.runRule()` into the `processRunResults()` function - in this commit856a78484e
- moved around some maintenance window handling code in this commit205e3e3f87
- extract initialization of alerts client into helper function in this commitb057e0a617
- extract out most of `runRule()` function into `RuleTypeRunner` class. this will be shared between the alerting task runner and the backfill task runner - in this commitb057e0a617
--------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
187a8f5fab
commit
80ff1a75f4
38 changed files with 2871 additions and 815 deletions
|
@ -13,7 +13,6 @@ const createAlertsClientMock = () => {
|
|||
logAlerts: jest.fn(),
|
||||
updateAlertMaintenanceWindowIds: jest.fn(),
|
||||
getMaintenanceWindowScopedQueryAlerts: jest.fn(),
|
||||
updateAlertsMaintenanceWindowIdByScopedQuery: jest.fn(),
|
||||
getTrackedAlerts: jest.fn(),
|
||||
getProcessedAlerts: jest.fn(),
|
||||
getAlertsToSerialize: jest.fn(),
|
||||
|
|
|
@ -1342,7 +1342,10 @@ describe('Alerts Client', () => {
|
|||
alertsClientParams
|
||||
);
|
||||
|
||||
expect(await alertsClient.persistAlerts()).toBe(void 0);
|
||||
expect(await alertsClient.persistAlerts()).toStrictEqual({
|
||||
alertIds: [],
|
||||
maintenanceWindowIds: [],
|
||||
});
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
`Resources registered and installed for test context but "shouldWrite" is set to false.`
|
||||
|
@ -1929,13 +1932,11 @@ describe('Alerts Client', () => {
|
|||
// @ts-ignore
|
||||
.mockResolvedValueOnce({});
|
||||
|
||||
const result = await alertsClient.updateAlertsMaintenanceWindowIdByScopedQuery({
|
||||
...getParamsByUpdateMaintenanceWindowIds,
|
||||
maintenanceWindows: [
|
||||
...getParamsByUpdateMaintenanceWindowIds.maintenanceWindows,
|
||||
{ id: 'mw3' } as unknown as MaintenanceWindow,
|
||||
],
|
||||
});
|
||||
// @ts-expect-error
|
||||
const result = await alertsClient.updateAlertsMaintenanceWindowIdByScopedQuery([
|
||||
...getParamsByUpdateMaintenanceWindowIds.maintenanceWindows,
|
||||
{ id: 'mw3' } as unknown as MaintenanceWindow,
|
||||
]);
|
||||
|
||||
expect(alert1.getMaintenanceWindowIds()).toEqual(['mw3', 'mw1']);
|
||||
expect(alert2.getMaintenanceWindowIds()).toEqual(['mw3', 'mw1']);
|
||||
|
|
|
@ -45,7 +45,6 @@ import {
|
|||
UpdateableAlert,
|
||||
GetSummarizedAlertsParams,
|
||||
GetMaintenanceWindowScopedQueryAlertsParams,
|
||||
UpdateAlertsMaintenanceWindowIdByScopedQueryParams,
|
||||
ScopedQueryAggregationResult,
|
||||
} from './types';
|
||||
import {
|
||||
|
@ -62,6 +61,11 @@ 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';
|
||||
|
||||
// Term queries can take up to 10,000 terms
|
||||
const CHUNK_SIZE = 10000;
|
||||
|
@ -299,7 +303,99 @@ export class AlertsClient<
|
|||
return this.legacyAlertsClient.getProcessedAlerts(type);
|
||||
}
|
||||
|
||||
public async persistAlerts() {
|
||||
public async persistAlerts(maintenanceWindows?: MaintenanceWindow[]): Promise<{
|
||||
alertIds: string[];
|
||||
maintenanceWindowIds: string[];
|
||||
} | null> {
|
||||
// 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 for rule ${this.ruleType.id}:${this.options.rule.id}: '${this.options.rule.name}'.`
|
||||
);
|
||||
}
|
||||
|
||||
return updateAlertsMaintenanceWindowResult;
|
||||
}
|
||||
|
||||
public getAlertsToSerialize() {
|
||||
// The flapping value that is persisted inside the task manager state (and used in the next execution)
|
||||
// is different than the value that should be written to the alert document. For this reason, we call
|
||||
// getAlertsToSerialize() twice, once before building and bulk indexing alert docs and once after to return
|
||||
// the value for task state serialization
|
||||
|
||||
// This will be a blocker if ever we want to stop serializing alert data inside the task state and just use
|
||||
// the fetched alert document.
|
||||
return this.legacyAlertsClient.getAlertsToSerialize();
|
||||
}
|
||||
|
||||
public factory() {
|
||||
return this.legacyAlertsClient.factory();
|
||||
}
|
||||
|
||||
public async getSummarizedAlerts({
|
||||
ruleId,
|
||||
spaceId,
|
||||
excludedAlertInstanceIds,
|
||||
alertsFilter,
|
||||
start,
|
||||
end,
|
||||
executionUuid,
|
||||
}: GetSummarizedAlertsParams): Promise<SummarizedAlerts> {
|
||||
if (!ruleId || !spaceId) {
|
||||
throw new Error(`Must specify both rule ID and space ID for AAD alert query.`);
|
||||
}
|
||||
const queryByExecutionUuid: boolean = !!executionUuid;
|
||||
const queryByTimeRange: boolean = !!start && !!end;
|
||||
// Either executionUuid or start/end dates must be specified, but not both
|
||||
if (
|
||||
(!queryByExecutionUuid && !queryByTimeRange) ||
|
||||
(queryByExecutionUuid && queryByTimeRange)
|
||||
) {
|
||||
throw new Error(`Must specify either execution UUID or time range for AAD alert query.`);
|
||||
}
|
||||
|
||||
const getQueryParams = {
|
||||
executionUuid,
|
||||
start,
|
||||
end,
|
||||
ruleId,
|
||||
excludedAlertInstanceIds,
|
||||
alertsFilter,
|
||||
};
|
||||
|
||||
const formatAlert = this.ruleType.alerts?.formatAlert;
|
||||
|
||||
const isLifecycleAlert = this.ruleType.autoRecoverAlerts ?? false;
|
||||
|
||||
if (isLifecycleAlert) {
|
||||
const queryBodies = getLifecycleAlertsQueries(getQueryParams);
|
||||
const responses = await Promise.all(queryBodies.map((queryBody) => this.search(queryBody)));
|
||||
|
||||
return {
|
||||
new: getHitsWithCount(responses[0], formatAlert),
|
||||
ongoing: getHitsWithCount(responses[1], formatAlert),
|
||||
recovered: getHitsWithCount(responses[2], formatAlert),
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.search(getContinualAlertsQuery(getQueryParams));
|
||||
|
||||
return {
|
||||
new: getHitsWithCount(response, formatAlert),
|
||||
ongoing: { count: 0, data: [] },
|
||||
recovered: { count: 0, data: [] },
|
||||
};
|
||||
}
|
||||
|
||||
private async persistAlertsHelper() {
|
||||
if (!this.ruleType.alerts?.shouldWrite) {
|
||||
this.options.logger.debug(
|
||||
`Resources registered and installed for ${this.ruleType.alerts?.context} context but "shouldWrite" is set to false.`
|
||||
|
@ -499,76 +595,6 @@ export class AlertsClient<
|
|||
}
|
||||
}
|
||||
|
||||
public getAlertsToSerialize() {
|
||||
// The flapping value that is persisted inside the task manager state (and used in the next execution)
|
||||
// is different than the value that should be written to the alert document. For this reason, we call
|
||||
// getAlertsToSerialize() twice, once before building and bulk indexing alert docs and once after to return
|
||||
// the value for task state serialization
|
||||
|
||||
// This will be a blocker if ever we want to stop serializing alert data inside the task state and just use
|
||||
// the fetched alert document.
|
||||
return this.legacyAlertsClient.getAlertsToSerialize();
|
||||
}
|
||||
|
||||
public factory() {
|
||||
return this.legacyAlertsClient.factory();
|
||||
}
|
||||
|
||||
public async getSummarizedAlerts({
|
||||
ruleId,
|
||||
spaceId,
|
||||
excludedAlertInstanceIds,
|
||||
alertsFilter,
|
||||
start,
|
||||
end,
|
||||
executionUuid,
|
||||
}: GetSummarizedAlertsParams): Promise<SummarizedAlerts> {
|
||||
if (!ruleId || !spaceId) {
|
||||
throw new Error(`Must specify both rule ID and space ID for AAD alert query.`);
|
||||
}
|
||||
const queryByExecutionUuid: boolean = !!executionUuid;
|
||||
const queryByTimeRange: boolean = !!start && !!end;
|
||||
// Either executionUuid or start/end dates must be specified, but not both
|
||||
if (
|
||||
(!queryByExecutionUuid && !queryByTimeRange) ||
|
||||
(queryByExecutionUuid && queryByTimeRange)
|
||||
) {
|
||||
throw new Error(`Must specify either execution UUID or time range for AAD alert query.`);
|
||||
}
|
||||
|
||||
const getQueryParams = {
|
||||
executionUuid,
|
||||
start,
|
||||
end,
|
||||
ruleId,
|
||||
excludedAlertInstanceIds,
|
||||
alertsFilter,
|
||||
};
|
||||
|
||||
const formatAlert = this.ruleType.alerts?.formatAlert;
|
||||
|
||||
const isLifecycleAlert = this.ruleType.autoRecoverAlerts ?? false;
|
||||
|
||||
if (isLifecycleAlert) {
|
||||
const queryBodies = getLifecycleAlertsQueries(getQueryParams);
|
||||
const responses = await Promise.all(queryBodies.map((queryBody) => this.search(queryBody)));
|
||||
|
||||
return {
|
||||
new: getHitsWithCount(responses[0], formatAlert),
|
||||
ongoing: getHitsWithCount(responses[1], formatAlert),
|
||||
recovered: getHitsWithCount(responses[2], formatAlert),
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.search(getContinualAlertsQuery(getQueryParams));
|
||||
|
||||
return {
|
||||
new: getHitsWithCount(response, formatAlert),
|
||||
ongoing: { count: 0, data: [] },
|
||||
recovered: { count: 0, data: [] },
|
||||
};
|
||||
}
|
||||
|
||||
private async getMaintenanceWindowScopedQueryAlerts({
|
||||
ruleId,
|
||||
spaceId,
|
||||
|
@ -633,21 +659,17 @@ export class AlertsClient<
|
|||
}
|
||||
}
|
||||
|
||||
public async updateAlertsMaintenanceWindowIdByScopedQuery({
|
||||
ruleId,
|
||||
spaceId,
|
||||
executionUuid,
|
||||
maintenanceWindows,
|
||||
}: UpdateAlertsMaintenanceWindowIdByScopedQueryParams) {
|
||||
const maintenanceWindowsWithScopedQuery = maintenanceWindows.filter(
|
||||
({ scopedQuery }) => scopedQuery
|
||||
);
|
||||
const maintenanceWindowsWithoutScopedQuery = maintenanceWindows.filter(
|
||||
({ scopedQuery }) => !scopedQuery
|
||||
);
|
||||
const maintenanceWindowsWithoutScopedQueryIds = maintenanceWindowsWithoutScopedQuery.map(
|
||||
({ id }) => id
|
||||
);
|
||||
private async updateAlertsMaintenanceWindowIdByScopedQuery(
|
||||
maintenanceWindows: MaintenanceWindow[]
|
||||
) {
|
||||
const maintenanceWindowsWithScopedQuery = filterMaintenanceWindows({
|
||||
maintenanceWindows,
|
||||
withScopedQuery: true,
|
||||
});
|
||||
const maintenanceWindowsWithoutScopedQueryIds = filterMaintenanceWindowsIds({
|
||||
maintenanceWindows,
|
||||
withScopedQuery: false,
|
||||
});
|
||||
|
||||
if (maintenanceWindowsWithScopedQuery.length === 0) {
|
||||
return {
|
||||
|
@ -659,9 +681,9 @@ export class AlertsClient<
|
|||
// Run aggs to get all scoped query alert IDs, returns a record<maintenanceWindowId, alertIds>,
|
||||
// indicating the maintenance window has matches a number of alerts with the scoped query.
|
||||
const aggsResult = await this.getMaintenanceWindowScopedQueryAlerts({
|
||||
ruleId,
|
||||
spaceId,
|
||||
executionUuid,
|
||||
ruleId: this.options.rule.id,
|
||||
spaceId: this.options.rule.spaceId,
|
||||
executionUuid: this.options.rule.executionId,
|
||||
maintenanceWindows: maintenanceWindowsWithScopedQuery,
|
||||
});
|
||||
|
||||
|
|
|
@ -8,5 +8,5 @@
|
|||
export { type LegacyAlertsClientParams, LegacyAlertsClient } from './legacy_alerts_client';
|
||||
export { AlertsClient } from './alerts_client';
|
||||
export type { AlertRuleData } from './types';
|
||||
export { sanitizeBulkErrorResponse } from './lib';
|
||||
export { sanitizeBulkErrorResponse, initializeAlertsClient } from './lib';
|
||||
export { AlertsClientError } from './alerts_client_error';
|
||||
|
|
|
@ -21,6 +21,7 @@ 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,
|
||||
|
@ -265,7 +266,9 @@ export class LegacyAlertsClient<
|
|||
return null;
|
||||
}
|
||||
|
||||
public async persistAlerts() {}
|
||||
public async persistAlerts(maintenanceWindows?: MaintenanceWindow[]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async setAlertStatusToUntracked() {
|
||||
return;
|
||||
|
|
|
@ -19,3 +19,4 @@ export {
|
|||
} from './get_summarized_alerts_query';
|
||||
export { expandFlattenedAlert } from './format_alert';
|
||||
export { sanitizeBulkErrorResponse } from './sanitize_bulk_response';
|
||||
export { initializeAlertsClient } from './initialize_alerts_client';
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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 { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import {
|
||||
mockedRule,
|
||||
mockTaskInstance,
|
||||
ruleType,
|
||||
RULE_ID,
|
||||
RULE_NAME,
|
||||
RULE_TYPE_ID,
|
||||
} from '../../task_runner/fixtures';
|
||||
import * as LegacyAlertsClientModule from '../legacy_alerts_client';
|
||||
import { alertsServiceMock } from '../../alerts_service/alerts_service.mock';
|
||||
import { ruleRunMetricsStoreMock } from '../../lib/rule_run_metrics_store.mock';
|
||||
import { alertingEventLoggerMock } from '../../lib/alerting_event_logger/alerting_event_logger.mock';
|
||||
import { DEFAULT_FLAPPING_SETTINGS, DEFAULT_QUERY_DELAY_SETTINGS } from '../../types';
|
||||
import { alertsClientMock } from '../alerts_client.mock';
|
||||
import { UntypedNormalizedRuleType } from '../../rule_type_registry';
|
||||
import { legacyAlertsClientMock } from '../legacy_alerts_client.mock';
|
||||
import { initializeAlertsClient } from './initialize_alerts_client';
|
||||
|
||||
const alertingEventLogger = alertingEventLoggerMock.create();
|
||||
const ruleRunMetricsStore = ruleRunMetricsStoreMock.create();
|
||||
const alertsService = alertsServiceMock.create();
|
||||
const alertsClient = alertsClientMock.create();
|
||||
const legacyAlertsClient = legacyAlertsClientMock.create();
|
||||
const logger = loggingSystemMock.create().get();
|
||||
|
||||
const ruleTypeWithAlerts: jest.Mocked<UntypedNormalizedRuleType> = {
|
||||
...ruleType,
|
||||
alerts: {
|
||||
context: 'test',
|
||||
mappings: {
|
||||
fieldMap: {
|
||||
textField: {
|
||||
type: 'keyword',
|
||||
required: false,
|
||||
},
|
||||
numericField: {
|
||||
type: 'long',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldWrite: true,
|
||||
},
|
||||
};
|
||||
|
||||
describe('initializeAlertsClient', () => {
|
||||
test('should initialize and return alertsClient if createAlertsClient succeeds', async () => {
|
||||
const spy1 = jest
|
||||
.spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient')
|
||||
.mockImplementation(() => legacyAlertsClient);
|
||||
alertsService.createAlertsClient.mockImplementationOnce(() => alertsClient);
|
||||
await initializeAlertsClient({
|
||||
alertsService,
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
executionId: 'abc',
|
||||
logger,
|
||||
maxAlerts: 100,
|
||||
rule: mockedRule,
|
||||
ruleType: ruleTypeWithAlerts,
|
||||
taskInstance: mockTaskInstance(),
|
||||
});
|
||||
|
||||
expect(alertsService.createAlertsClient).toHaveBeenCalledWith({
|
||||
logger,
|
||||
ruleType: ruleTypeWithAlerts,
|
||||
namespace: 'default',
|
||||
rule: {
|
||||
alertDelay: 0,
|
||||
consumer: 'bar',
|
||||
executionId: 'abc',
|
||||
id: '1',
|
||||
name: 'rule-name',
|
||||
parameters: {
|
||||
bar: true,
|
||||
},
|
||||
revision: 0,
|
||||
spaceId: 'default',
|
||||
tags: ['rule-', '-tags'],
|
||||
},
|
||||
});
|
||||
expect(LegacyAlertsClientModule.LegacyAlertsClient).not.toHaveBeenCalled();
|
||||
expect(alertsClient.initializeExecution).toHaveBeenCalledWith({
|
||||
activeAlertsFromState: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
maxAlerts: 100,
|
||||
recoveredAlertsFromState: {},
|
||||
ruleLabel: `test:1: 'rule-name'`,
|
||||
startedAt: expect.any(Date),
|
||||
});
|
||||
spy1.mockRestore();
|
||||
});
|
||||
|
||||
test('should use LegacyAlertsClient if createAlertsClient returns null', async () => {
|
||||
const spy1 = jest
|
||||
.spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient')
|
||||
.mockImplementation(() => legacyAlertsClient);
|
||||
alertsService.createAlertsClient.mockImplementationOnce(() => null);
|
||||
await initializeAlertsClient({
|
||||
alertsService,
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
executionId: 'abc',
|
||||
logger,
|
||||
maxAlerts: 100,
|
||||
rule: mockedRule,
|
||||
ruleType: ruleTypeWithAlerts,
|
||||
taskInstance: mockTaskInstance(),
|
||||
});
|
||||
|
||||
expect(alertsService.createAlertsClient).toHaveBeenCalledWith({
|
||||
logger,
|
||||
ruleType: ruleTypeWithAlerts,
|
||||
namespace: 'default',
|
||||
rule: {
|
||||
alertDelay: 0,
|
||||
consumer: 'bar',
|
||||
executionId: 'abc',
|
||||
id: '1',
|
||||
name: 'rule-name',
|
||||
parameters: {
|
||||
bar: true,
|
||||
},
|
||||
revision: 0,
|
||||
spaceId: 'default',
|
||||
tags: ['rule-', '-tags'],
|
||||
},
|
||||
});
|
||||
expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({
|
||||
logger,
|
||||
ruleType: ruleTypeWithAlerts,
|
||||
});
|
||||
expect(legacyAlertsClient.initializeExecution).toHaveBeenCalledWith({
|
||||
activeAlertsFromState: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
maxAlerts: 100,
|
||||
recoveredAlertsFromState: {},
|
||||
ruleLabel: `test:1: 'rule-name'`,
|
||||
startedAt: expect.any(Date),
|
||||
});
|
||||
spy1.mockRestore();
|
||||
});
|
||||
|
||||
test('should use LegacyAlertsClient if createAlertsClient throws error', async () => {
|
||||
const spy1 = jest
|
||||
.spyOn(LegacyAlertsClientModule, 'LegacyAlertsClient')
|
||||
.mockImplementation(() => legacyAlertsClient);
|
||||
alertsService.createAlertsClient.mockImplementationOnce(() => {
|
||||
throw new Error('fail fail');
|
||||
});
|
||||
await initializeAlertsClient({
|
||||
alertsService,
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
executionId: 'abc',
|
||||
logger,
|
||||
maxAlerts: 100,
|
||||
rule: mockedRule,
|
||||
ruleType: ruleTypeWithAlerts,
|
||||
taskInstance: mockTaskInstance(),
|
||||
});
|
||||
|
||||
expect(alertsService.createAlertsClient).toHaveBeenCalledWith({
|
||||
logger,
|
||||
ruleType: ruleTypeWithAlerts,
|
||||
namespace: 'default',
|
||||
rule: {
|
||||
alertDelay: 0,
|
||||
consumer: 'bar',
|
||||
executionId: 'abc',
|
||||
id: '1',
|
||||
name: 'rule-name',
|
||||
parameters: {
|
||||
bar: true,
|
||||
},
|
||||
revision: 0,
|
||||
spaceId: 'default',
|
||||
tags: ['rule-', '-tags'],
|
||||
},
|
||||
});
|
||||
expect(LegacyAlertsClientModule.LegacyAlertsClient).toHaveBeenCalledWith({
|
||||
logger,
|
||||
ruleType: ruleTypeWithAlerts,
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Error initializing AlertsClient for context test. Using legacy alerts client instead. - fail fail`
|
||||
);
|
||||
expect(legacyAlertsClient.initializeExecution).toHaveBeenCalledWith({
|
||||
activeAlertsFromState: {},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
maxAlerts: 100,
|
||||
recoveredAlertsFromState: {},
|
||||
ruleLabel: `test:1: 'rule-name'`,
|
||||
startedAt: expect.any(Date),
|
||||
});
|
||||
spy1.mockRestore();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { LegacyAlertsClient } from '..';
|
||||
import { IAlertsClient } from '../types';
|
||||
import { AlertsService } from '../../alerts_service';
|
||||
import { UntypedNormalizedRuleType } from '../../rule_type_registry';
|
||||
import {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
RuleAlertData,
|
||||
RuleTypeParams,
|
||||
SanitizedRule,
|
||||
} from '../../types';
|
||||
import { RuleTaskInstance, RuleTypeRunnerContext } from '../../task_runner/types';
|
||||
|
||||
interface InitializeAlertsClientOpts<Params extends RuleTypeParams> {
|
||||
alertsService: AlertsService | null;
|
||||
context: RuleTypeRunnerContext;
|
||||
executionId: string;
|
||||
logger: Logger;
|
||||
maxAlerts: number;
|
||||
rule: SanitizedRule<Params>;
|
||||
ruleType: UntypedNormalizedRuleType;
|
||||
taskInstance: RuleTaskInstance;
|
||||
}
|
||||
|
||||
export const initializeAlertsClient = async <
|
||||
Params extends RuleTypeParams,
|
||||
AlertData extends RuleAlertData,
|
||||
State extends AlertInstanceState,
|
||||
Context extends AlertInstanceContext,
|
||||
ActionGroupIds extends string,
|
||||
RecoveryActionGroupId extends string
|
||||
>({
|
||||
alertsService,
|
||||
context,
|
||||
executionId,
|
||||
logger,
|
||||
maxAlerts,
|
||||
rule,
|
||||
ruleType,
|
||||
taskInstance,
|
||||
}: InitializeAlertsClientOpts<Params>) => {
|
||||
const {
|
||||
state: {
|
||||
alertInstances: alertRawInstances = {},
|
||||
alertRecoveredInstances: alertRecoveredRawInstances = {},
|
||||
},
|
||||
} = taskInstance;
|
||||
|
||||
const alertsClientParams = { logger, ruleType };
|
||||
|
||||
// Create AlertsClient if rule type has registered an alerts context
|
||||
// with the framework. The AlertsClient will handle reading and
|
||||
// writing from alerts-as-data indices and eventually
|
||||
// we will want to migrate all the processing of alerts out
|
||||
// of the LegacyAlertsClient and into the AlertsClient.
|
||||
let alertsClient: IAlertsClient<AlertData, State, Context, ActionGroupIds, RecoveryActionGroupId>;
|
||||
|
||||
try {
|
||||
const client =
|
||||
(await alertsService?.createAlertsClient<
|
||||
AlertData,
|
||||
State,
|
||||
Context,
|
||||
ActionGroupIds,
|
||||
RecoveryActionGroupId
|
||||
>({
|
||||
...alertsClientParams,
|
||||
namespace: context.namespace ?? DEFAULT_NAMESPACE_STRING,
|
||||
rule: {
|
||||
consumer: rule.consumer,
|
||||
executionId,
|
||||
id: rule.id,
|
||||
name: rule.name,
|
||||
parameters: rule.params,
|
||||
revision: rule.revision,
|
||||
spaceId: context.spaceId,
|
||||
tags: rule.tags,
|
||||
alertDelay: rule.alertDelay?.active ?? 0,
|
||||
},
|
||||
})) ?? null;
|
||||
|
||||
alertsClient = client
|
||||
? client
|
||||
: new LegacyAlertsClient<State, Context, ActionGroupIds, RecoveryActionGroupId>(
|
||||
alertsClientParams
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Error initializing AlertsClient for context ${ruleType.alerts?.context}. Using legacy alerts client instead. - ${err.message}`
|
||||
);
|
||||
|
||||
alertsClient = new LegacyAlertsClient<State, Context, ActionGroupIds, RecoveryActionGroupId>(
|
||||
alertsClientParams
|
||||
);
|
||||
}
|
||||
|
||||
await alertsClient.initializeExecution({
|
||||
maxAlerts,
|
||||
ruleLabel: context.ruleLogPrefix,
|
||||
flappingSettings: context.flappingSettings,
|
||||
startedAt: taskInstance.startedAt!,
|
||||
activeAlertsFromState: alertRawInstances,
|
||||
recoveredAlertsFromState: alertRecoveredRawInstances,
|
||||
});
|
||||
|
||||
return alertsClient;
|
||||
};
|
|
@ -80,14 +80,11 @@ export interface IAlertsClient<
|
|||
getProcessedAlerts(
|
||||
type: 'new' | 'active' | 'activeCurrent' | 'recovered' | 'recoveredCurrent'
|
||||
): Record<string, LegacyAlert<State, Context, ActionGroupIds | RecoveryActionGroupId>>;
|
||||
persistAlerts(): Promise<void>;
|
||||
getSummarizedAlerts?(params: GetSummarizedAlertsParams): Promise<SummarizedAlerts>;
|
||||
updateAlertsMaintenanceWindowIdByScopedQuery?(
|
||||
params: UpdateAlertsMaintenanceWindowIdByScopedQueryParams
|
||||
): Promise<{
|
||||
persistAlerts(maintenanceWindows?: MaintenanceWindow[]): Promise<{
|
||||
alertIds: string[];
|
||||
maintenanceWindowIds: string[];
|
||||
}>;
|
||||
} | null>;
|
||||
getSummarizedAlerts?(params: GetSummarizedAlertsParams): Promise<SummarizedAlerts>;
|
||||
getAlertsToSerialize(): {
|
||||
alertsToReturn: Record<string, RawAlertInstance>;
|
||||
recoveredAlertsToReturn: Record<string, RawAlertInstance>;
|
||||
|
|
|
@ -777,6 +777,7 @@ describe('AlertingEventLogger', () => {
|
|||
[TaskRunnerTimerSpan.StartTaskRun]: 10,
|
||||
[TaskRunnerTimerSpan.TotalRunDuration]: 20,
|
||||
[TaskRunnerTimerSpan.PrepareRule]: 30,
|
||||
[TaskRunnerTimerSpan.PrepareToRun]: 35,
|
||||
[TaskRunnerTimerSpan.RuleTypeRun]: 40,
|
||||
[TaskRunnerTimerSpan.ProcessAlerts]: 50,
|
||||
[TaskRunnerTimerSpan.PersistAlerts]: 60,
|
||||
|
@ -800,6 +801,7 @@ describe('AlertingEventLogger', () => {
|
|||
claim_to_start_duration_ms: 10,
|
||||
total_run_duration_ms: 20,
|
||||
prepare_rule_duration_ms: 30,
|
||||
prepare_to_run_duration_ms: 35,
|
||||
rule_type_run_duration_ms: 40,
|
||||
process_alerts_duration_ms: 50,
|
||||
persist_alerts_duration_ms: 60,
|
||||
|
@ -838,6 +840,7 @@ describe('AlertingEventLogger', () => {
|
|||
[TaskRunnerTimerSpan.StartTaskRun]: 10,
|
||||
[TaskRunnerTimerSpan.TotalRunDuration]: 20,
|
||||
[TaskRunnerTimerSpan.PrepareRule]: 30,
|
||||
[TaskRunnerTimerSpan.PrepareToRun]: 35,
|
||||
[TaskRunnerTimerSpan.RuleTypeRun]: 40,
|
||||
[TaskRunnerTimerSpan.ProcessAlerts]: 50,
|
||||
[TaskRunnerTimerSpan.PersistAlerts]: 60,
|
||||
|
@ -872,6 +875,7 @@ describe('AlertingEventLogger', () => {
|
|||
claim_to_start_duration_ms: 10,
|
||||
total_run_duration_ms: 20,
|
||||
prepare_rule_duration_ms: 30,
|
||||
prepare_to_run_duration_ms: 35,
|
||||
rule_type_run_duration_ms: 40,
|
||||
process_alerts_duration_ms: 50,
|
||||
persist_alerts_duration_ms: 60,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
export const createWrappedScopedClusterClientMock = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
client: jest.fn().mockReturnValue(elasticsearchServiceMock.createScopedClusterClient()),
|
||||
getMetrics: jest.fn(),
|
||||
};
|
||||
});
|
||||
export const wrappedScopedClusterClientMock = {
|
||||
create: createWrappedScopedClusterClientMock,
|
||||
};
|
|
@ -49,7 +49,14 @@ interface LogSearchMetricsOpts {
|
|||
}
|
||||
type LogSearchMetricsFn = (metrics: LogSearchMetricsOpts) => void;
|
||||
|
||||
export function createWrappedScopedClusterClientFactory(opts: WrapScopedClusterClientFactoryOpts) {
|
||||
export interface WrappedScopedClusterClient {
|
||||
client: () => IScopedClusterClient;
|
||||
getMetrics: () => SearchMetrics;
|
||||
}
|
||||
|
||||
export function createWrappedScopedClusterClientFactory(
|
||||
opts: WrapScopedClusterClientFactoryOpts
|
||||
): WrappedScopedClusterClient {
|
||||
let numSearches: number = 0;
|
||||
let esSearchDurationMs: number = 0;
|
||||
let totalSearchDurationMs: number = 0;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import { searchSourceCommonMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
|
||||
export const createWrappedSearchSourceClientMock = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
client: jest.fn().mockReturnValue(searchSourceCommonMock),
|
||||
getMetrics: jest.fn(),
|
||||
};
|
||||
});
|
||||
export const wrappedSearchSourceClientMock = {
|
||||
create: createWrappedSearchSourceClientMock,
|
||||
};
|
|
@ -33,13 +33,18 @@ interface WrapParams<T extends ISearchSource | SearchSource> {
|
|||
requestTimeout?: number;
|
||||
}
|
||||
|
||||
export interface WrappedSearchSourceClient {
|
||||
searchSourceClient: ISearchStartSearchSource;
|
||||
getMetrics: () => SearchMetrics;
|
||||
}
|
||||
|
||||
export function wrapSearchSourceClient({
|
||||
logger,
|
||||
rule,
|
||||
abortController,
|
||||
searchSourceClient: pureSearchSourceClient,
|
||||
requestTimeout,
|
||||
}: Props) {
|
||||
}: Props): WrappedSearchSourceClient {
|
||||
let numSearches: number = 0;
|
||||
let esSearchDurationMs: number = 0;
|
||||
let totalSearchDurationMs: number = 0;
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
function createRuleMonitoringServiceMock() {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
addHistory: jest.fn(),
|
||||
getLastRunMetricsSetters: jest.fn(),
|
||||
getMonitoring: jest.fn(),
|
||||
setLastRunMetricsDuration: jest.fn(),
|
||||
setMonitoring: jest.fn(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function createPublicRuleMonitoringServiceMock() {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
setLastRunMetricsGapDurationS: jest.fn(),
|
||||
setLastRunMetricsTotalAlertsCreated: jest.fn(),
|
||||
setLastRunMetricsTotalAlertsDetected: jest.fn(),
|
||||
setLastRunMetricsTotalIndexingDurationMs: jest.fn(),
|
||||
setLastRunMetricsTotalSearchDurationMs: jest.fn(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const ruleMonitoringServiceMock = {
|
||||
create: createRuleMonitoringServiceMock(),
|
||||
};
|
||||
|
||||
export const publicRuleMonitoringServiceMock = {
|
||||
create: createPublicRuleMonitoringServiceMock(),
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
function createRuleResultServiceMock() {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getLastRunErrors: jest.fn(),
|
||||
getLastRunOutcomeMessage: jest.fn(),
|
||||
getLastRunResults: jest.fn(),
|
||||
getLastRunSetters: jest.fn(),
|
||||
getLastRunWarnings: jest.fn(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function createPublicRuleResultServiceMock() {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
addLastRunError: jest.fn(),
|
||||
addLastRunWarning: jest.fn(),
|
||||
setLastRunOutcomeMessage: jest.fn(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const ruleResultServiceMock = {
|
||||
create: createRuleResultServiceMock(),
|
||||
};
|
||||
|
||||
export const publicRuleResultServiceMock = {
|
||||
create: createPublicRuleResultServiceMock(),
|
||||
};
|
|
@ -26,7 +26,6 @@ import {
|
|||
} from '../types';
|
||||
import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store';
|
||||
import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { ConcreteTaskInstance, TaskErrorSource } from '@kbn/task-manager-plugin/server';
|
||||
import { Alert } from '../alert';
|
||||
import { AlertInstanceState, AlertInstanceContext, RuleNotifyWhen } from '../../common';
|
||||
|
@ -38,6 +37,7 @@ import { alertsClientMock } from '../alerts_client/alerts_client.mock';
|
|||
import { ExecutionResponseType } from '@kbn/actions-plugin/server/create_execute_function';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects';
|
||||
import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running';
|
||||
import { TaskRunnerContext } from './types';
|
||||
|
||||
jest.mock('./inject_action_params', () => ({
|
||||
injectActionParams: jest.fn(),
|
||||
|
|
|
@ -27,8 +27,7 @@ import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event
|
|||
import { AlertHit, parseDuration, CombinedSummarizedAlerts, ThrottledActions } from '../types';
|
||||
import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store';
|
||||
import { injectActionParams } from './inject_action_params';
|
||||
import { Executable, ExecutionHandlerOptions, RuleTaskInstance } from './types';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { Executable, ExecutionHandlerOptions, RuleTaskInstance, TaskRunnerContext } from './types';
|
||||
import {
|
||||
transformActionParams,
|
||||
TransformActionParamsOptions,
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 {
|
||||
IUiSettingsClient,
|
||||
KibanaRequest,
|
||||
Logger,
|
||||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '..';
|
||||
import { getEsRequestTimeout } from '../lib';
|
||||
import {
|
||||
createWrappedScopedClusterClientFactory,
|
||||
WrappedScopedClusterClient,
|
||||
} from '../lib/wrap_scoped_cluster_client';
|
||||
import {
|
||||
WrappedSearchSourceClient,
|
||||
wrapSearchSourceClient,
|
||||
} from '../lib/wrap_search_source_client';
|
||||
import { RuleMonitoringService } from '../monitoring/rule_monitoring_service';
|
||||
import { RuleResultService } from '../monitoring/rule_result_service';
|
||||
import { PublicRuleMonitoringService, PublicRuleResultService } from '../types';
|
||||
import { TaskRunnerContext } from './types';
|
||||
|
||||
interface GetExecutorServicesOpts {
|
||||
context: TaskRunnerContext;
|
||||
fakeRequest: KibanaRequest;
|
||||
abortController: AbortController;
|
||||
logger: Logger;
|
||||
ruleMonitoringService: RuleMonitoringService;
|
||||
ruleResultService: RuleResultService;
|
||||
ruleData: { name: string; alertTypeId: string; id: string; spaceId: string };
|
||||
ruleTaskTimeout?: string;
|
||||
}
|
||||
|
||||
export interface ExecutorServices {
|
||||
dataViews: DataViewsContract;
|
||||
ruleMonitoringService: PublicRuleMonitoringService;
|
||||
ruleResultService: PublicRuleResultService;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
uiSettingsClient: IUiSettingsClient;
|
||||
wrappedScopedClusterClient: WrappedScopedClusterClient;
|
||||
wrappedSearchSourceClient: WrappedSearchSourceClient;
|
||||
}
|
||||
|
||||
export const getExecutorServices = async (opts: GetExecutorServicesOpts) => {
|
||||
const { context, abortController, fakeRequest, logger, ruleData, ruleTaskTimeout } = opts;
|
||||
|
||||
const wrappedClientOptions = {
|
||||
rule: ruleData,
|
||||
logger,
|
||||
abortController,
|
||||
// Set the ES request timeout to the rule task timeout
|
||||
requestTimeout: getEsRequestTimeout(logger, ruleTaskTimeout),
|
||||
};
|
||||
|
||||
const scopedClusterClient = context.elasticsearch.client.asScoped(fakeRequest);
|
||||
const wrappedScopedClusterClient = createWrappedScopedClusterClientFactory({
|
||||
...wrappedClientOptions,
|
||||
scopedClusterClient,
|
||||
});
|
||||
|
||||
const searchSourceClient = await context.data.search.searchSource.asScoped(fakeRequest);
|
||||
const wrappedSearchSourceClient = wrapSearchSourceClient({
|
||||
...wrappedClientOptions,
|
||||
searchSourceClient,
|
||||
});
|
||||
|
||||
const savedObjectsClient = context.savedObjects.getScopedClient(fakeRequest, {
|
||||
includedHiddenTypes: [RULE_SAVED_OBJECT_TYPE, 'action'],
|
||||
});
|
||||
|
||||
const dataViews = await context.dataViews.dataViewsServiceFactory(
|
||||
savedObjectsClient,
|
||||
scopedClusterClient.asInternalUser
|
||||
);
|
||||
|
||||
const uiSettingsClient = context.uiSettings.asScopedToClient(savedObjectsClient);
|
||||
|
||||
return {
|
||||
dataViews,
|
||||
ruleMonitoringService: opts.ruleMonitoringService.getLastRunMetricsSetters(),
|
||||
ruleResultService: opts.ruleResultService.getLastRunSetters(),
|
||||
savedObjectsClient,
|
||||
uiSettingsClient,
|
||||
wrappedScopedClusterClient,
|
||||
wrappedSearchSourceClient,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
* 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 { 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 {
|
||||
filterMaintenanceWindows,
|
||||
filterMaintenanceWindowsIds,
|
||||
getMaintenanceWindows,
|
||||
} from './get_maintenance_windows';
|
||||
import { getFakeKibanaRequest } from './rule_loader';
|
||||
import { TaskRunnerContext } from './types';
|
||||
|
||||
const logger = loggingSystemMock.create().get();
|
||||
const mockBasePathService = { set: jest.fn() };
|
||||
const maintenanceWindowClient = maintenanceWindowClientMock.create();
|
||||
|
||||
const apiKey = mockedRawRuleSO.attributes.apiKey!;
|
||||
const ruleId = mockedRule.id;
|
||||
const ruleTypeId = mockedRule.alertTypeId;
|
||||
|
||||
describe('getMaintenanceWindows', () => {
|
||||
let context: TaskRunnerContext;
|
||||
let fakeRequest: CoreKibanaRequest;
|
||||
let contextMock: ReturnType<typeof getTaskRunnerContext>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
contextMock = getTaskRunnerContext();
|
||||
context = contextMock as unknown as TaskRunnerContext;
|
||||
fakeRequest = getFakeKibanaRequest(context, 'default', apiKey);
|
||||
});
|
||||
|
||||
test('returns active maintenance windows if they exist', async () => {
|
||||
const mockMaintenanceWindows = [
|
||||
{
|
||||
...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',
|
||||
},
|
||||
];
|
||||
maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(
|
||||
mockMaintenanceWindows
|
||||
);
|
||||
expect(
|
||||
await getMaintenanceWindows({
|
||||
context,
|
||||
fakeRequest,
|
||||
logger,
|
||||
ruleTypeId,
|
||||
ruleTypeCategory: 'observability',
|
||||
ruleId,
|
||||
})
|
||||
).toEqual(mockMaintenanceWindows);
|
||||
});
|
||||
|
||||
test('filters to rule type category if category IDs array exists', async () => {
|
||||
const mockMaintenanceWindows = [
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id1',
|
||||
categoryIds: [maintenanceWindowCategoryIdTypes.OBSERVABILITY],
|
||||
},
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id2',
|
||||
categoryIds: [maintenanceWindowCategoryIdTypes.SECURITY_SOLUTION],
|
||||
},
|
||||
];
|
||||
maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(
|
||||
mockMaintenanceWindows
|
||||
);
|
||||
expect(
|
||||
await getMaintenanceWindows({
|
||||
context,
|
||||
fakeRequest,
|
||||
logger,
|
||||
ruleTypeId,
|
||||
ruleTypeCategory: 'observability',
|
||||
ruleId,
|
||||
})
|
||||
).toEqual([mockMaintenanceWindows[0]]);
|
||||
});
|
||||
|
||||
test('filters to rule type category and no category IDs', async () => {
|
||||
const mockMaintenanceWindows = [
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id1',
|
||||
categoryIds: [maintenanceWindowCategoryIdTypes.OBSERVABILITY],
|
||||
},
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id2',
|
||||
categoryIds: [maintenanceWindowCategoryIdTypes.SECURITY_SOLUTION],
|
||||
},
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id3',
|
||||
},
|
||||
];
|
||||
maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce(
|
||||
mockMaintenanceWindows
|
||||
);
|
||||
expect(
|
||||
await getMaintenanceWindows({
|
||||
context,
|
||||
fakeRequest,
|
||||
logger,
|
||||
ruleTypeId,
|
||||
ruleTypeCategory: 'observability',
|
||||
ruleId,
|
||||
})
|
||||
).toEqual([mockMaintenanceWindows[0], mockMaintenanceWindows[2]]);
|
||||
});
|
||||
|
||||
test('returns empty array if no active maintenance windows exist', async () => {
|
||||
maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce([]);
|
||||
expect(
|
||||
await getMaintenanceWindows({
|
||||
context,
|
||||
fakeRequest,
|
||||
logger,
|
||||
ruleTypeId,
|
||||
ruleTypeCategory: 'observability',
|
||||
ruleId,
|
||||
})
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('logs error if error loading maintenance window but does not throw', async () => {
|
||||
maintenanceWindowClient.getActiveMaintenanceWindows.mockImplementationOnce(() => {
|
||||
throw new Error('fail fail');
|
||||
});
|
||||
expect(
|
||||
await getMaintenanceWindows({
|
||||
context,
|
||||
fakeRequest,
|
||||
logger,
|
||||
ruleTypeId,
|
||||
ruleTypeCategory: 'observability',
|
||||
ruleId,
|
||||
})
|
||||
).toEqual([]);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`error getting active maintenance window for test:1 fail fail`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterMaintenanceWindows', () => {
|
||||
const mockMaintenanceWindows: MaintenanceWindow[] = [
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id1',
|
||||
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: 'appState',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.action_group': 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id2',
|
||||
},
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id3',
|
||||
},
|
||||
];
|
||||
test('correctly filters maintenance windows when withScopedQuery = true', () => {
|
||||
expect(
|
||||
filterMaintenanceWindows({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
withScopedQuery: true,
|
||||
})
|
||||
).toEqual([mockMaintenanceWindows[0]]);
|
||||
});
|
||||
test('correctly filters maintenance windows when withScopedQuery = false', () => {
|
||||
expect(
|
||||
filterMaintenanceWindows({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
withScopedQuery: false,
|
||||
})
|
||||
).toEqual([mockMaintenanceWindows[1], mockMaintenanceWindows[2]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterMaintenanceWindowsIds', () => {
|
||||
const mockMaintenanceWindows: MaintenanceWindow[] = [
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id1',
|
||||
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: 'appState',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.action_group': 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id2',
|
||||
},
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id3',
|
||||
},
|
||||
];
|
||||
test('correctly filters maintenance windows when withScopedQuery = true', () => {
|
||||
expect(
|
||||
filterMaintenanceWindowsIds({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
withScopedQuery: true,
|
||||
})
|
||||
).toEqual(['test-id1']);
|
||||
});
|
||||
test('correctly filters maintenance windows when withScopedQuery = false', () => {
|
||||
expect(
|
||||
filterMaintenanceWindowsIds({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
withScopedQuery: false,
|
||||
})
|
||||
).toEqual(['test-id2', 'test-id3']);
|
||||
});
|
||||
});
|
||||
|
||||
function getTaskRunnerContext() {
|
||||
return {
|
||||
basePathService: mockBasePathService,
|
||||
getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { TaskRunnerContext } from './types';
|
||||
|
||||
interface GetMaintenanceWindowsOpts {
|
||||
context: TaskRunnerContext;
|
||||
fakeRequest: KibanaRequest;
|
||||
logger: Logger;
|
||||
ruleTypeId: string;
|
||||
ruleTypeCategory: string;
|
||||
ruleId: string;
|
||||
}
|
||||
|
||||
interface FilterMaintenanceWindowsOpts {
|
||||
maintenanceWindows: MaintenanceWindow[];
|
||||
withScopedQuery: boolean;
|
||||
}
|
||||
|
||||
export const filterMaintenanceWindows = ({
|
||||
maintenanceWindows,
|
||||
withScopedQuery,
|
||||
}: FilterMaintenanceWindowsOpts): MaintenanceWindow[] => {
|
||||
const filteredMaintenanceWindows = maintenanceWindows.filter(({ scopedQuery }) => {
|
||||
if (withScopedQuery && scopedQuery) {
|
||||
return true;
|
||||
} else if (!withScopedQuery && !scopedQuery) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return filteredMaintenanceWindows;
|
||||
};
|
||||
|
||||
export const filterMaintenanceWindowsIds = ({
|
||||
maintenanceWindows,
|
||||
withScopedQuery,
|
||||
}: FilterMaintenanceWindowsOpts): string[] => {
|
||||
const filteredMaintenanceWindows = filterMaintenanceWindows({
|
||||
maintenanceWindows,
|
||||
withScopedQuery,
|
||||
});
|
||||
|
||||
return filteredMaintenanceWindows.map(({ id }) => id);
|
||||
};
|
||||
|
||||
export const getMaintenanceWindows = async (
|
||||
opts: GetMaintenanceWindowsOpts
|
||||
): Promise<MaintenanceWindow[]> => {
|
||||
const { context, fakeRequest, logger, ruleTypeId, ruleId, ruleTypeCategory } = opts;
|
||||
const maintenanceWindowClient = context.getMaintenanceWindowClientWithRequest(fakeRequest);
|
||||
|
||||
let activeMaintenanceWindows: MaintenanceWindow[] = [];
|
||||
try {
|
||||
activeMaintenanceWindows = await maintenanceWindowClient.getActiveMaintenanceWindows();
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`error getting active maintenance window for ${ruleTypeId}:${ruleId} ${err.message}`
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
|
@ -9,14 +9,17 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s
|
|||
import { CoreKibanaRequest, SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { getRuleAttributes, getFakeKibanaRequest, validateRule } from './rule_loader';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import {
|
||||
getDecryptedRule,
|
||||
getFakeKibanaRequest,
|
||||
validateRuleAndCreateFakeRequest,
|
||||
} from './rule_loader';
|
||||
import { TaskRunnerContext } from './types';
|
||||
import { ruleTypeRegistryMock } from '../rule_type_registry.mock';
|
||||
import { rulesClientMock } from '../rules_client.mock';
|
||||
import { Rule } from '../types';
|
||||
import { MONITORING_HISTORY_LIMIT, RuleExecutionStatusErrorReasons } from '../../common';
|
||||
import { ErrorWithReason, getReasonFromError } from '../lib/error_with_reason';
|
||||
import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock';
|
||||
import { mockedRawRuleSO, mockedRule } from './fixtures';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects';
|
||||
import { getErrorSource, TaskErrorSource } from '@kbn/task-manager-plugin/server/task_running';
|
||||
|
@ -24,7 +27,6 @@ import { getErrorSource, TaskErrorSource } from '@kbn/task-manager-plugin/server
|
|||
// create mocks
|
||||
const rulesClient = rulesClientMock.create();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
const alertingEventLogger = alertingEventLoggerMock.create();
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
|
||||
const mockBasePathService = { set: jest.fn() };
|
||||
|
||||
|
@ -47,30 +49,23 @@ describe('rule_loader', () => {
|
|||
});
|
||||
|
||||
const getDefaultValidateRuleParams = ({
|
||||
fakeRequest,
|
||||
error,
|
||||
enabled: ruleEnabled = true,
|
||||
params = mockedRule.params,
|
||||
}: {
|
||||
fakeRequest: CoreKibanaRequest<unknown, unknown, unknown>;
|
||||
error?: ErrorWithReason;
|
||||
enabled?: boolean;
|
||||
params?: typeof mockedRule.params;
|
||||
}) => ({
|
||||
paramValidator,
|
||||
ruleId,
|
||||
spaceId,
|
||||
ruleTypeRegistry,
|
||||
alertingEventLogger,
|
||||
ruleData: error
|
||||
? { error }
|
||||
: {
|
||||
data: {
|
||||
indirectParams: { ...mockedRawRuleSO.attributes, enabled: ruleEnabled },
|
||||
rule: { ...mockedRule, params },
|
||||
rulesClient,
|
||||
version: '1',
|
||||
fakeRequest,
|
||||
references: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -93,34 +88,30 @@ describe('rule_loader', () => {
|
|||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('validateRule()', () => {
|
||||
describe('validateRuleAndCreateFakeRequest()', () => {
|
||||
describe('succeeds', () => {
|
||||
test('validates and returns the results', () => {
|
||||
const fakeRequest = getFakeKibanaRequest(context, 'default', apiKey);
|
||||
const result = validateRule({
|
||||
...getDefaultValidateRuleParams({ fakeRequest }),
|
||||
const result = validateRuleAndCreateFakeRequest({
|
||||
...getDefaultValidateRuleParams({}),
|
||||
context,
|
||||
});
|
||||
|
||||
expect(result.apiKey).toBe(apiKey);
|
||||
expect(result.validatedParams).toEqual(ruleParams);
|
||||
expect(result.fakeRequest.headers.authorization).toEqual(`ApiKey ${apiKey}`);
|
||||
expect(result.rule.alertTypeId).toBe(ruleTypeId);
|
||||
expect(result.rule.name).toBe(ruleName);
|
||||
expect(result.rule.params).toBe(ruleParams);
|
||||
expect(result.indirectParams).toEqual(mockedRawRuleSO.attributes);
|
||||
expect(result.version).toBe('1');
|
||||
expect(result.rulesClient).toBe(rulesClient);
|
||||
expect(result.validatedParams).toEqual(ruleParams);
|
||||
expect(result.version).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
test('throws when there is decrypt attributes error', () => {
|
||||
const fakeRequest = getFakeKibanaRequest(context, 'default', apiKey);
|
||||
let outcome = 'success';
|
||||
try {
|
||||
validateRule({
|
||||
validateRuleAndCreateFakeRequest({
|
||||
...getDefaultValidateRuleParams({
|
||||
fakeRequest,
|
||||
error: new ErrorWithReason(RuleExecutionStatusErrorReasons.Decrypt, new Error('test')),
|
||||
}),
|
||||
context,
|
||||
|
@ -133,11 +124,10 @@ describe('rule_loader', () => {
|
|||
});
|
||||
|
||||
test('throws when rule is not enabled', async () => {
|
||||
const fakeRequest = getFakeKibanaRequest(context, 'default', apiKey);
|
||||
let outcome = 'success';
|
||||
try {
|
||||
validateRule({
|
||||
...getDefaultValidateRuleParams({ fakeRequest, enabled: false }),
|
||||
validateRuleAndCreateFakeRequest({
|
||||
...getDefaultValidateRuleParams({ enabled: false }),
|
||||
context,
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -149,15 +139,14 @@ describe('rule_loader', () => {
|
|||
});
|
||||
|
||||
test('throws when rule type is not enabled', async () => {
|
||||
const fakeRequest = getFakeKibanaRequest(context, 'default', apiKey);
|
||||
ruleTypeRegistry.ensureRuleTypeEnabled.mockImplementation(() => {
|
||||
throw new Error('rule-type-not-enabled: 2112');
|
||||
});
|
||||
|
||||
let outcome = 'success';
|
||||
try {
|
||||
validateRule({
|
||||
...getDefaultValidateRuleParams({ fakeRequest }),
|
||||
validateRuleAndCreateFakeRequest({
|
||||
...getDefaultValidateRuleParams({}),
|
||||
context,
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -169,12 +158,13 @@ describe('rule_loader', () => {
|
|||
expect(outcome).toBe('failure');
|
||||
});
|
||||
|
||||
test('throws when rule params fail validation', async () => {
|
||||
const fakeRequest = getFakeKibanaRequest(context, 'default', apiKey);
|
||||
test('test throws when rule params fail validation', async () => {
|
||||
contextMock = getTaskRunnerContext({ bar: 'foo' }, MONITORING_HISTORY_LIMIT);
|
||||
context = contextMock as unknown as TaskRunnerContext;
|
||||
let outcome = 'success';
|
||||
try {
|
||||
validateRule({
|
||||
...getDefaultValidateRuleParams({ fakeRequest, params: { bar: 'foo' } }),
|
||||
validateRuleAndCreateFakeRequest({
|
||||
...getDefaultValidateRuleParams({}),
|
||||
context,
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -190,17 +180,16 @@ describe('rule_loader', () => {
|
|||
describe('getDecryptedAttributes()', () => {
|
||||
test('succeeds with default space', async () => {
|
||||
contextMock.spaceIdToNamespace.mockReturnValue(undefined);
|
||||
const result = await getRuleAttributes(context, ruleId, 'default');
|
||||
const result = await getDecryptedRule(context, ruleId, 'default');
|
||||
|
||||
expect(result.fakeRequest).toEqual(expect.any(CoreKibanaRequest));
|
||||
expect(result.rule.alertTypeId).toBe(ruleTypeId);
|
||||
expect(result.indirectParams).toEqual({
|
||||
...mockedRawRuleSO.attributes,
|
||||
apiKey,
|
||||
enabled,
|
||||
consumer,
|
||||
});
|
||||
expect(result.rulesClient).toBeTruthy();
|
||||
expect(result.references).toEqual([]);
|
||||
expect(result.version).toEqual('1');
|
||||
expect(contextMock.spaceIdToNamespace.mock.calls[0]).toEqual(['default']);
|
||||
|
||||
const esoArgs = encryptedSavedObjects.getDecryptedAsInternalUser.mock.calls[0];
|
||||
|
@ -209,11 +198,8 @@ describe('rule_loader', () => {
|
|||
|
||||
test('succeeds with non-default space', async () => {
|
||||
contextMock.spaceIdToNamespace.mockReturnValue(spaceId);
|
||||
const result = await getRuleAttributes(context, ruleId, spaceId);
|
||||
const result = await getDecryptedRule(context, ruleId, spaceId);
|
||||
|
||||
expect(result.fakeRequest).toEqual(expect.any(CoreKibanaRequest));
|
||||
expect(result.rule.alertTypeId).toBe(ruleTypeId);
|
||||
expect(result.rulesClient).toBeTruthy();
|
||||
expect(contextMock.spaceIdToNamespace.mock.calls[0]).toEqual([spaceId]);
|
||||
expect(result.indirectParams).toEqual({
|
||||
...mockedRawRuleSO.attributes,
|
||||
|
@ -221,6 +207,8 @@ describe('rule_loader', () => {
|
|||
enabled,
|
||||
consumer,
|
||||
});
|
||||
expect(result.references).toEqual([]);
|
||||
expect(result.version).toEqual('1');
|
||||
|
||||
const esoArgs = encryptedSavedObjects.getDecryptedAsInternalUser.mock.calls[0];
|
||||
expect(esoArgs).toEqual([RULE_SAVED_OBJECT_TYPE, ruleId, { namespace: spaceId }]);
|
||||
|
@ -234,7 +222,7 @@ describe('rule_loader', () => {
|
|||
);
|
||||
|
||||
try {
|
||||
await getRuleAttributes(context, ruleId, spaceId);
|
||||
await getDecryptedRule(context, ruleId, spaceId);
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('wops');
|
||||
expect(getErrorSource(e)).toBe(TaskErrorSource.FRAMEWORK);
|
||||
|
@ -247,7 +235,7 @@ describe('rule_loader', () => {
|
|||
);
|
||||
|
||||
try {
|
||||
await getRuleAttributes(context, ruleId, spaceId);
|
||||
await getDecryptedRule(context, ruleId, spaceId);
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('Not Found');
|
||||
expect(getErrorSource(e)).toBe(TaskErrorSource.USER);
|
||||
|
@ -317,7 +305,7 @@ describe('rule_loader', () => {
|
|||
// returns a version of encryptedSavedObjects.getDecryptedAsInternalUser() with provided params
|
||||
function mockGetDecrypted(attributes: { apiKey?: string; enabled: boolean; consumer: string }) {
|
||||
return async (type: string, id: string, opts_: unknown) => {
|
||||
return { id, type, references: [], attributes };
|
||||
return { id, type, references: [], version: '1', attributes };
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,70 +11,68 @@ import {
|
|||
FakeRawRequest,
|
||||
Headers,
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsErrorHelpers,
|
||||
} from '@kbn/core/server';
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import {
|
||||
LoadedIndirectParams,
|
||||
LoadIndirectParamsResult,
|
||||
} from '@kbn/task-manager-plugin/server/task';
|
||||
import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { RunRuleParams, TaskRunnerContext } from './types';
|
||||
import { ErrorWithReason, validateRuleTypeParams } from '../lib';
|
||||
import {
|
||||
RuleExecutionStatusErrorReasons,
|
||||
RawRule,
|
||||
RuleTypeRegistry,
|
||||
RuleTypeParamsValidator,
|
||||
SanitizedRule,
|
||||
RulesClientApi,
|
||||
} from '../types';
|
||||
import { MONITORING_HISTORY_LIMIT, RuleTypeParams } from '../../common';
|
||||
import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event_logger';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects';
|
||||
|
||||
export interface RuleData<Params extends RuleTypeParams> extends LoadedIndirectParams<RawRule> {
|
||||
export interface RuleData extends LoadedIndirectParams<RawRule> {
|
||||
indirectParams: RawRule;
|
||||
rule: SanitizedRule<Params>;
|
||||
version: string | undefined;
|
||||
fakeRequest: CoreKibanaRequest;
|
||||
rulesClient: RulesClientApi;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
export type RuleDataResult<T extends LoadedIndirectParams> = LoadIndirectParamsResult<T>;
|
||||
|
||||
export interface ValidatedRuleData<Params extends RuleTypeParams> extends RuleData<Params> {
|
||||
validatedParams: Params;
|
||||
apiKey: string | null;
|
||||
}
|
||||
|
||||
interface ValidateRuleParams<Params extends RuleTypeParams> {
|
||||
alertingEventLogger: PublicMethodsOf<AlertingEventLogger>;
|
||||
paramValidator?: RuleTypeParamsValidator<Params>;
|
||||
ruleId: string;
|
||||
spaceId: string;
|
||||
interface ValidateRuleAndCreateFakeRequestParams<Params extends RuleTypeParams> {
|
||||
context: TaskRunnerContext;
|
||||
paramValidator?: RuleTypeParamsValidator<Params>;
|
||||
ruleData: RuleDataResult<RuleData>;
|
||||
ruleId: string;
|
||||
ruleTypeRegistry: RuleTypeRegistry;
|
||||
ruleData: RuleDataResult<RuleData<Params>>;
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
export function validateRule<Params extends RuleTypeParams>(
|
||||
params: ValidateRuleParams<Params>
|
||||
): ValidatedRuleData<Params> {
|
||||
/**
|
||||
* With the decrypted rule saved object
|
||||
* - transform from domain model to application model (rule)
|
||||
* - create a fakeRequest object using the rule API key
|
||||
* - get an instance of the RulesClient using the fakeRequest
|
||||
*/
|
||||
export function validateRuleAndCreateFakeRequest<Params extends RuleTypeParams>(
|
||||
params: ValidateRuleAndCreateFakeRequestParams<Params>
|
||||
): RunRuleParams<Params> {
|
||||
// If there was a prior error loading the decrypted rule SO, exit early
|
||||
if (params.ruleData.error) {
|
||||
throw params.ruleData.error;
|
||||
}
|
||||
|
||||
const {
|
||||
ruleData: {
|
||||
data: { indirectParams, rule, fakeRequest, rulesClient, version },
|
||||
},
|
||||
ruleTypeRegistry,
|
||||
context,
|
||||
paramValidator,
|
||||
alertingEventLogger,
|
||||
ruleData: {
|
||||
data: { indirectParams, references, version },
|
||||
},
|
||||
ruleId,
|
||||
ruleTypeRegistry,
|
||||
spaceId,
|
||||
} = params;
|
||||
|
||||
const { enabled, apiKey } = indirectParams;
|
||||
const { enabled, apiKey, alertTypeId: ruleTypeId } = indirectParams;
|
||||
|
||||
if (!enabled) {
|
||||
throw createTaskRunError(
|
||||
|
@ -86,7 +84,17 @@ export function validateRule<Params extends RuleTypeParams>(
|
|||
);
|
||||
}
|
||||
|
||||
alertingEventLogger.setRuleName(rule.name);
|
||||
const fakeRequest = getFakeKibanaRequest(context, spaceId, apiKey);
|
||||
const rulesClient = context.getRulesClientWithRequest(fakeRequest);
|
||||
const rule = rulesClient.getAlertFromRaw({
|
||||
id: ruleId,
|
||||
ruleTypeId,
|
||||
rawRule: indirectParams as RawRule,
|
||||
references,
|
||||
includeLegacyId: false,
|
||||
omitGeneratedValues: false,
|
||||
});
|
||||
|
||||
try {
|
||||
ruleTypeRegistry.ensureRuleTypeEnabled(rule.alertTypeId);
|
||||
} catch (err) {
|
||||
|
@ -114,21 +122,23 @@ export function validateRule<Params extends RuleTypeParams>(
|
|||
}
|
||||
|
||||
return {
|
||||
rule,
|
||||
indirectParams,
|
||||
fakeRequest,
|
||||
apiKey,
|
||||
fakeRequest,
|
||||
rule,
|
||||
rulesClient,
|
||||
validatedParams,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getRuleAttributes<Params extends RuleTypeParams>(
|
||||
/**
|
||||
* Loads the decrypted rule saved object
|
||||
*/
|
||||
export async function getDecryptedRule(
|
||||
context: TaskRunnerContext,
|
||||
ruleId: string,
|
||||
spaceId: string
|
||||
): Promise<RuleData<Params>> {
|
||||
): Promise<RuleData> {
|
||||
const namespace = context.spaceIdToNamespace(spaceId);
|
||||
|
||||
let rawRule: SavedObject<RawRule>;
|
||||
|
@ -146,23 +156,10 @@ export async function getRuleAttributes<Params extends RuleTypeParams>(
|
|||
throw createTaskRunError(e, TaskErrorSource.FRAMEWORK);
|
||||
}
|
||||
|
||||
const fakeRequest = getFakeKibanaRequest(context, spaceId, rawRule.attributes.apiKey);
|
||||
const rulesClient = context.getRulesClientWithRequest(fakeRequest);
|
||||
const rule = rulesClient.getAlertFromRaw({
|
||||
id: ruleId,
|
||||
ruleTypeId: rawRule.attributes.alertTypeId as string,
|
||||
rawRule: rawRule.attributes as RawRule,
|
||||
references: rawRule.references,
|
||||
includeLegacyId: false,
|
||||
omitGeneratedValues: false,
|
||||
});
|
||||
|
||||
return {
|
||||
rule,
|
||||
version: rawRule.version,
|
||||
indirectParams: rawRule.attributes,
|
||||
fakeRequest,
|
||||
rulesClient,
|
||||
references: rawRule.references,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,936 @@
|
|||
/*
|
||||
* 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 { savedObjectsClientMock, uiSettingsServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import {
|
||||
DATE_1970,
|
||||
mockedRule,
|
||||
mockTaskInstance,
|
||||
RULE_ID,
|
||||
RULE_NAME,
|
||||
RULE_TYPE_ID,
|
||||
} from './fixtures';
|
||||
import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock';
|
||||
import { ruleRunMetricsStoreMock } from '../lib/rule_run_metrics_store.mock';
|
||||
import { RuleTypeRunner } from './rule_type_runner';
|
||||
import { TaskRunnerTimer } from './task_runner_timer';
|
||||
import {
|
||||
DEFAULT_FLAPPING_SETTINGS,
|
||||
DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
RecoveredActionGroup,
|
||||
} from '../types';
|
||||
import { TaskRunnerContext } from './types';
|
||||
import { executionContextServiceMock } from '@kbn/core-execution-context-server-mocks';
|
||||
import { SharePluginStart } from '@kbn/share-plugin/server';
|
||||
import { alertsClientMock } from '../alerts_client/alerts_client.mock';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { publicRuleMonitoringServiceMock } from '../monitoring/rule_monitoring_service.mock';
|
||||
import { publicRuleResultServiceMock } from '../monitoring/rule_result_service.mock';
|
||||
import { wrappedScopedClusterClientMock } from '../lib/wrap_scoped_cluster_client.mock';
|
||||
import { wrappedSearchSourceClientMock } from '../lib/wrap_search_source_client.mock';
|
||||
import { NormalizedRuleType } from '../rule_type_registry';
|
||||
|
||||
const alertingEventLogger = alertingEventLoggerMock.create();
|
||||
const alertsClient = alertsClientMock.create();
|
||||
const dataViews = dataViewPluginMocks.createStartContract();
|
||||
const logger = loggingSystemMock.create().get();
|
||||
const publicRuleMonitoringService = publicRuleMonitoringServiceMock.create();
|
||||
const publicRuleResultService = publicRuleResultServiceMock.create();
|
||||
const ruleRunMetricsStore = ruleRunMetricsStoreMock.create();
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
const uiSettingsClient = uiSettingsServiceMock.createClient();
|
||||
const wrappedScopedClusterClient = wrappedScopedClusterClientMock.create();
|
||||
const wrappedSearchSourceClient = wrappedSearchSourceClientMock.create();
|
||||
|
||||
const timer = new TaskRunnerTimer({ logger });
|
||||
const ruleType: jest.Mocked<
|
||||
NormalizedRuleType<{}, {}, { foo: string }, {}, {}, 'default', 'recovered', {}>
|
||||
> = {
|
||||
id: RULE_TYPE_ID,
|
||||
name: 'My test rule',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup],
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
autoRecoverAlerts: true,
|
||||
validate: {
|
||||
params: { validate: (params) => params },
|
||||
},
|
||||
alerts: {
|
||||
context: 'test',
|
||||
mappings: { fieldMap: { field: { type: 'keyword', required: false } } },
|
||||
},
|
||||
validLegacyConsumers: [],
|
||||
};
|
||||
|
||||
describe('RuleTypeRunner', () => {
|
||||
let ruleTypeRunner: RuleTypeRunner<{}, {}, { foo: string }, {}, {}, 'default', 'recovered', {}>;
|
||||
let context: TaskRunnerContext;
|
||||
let contextMock: ReturnType<typeof getTaskRunnerContext>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
contextMock = getTaskRunnerContext();
|
||||
context = contextMock as unknown as TaskRunnerContext;
|
||||
|
||||
ruleTypeRunner = new RuleTypeRunner<
|
||||
{},
|
||||
{},
|
||||
{ foo: string },
|
||||
{},
|
||||
{},
|
||||
'default',
|
||||
'recovered',
|
||||
{}
|
||||
>({
|
||||
context,
|
||||
timer,
|
||||
logger,
|
||||
ruleType,
|
||||
});
|
||||
});
|
||||
|
||||
describe('run', () => {
|
||||
test('should return state when rule type executor succeeds', async () => {
|
||||
ruleType.executor.mockResolvedValueOnce({ state: { foo: 'bar' } });
|
||||
|
||||
const { state, error, stackTrace } = await ruleTypeRunner.run({
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
alertsClient,
|
||||
executionId: 'abc',
|
||||
executorServices: {
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
uiSettingsClient,
|
||||
wrappedScopedClusterClient,
|
||||
wrappedSearchSourceClient,
|
||||
},
|
||||
rule: mockedRule,
|
||||
startedAt: new Date(DATE_1970),
|
||||
state: mockTaskInstance().state,
|
||||
validatedParams: mockedRule.params,
|
||||
});
|
||||
|
||||
expect(ruleType.executor).toHaveBeenCalledWith({
|
||||
executionId: 'abc',
|
||||
services: {
|
||||
alertFactory: alertsClient.factory(),
|
||||
alertsClient: alertsClient.client(),
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient: wrappedScopedClusterClient.client(),
|
||||
searchSourceClient: wrappedSearchSourceClient.searchSourceClient,
|
||||
share: {},
|
||||
shouldStopExecution: expect.any(Function),
|
||||
shouldWriteAlerts: expect.any(Function),
|
||||
uiSettingsClient,
|
||||
},
|
||||
params: mockedRule.params,
|
||||
state: mockTaskInstance().state,
|
||||
startedAt: new Date(DATE_1970),
|
||||
previousStartedAt: null,
|
||||
spaceId: 'default',
|
||||
rule: {
|
||||
id: RULE_ID,
|
||||
name: mockedRule.name,
|
||||
tags: mockedRule.tags,
|
||||
consumer: mockedRule.consumer,
|
||||
producer: ruleType.producer,
|
||||
revision: mockedRule.revision,
|
||||
ruleTypeId: mockedRule.alertTypeId,
|
||||
ruleTypeName: ruleType.name,
|
||||
enabled: mockedRule.enabled,
|
||||
schedule: mockedRule.schedule,
|
||||
actions: mockedRule.actions,
|
||||
createdBy: mockedRule.createdBy,
|
||||
updatedBy: mockedRule.updatedBy,
|
||||
createdAt: mockedRule.createdAt,
|
||||
updatedAt: mockedRule.updatedAt,
|
||||
throttle: mockedRule.throttle,
|
||||
notifyWhen: mockedRule.notifyWhen,
|
||||
muteAll: mockedRule.muteAll,
|
||||
snoozeSchedule: mockedRule.snoozeSchedule,
|
||||
alertDelay: mockedRule.alertDelay,
|
||||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(state).toEqual({ foo: 'bar' });
|
||||
expect(error).toBeUndefined();
|
||||
expect(stackTrace).toBeUndefined();
|
||||
expect(alertsClient.hasReachedAlertLimit).toHaveBeenCalled();
|
||||
expect(alertsClient.checkLimitUsage).toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionSucceeded).toHaveBeenCalledWith(
|
||||
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
|
||||
);
|
||||
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
|
||||
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
notifyOnActionGroupChange: false,
|
||||
maintenanceWindowIds: [],
|
||||
alertDelay: 0,
|
||||
ruleRunMetricsStore,
|
||||
});
|
||||
expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]);
|
||||
expect(alertsClient.logAlerts).toHaveBeenCalledWith({
|
||||
eventLogger: alertingEventLogger,
|
||||
ruleRunMetricsStore,
|
||||
shouldLogAlerts: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return error when checkLimitUsage() throws error', async () => {
|
||||
const err = new Error('limit exceeded');
|
||||
alertsClient.checkLimitUsage.mockImplementationOnce(() => {
|
||||
throw err;
|
||||
});
|
||||
ruleType.executor.mockResolvedValueOnce({ state: { foo: 'bar' } });
|
||||
|
||||
const { state, error, stackTrace } = await ruleTypeRunner.run({
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
alertsClient,
|
||||
executionId: 'abc',
|
||||
executorServices: {
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
uiSettingsClient,
|
||||
wrappedScopedClusterClient,
|
||||
wrappedSearchSourceClient,
|
||||
},
|
||||
rule: mockedRule,
|
||||
startedAt: new Date(DATE_1970),
|
||||
state: mockTaskInstance().state,
|
||||
validatedParams: mockedRule.params,
|
||||
});
|
||||
|
||||
expect(ruleType.executor).toHaveBeenCalledWith({
|
||||
executionId: 'abc',
|
||||
services: {
|
||||
alertFactory: alertsClient.factory(),
|
||||
alertsClient: alertsClient.client(),
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient: wrappedScopedClusterClient.client(),
|
||||
searchSourceClient: wrappedSearchSourceClient.searchSourceClient,
|
||||
share: {},
|
||||
shouldStopExecution: expect.any(Function),
|
||||
shouldWriteAlerts: expect.any(Function),
|
||||
uiSettingsClient,
|
||||
},
|
||||
params: mockedRule.params,
|
||||
state: mockTaskInstance().state,
|
||||
startedAt: new Date(DATE_1970),
|
||||
previousStartedAt: null,
|
||||
spaceId: 'default',
|
||||
rule: {
|
||||
id: RULE_ID,
|
||||
name: mockedRule.name,
|
||||
tags: mockedRule.tags,
|
||||
consumer: mockedRule.consumer,
|
||||
producer: ruleType.producer,
|
||||
revision: mockedRule.revision,
|
||||
ruleTypeId: mockedRule.alertTypeId,
|
||||
ruleTypeName: ruleType.name,
|
||||
enabled: mockedRule.enabled,
|
||||
schedule: mockedRule.schedule,
|
||||
actions: mockedRule.actions,
|
||||
createdBy: mockedRule.createdBy,
|
||||
updatedBy: mockedRule.updatedBy,
|
||||
createdAt: mockedRule.createdAt,
|
||||
updatedAt: mockedRule.updatedAt,
|
||||
throttle: mockedRule.throttle,
|
||||
notifyWhen: mockedRule.notifyWhen,
|
||||
muteAll: mockedRule.muteAll,
|
||||
snoozeSchedule: mockedRule.snoozeSchedule,
|
||||
alertDelay: mockedRule.alertDelay,
|
||||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(state).toBeUndefined();
|
||||
expect(error).toEqual(err);
|
||||
expect(stackTrace).toEqual({ message: err, stackTrace: err.stack });
|
||||
expect(alertsClient.checkLimitUsage).toHaveBeenCalled();
|
||||
expect(alertsClient.hasReachedAlertLimit).toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionSucceeded).not.toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionFailed).toHaveBeenCalledWith(
|
||||
`rule execution failure: test:1: 'rule-name'`,
|
||||
'limit exceeded'
|
||||
);
|
||||
expect(ruleRunMetricsStore.setSearchMetrics).not.toHaveBeenCalled();
|
||||
expect(alertsClient.processAlerts).not.toHaveBeenCalled();
|
||||
expect(alertsClient.persistAlerts).not.toHaveBeenCalled();
|
||||
expect(alertsClient.logAlerts).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should return error when rule type executor throws error', async () => {
|
||||
const err = new Error('executor error');
|
||||
ruleType.executor.mockImplementationOnce(() => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
const { state, error, stackTrace } = await ruleTypeRunner.run({
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
alertsClient,
|
||||
executionId: 'abc',
|
||||
executorServices: {
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
uiSettingsClient,
|
||||
wrappedScopedClusterClient,
|
||||
wrappedSearchSourceClient,
|
||||
},
|
||||
rule: mockedRule,
|
||||
startedAt: new Date(DATE_1970),
|
||||
state: mockTaskInstance().state,
|
||||
validatedParams: mockedRule.params,
|
||||
});
|
||||
|
||||
expect(ruleType.executor).toHaveBeenCalledWith({
|
||||
executionId: 'abc',
|
||||
services: {
|
||||
alertFactory: alertsClient.factory(),
|
||||
alertsClient: alertsClient.client(),
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient: wrappedScopedClusterClient.client(),
|
||||
searchSourceClient: wrappedSearchSourceClient.searchSourceClient,
|
||||
share: {},
|
||||
shouldStopExecution: expect.any(Function),
|
||||
shouldWriteAlerts: expect.any(Function),
|
||||
uiSettingsClient,
|
||||
},
|
||||
params: mockedRule.params,
|
||||
state: mockTaskInstance().state,
|
||||
startedAt: new Date(DATE_1970),
|
||||
previousStartedAt: null,
|
||||
spaceId: 'default',
|
||||
rule: {
|
||||
id: RULE_ID,
|
||||
name: mockedRule.name,
|
||||
tags: mockedRule.tags,
|
||||
consumer: mockedRule.consumer,
|
||||
producer: ruleType.producer,
|
||||
revision: mockedRule.revision,
|
||||
ruleTypeId: mockedRule.alertTypeId,
|
||||
ruleTypeName: ruleType.name,
|
||||
enabled: mockedRule.enabled,
|
||||
schedule: mockedRule.schedule,
|
||||
actions: mockedRule.actions,
|
||||
createdBy: mockedRule.createdBy,
|
||||
updatedBy: mockedRule.updatedBy,
|
||||
createdAt: mockedRule.createdAt,
|
||||
updatedAt: mockedRule.updatedAt,
|
||||
throttle: mockedRule.throttle,
|
||||
notifyWhen: mockedRule.notifyWhen,
|
||||
muteAll: mockedRule.muteAll,
|
||||
snoozeSchedule: mockedRule.snoozeSchedule,
|
||||
alertDelay: mockedRule.alertDelay,
|
||||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(state).toBeUndefined();
|
||||
expect(error).toEqual(err);
|
||||
expect(stackTrace).toEqual({ message: err, stackTrace: err.stack });
|
||||
expect(alertsClient.checkLimitUsage).not.toHaveBeenCalled();
|
||||
expect(alertsClient.hasReachedAlertLimit).toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionSucceeded).not.toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionFailed).toHaveBeenCalledWith(
|
||||
`rule execution failure: test:1: 'rule-name'`,
|
||||
'executor error'
|
||||
);
|
||||
expect(ruleRunMetricsStore.setSearchMetrics).not.toHaveBeenCalled();
|
||||
expect(alertsClient.processAlerts).not.toHaveBeenCalled();
|
||||
expect(alertsClient.persistAlerts).not.toHaveBeenCalled();
|
||||
expect(alertsClient.logAlerts).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle reaching alert limit when rule type executor succeeds', async () => {
|
||||
alertsClient.hasReachedAlertLimit.mockReturnValueOnce(true);
|
||||
ruleType.executor.mockResolvedValueOnce({ state: { foo: 'bar' } });
|
||||
|
||||
const { state, error, stackTrace } = await ruleTypeRunner.run({
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
alertsClient,
|
||||
executionId: 'abc',
|
||||
executorServices: {
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
uiSettingsClient,
|
||||
wrappedScopedClusterClient,
|
||||
wrappedSearchSourceClient,
|
||||
},
|
||||
rule: mockedRule,
|
||||
startedAt: new Date(DATE_1970),
|
||||
state: mockTaskInstance().state,
|
||||
validatedParams: mockedRule.params,
|
||||
});
|
||||
|
||||
expect(ruleType.executor).toHaveBeenCalledWith({
|
||||
executionId: 'abc',
|
||||
services: {
|
||||
alertFactory: alertsClient.factory(),
|
||||
alertsClient: alertsClient.client(),
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient: wrappedScopedClusterClient.client(),
|
||||
searchSourceClient: wrappedSearchSourceClient.searchSourceClient,
|
||||
share: {},
|
||||
shouldStopExecution: expect.any(Function),
|
||||
shouldWriteAlerts: expect.any(Function),
|
||||
uiSettingsClient,
|
||||
},
|
||||
params: mockedRule.params,
|
||||
state: mockTaskInstance().state,
|
||||
startedAt: new Date(DATE_1970),
|
||||
previousStartedAt: null,
|
||||
spaceId: 'default',
|
||||
rule: {
|
||||
id: RULE_ID,
|
||||
name: mockedRule.name,
|
||||
tags: mockedRule.tags,
|
||||
consumer: mockedRule.consumer,
|
||||
producer: ruleType.producer,
|
||||
revision: mockedRule.revision,
|
||||
ruleTypeId: mockedRule.alertTypeId,
|
||||
ruleTypeName: ruleType.name,
|
||||
enabled: mockedRule.enabled,
|
||||
schedule: mockedRule.schedule,
|
||||
actions: mockedRule.actions,
|
||||
createdBy: mockedRule.createdBy,
|
||||
updatedBy: mockedRule.updatedBy,
|
||||
createdAt: mockedRule.createdAt,
|
||||
updatedAt: mockedRule.updatedAt,
|
||||
throttle: mockedRule.throttle,
|
||||
notifyWhen: mockedRule.notifyWhen,
|
||||
muteAll: mockedRule.muteAll,
|
||||
snoozeSchedule: mockedRule.snoozeSchedule,
|
||||
alertDelay: mockedRule.alertDelay,
|
||||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`rule execution generated greater than 100 alerts: test:1: 'rule-name'`
|
||||
);
|
||||
expect(ruleRunMetricsStore.setHasReachedAlertLimit).toHaveBeenCalledWith(true);
|
||||
expect(state).toEqual({ foo: 'bar' });
|
||||
expect(error).toBeUndefined();
|
||||
expect(stackTrace).toBeUndefined();
|
||||
expect(alertsClient.hasReachedAlertLimit).toHaveBeenCalled();
|
||||
expect(alertsClient.checkLimitUsage).toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionSucceeded).toHaveBeenCalledWith(
|
||||
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
|
||||
);
|
||||
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
|
||||
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
notifyOnActionGroupChange: false,
|
||||
maintenanceWindowIds: [],
|
||||
alertDelay: 0,
|
||||
ruleRunMetricsStore,
|
||||
});
|
||||
expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]);
|
||||
expect(alertsClient.logAlerts).toHaveBeenCalledWith({
|
||||
eventLogger: alertingEventLogger,
|
||||
ruleRunMetricsStore,
|
||||
shouldLogAlerts: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle reaching alert limit when rule type executor throws error', async () => {
|
||||
alertsClient.hasReachedAlertLimit.mockReturnValueOnce(true);
|
||||
alertsClient.hasReachedAlertLimit.mockReturnValueOnce(true);
|
||||
const err = new Error('executor error');
|
||||
ruleType.executor.mockImplementationOnce(() => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
const { state, error, stackTrace } = await ruleTypeRunner.run({
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
alertsClient,
|
||||
executionId: 'abc',
|
||||
executorServices: {
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
uiSettingsClient,
|
||||
wrappedScopedClusterClient,
|
||||
wrappedSearchSourceClient,
|
||||
},
|
||||
rule: mockedRule,
|
||||
startedAt: new Date(DATE_1970),
|
||||
state: mockTaskInstance().state,
|
||||
validatedParams: mockedRule.params,
|
||||
});
|
||||
|
||||
expect(ruleType.executor).toHaveBeenCalledWith({
|
||||
executionId: 'abc',
|
||||
services: {
|
||||
alertFactory: alertsClient.factory(),
|
||||
alertsClient: alertsClient.client(),
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient: wrappedScopedClusterClient.client(),
|
||||
searchSourceClient: wrappedSearchSourceClient.searchSourceClient,
|
||||
share: {},
|
||||
shouldStopExecution: expect.any(Function),
|
||||
shouldWriteAlerts: expect.any(Function),
|
||||
uiSettingsClient,
|
||||
},
|
||||
params: mockedRule.params,
|
||||
state: mockTaskInstance().state,
|
||||
startedAt: new Date(DATE_1970),
|
||||
previousStartedAt: null,
|
||||
spaceId: 'default',
|
||||
rule: {
|
||||
id: RULE_ID,
|
||||
name: mockedRule.name,
|
||||
tags: mockedRule.tags,
|
||||
consumer: mockedRule.consumer,
|
||||
producer: ruleType.producer,
|
||||
revision: mockedRule.revision,
|
||||
ruleTypeId: mockedRule.alertTypeId,
|
||||
ruleTypeName: ruleType.name,
|
||||
enabled: mockedRule.enabled,
|
||||
schedule: mockedRule.schedule,
|
||||
actions: mockedRule.actions,
|
||||
createdBy: mockedRule.createdBy,
|
||||
updatedBy: mockedRule.updatedBy,
|
||||
createdAt: mockedRule.createdAt,
|
||||
updatedAt: mockedRule.updatedAt,
|
||||
throttle: mockedRule.throttle,
|
||||
notifyWhen: mockedRule.notifyWhen,
|
||||
muteAll: mockedRule.muteAll,
|
||||
snoozeSchedule: mockedRule.snoozeSchedule,
|
||||
alertDelay: mockedRule.alertDelay,
|
||||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`rule execution generated greater than 100 alerts: test:1: 'rule-name'`
|
||||
);
|
||||
expect(ruleRunMetricsStore.setHasReachedAlertLimit).toHaveBeenCalledWith(true);
|
||||
expect(state).toBeUndefined();
|
||||
expect(error).toBeUndefined();
|
||||
expect(stackTrace).toBeUndefined();
|
||||
expect(alertsClient.checkLimitUsage).not.toHaveBeenCalled();
|
||||
expect(alertsClient.hasReachedAlertLimit).toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionSucceeded).toHaveBeenCalledWith(
|
||||
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
|
||||
);
|
||||
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
|
||||
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
notifyOnActionGroupChange: false,
|
||||
maintenanceWindowIds: [],
|
||||
alertDelay: 0,
|
||||
ruleRunMetricsStore,
|
||||
});
|
||||
expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]);
|
||||
expect(alertsClient.logAlerts).toHaveBeenCalledWith({
|
||||
eventLogger: alertingEventLogger,
|
||||
ruleRunMetricsStore,
|
||||
shouldLogAlerts: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw error if alertsClient.processAlerts throws error', async () => {
|
||||
alertsClient.processAlerts.mockImplementationOnce(() => {
|
||||
throw new Error('process alerts failed');
|
||||
});
|
||||
|
||||
ruleType.executor.mockResolvedValueOnce({ state: { foo: 'bar' } });
|
||||
|
||||
await expect(
|
||||
ruleTypeRunner.run({
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
alertsClient,
|
||||
executionId: 'abc',
|
||||
executorServices: {
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
uiSettingsClient,
|
||||
wrappedScopedClusterClient,
|
||||
wrappedSearchSourceClient,
|
||||
},
|
||||
rule: mockedRule,
|
||||
startedAt: new Date(DATE_1970),
|
||||
state: mockTaskInstance().state,
|
||||
validatedParams: mockedRule.params,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"process alerts failed"`);
|
||||
|
||||
expect(ruleType.executor).toHaveBeenCalledWith({
|
||||
executionId: 'abc',
|
||||
services: {
|
||||
alertFactory: alertsClient.factory(),
|
||||
alertsClient: alertsClient.client(),
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient: wrappedScopedClusterClient.client(),
|
||||
searchSourceClient: wrappedSearchSourceClient.searchSourceClient,
|
||||
share: {},
|
||||
shouldStopExecution: expect.any(Function),
|
||||
shouldWriteAlerts: expect.any(Function),
|
||||
uiSettingsClient,
|
||||
},
|
||||
params: mockedRule.params,
|
||||
state: mockTaskInstance().state,
|
||||
startedAt: new Date(DATE_1970),
|
||||
previousStartedAt: null,
|
||||
spaceId: 'default',
|
||||
rule: {
|
||||
id: RULE_ID,
|
||||
name: mockedRule.name,
|
||||
tags: mockedRule.tags,
|
||||
consumer: mockedRule.consumer,
|
||||
producer: ruleType.producer,
|
||||
revision: mockedRule.revision,
|
||||
ruleTypeId: mockedRule.alertTypeId,
|
||||
ruleTypeName: ruleType.name,
|
||||
enabled: mockedRule.enabled,
|
||||
schedule: mockedRule.schedule,
|
||||
actions: mockedRule.actions,
|
||||
createdBy: mockedRule.createdBy,
|
||||
updatedBy: mockedRule.updatedBy,
|
||||
createdAt: mockedRule.createdAt,
|
||||
updatedAt: mockedRule.updatedAt,
|
||||
throttle: mockedRule.throttle,
|
||||
notifyWhen: mockedRule.notifyWhen,
|
||||
muteAll: mockedRule.muteAll,
|
||||
snoozeSchedule: mockedRule.snoozeSchedule,
|
||||
alertDelay: mockedRule.alertDelay,
|
||||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(alertsClient.hasReachedAlertLimit).toHaveBeenCalled();
|
||||
expect(alertsClient.checkLimitUsage).toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionSucceeded).toHaveBeenCalledWith(
|
||||
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
|
||||
);
|
||||
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
|
||||
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
notifyOnActionGroupChange: false,
|
||||
maintenanceWindowIds: [],
|
||||
alertDelay: 0,
|
||||
ruleRunMetricsStore,
|
||||
});
|
||||
expect(alertsClient.persistAlerts).not.toHaveBeenCalled();
|
||||
expect(alertsClient.logAlerts).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw error if alertsClient.persistAlerts throws error', async () => {
|
||||
alertsClient.persistAlerts.mockImplementationOnce(() => {
|
||||
throw new Error('persist alerts failed');
|
||||
});
|
||||
|
||||
ruleType.executor.mockResolvedValueOnce({ state: { foo: 'bar' } });
|
||||
|
||||
await expect(
|
||||
ruleTypeRunner.run({
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
alertsClient,
|
||||
executionId: 'abc',
|
||||
executorServices: {
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
uiSettingsClient,
|
||||
wrappedScopedClusterClient,
|
||||
wrappedSearchSourceClient,
|
||||
},
|
||||
rule: mockedRule,
|
||||
startedAt: new Date(DATE_1970),
|
||||
state: mockTaskInstance().state,
|
||||
validatedParams: mockedRule.params,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"persist alerts failed"`);
|
||||
|
||||
expect(ruleType.executor).toHaveBeenCalledWith({
|
||||
executionId: 'abc',
|
||||
services: {
|
||||
alertFactory: alertsClient.factory(),
|
||||
alertsClient: alertsClient.client(),
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient: wrappedScopedClusterClient.client(),
|
||||
searchSourceClient: wrappedSearchSourceClient.searchSourceClient,
|
||||
share: {},
|
||||
shouldStopExecution: expect.any(Function),
|
||||
shouldWriteAlerts: expect.any(Function),
|
||||
uiSettingsClient,
|
||||
},
|
||||
params: mockedRule.params,
|
||||
state: mockTaskInstance().state,
|
||||
startedAt: new Date(DATE_1970),
|
||||
previousStartedAt: null,
|
||||
spaceId: 'default',
|
||||
rule: {
|
||||
id: RULE_ID,
|
||||
name: mockedRule.name,
|
||||
tags: mockedRule.tags,
|
||||
consumer: mockedRule.consumer,
|
||||
producer: ruleType.producer,
|
||||
revision: mockedRule.revision,
|
||||
ruleTypeId: mockedRule.alertTypeId,
|
||||
ruleTypeName: ruleType.name,
|
||||
enabled: mockedRule.enabled,
|
||||
schedule: mockedRule.schedule,
|
||||
actions: mockedRule.actions,
|
||||
createdBy: mockedRule.createdBy,
|
||||
updatedBy: mockedRule.updatedBy,
|
||||
createdAt: mockedRule.createdAt,
|
||||
updatedAt: mockedRule.updatedAt,
|
||||
throttle: mockedRule.throttle,
|
||||
notifyWhen: mockedRule.notifyWhen,
|
||||
muteAll: mockedRule.muteAll,
|
||||
snoozeSchedule: mockedRule.snoozeSchedule,
|
||||
alertDelay: mockedRule.alertDelay,
|
||||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(alertsClient.hasReachedAlertLimit).toHaveBeenCalled();
|
||||
expect(alertsClient.checkLimitUsage).toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionSucceeded).toHaveBeenCalledWith(
|
||||
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
|
||||
);
|
||||
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
|
||||
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
notifyOnActionGroupChange: false,
|
||||
maintenanceWindowIds: [],
|
||||
alertDelay: 0,
|
||||
ruleRunMetricsStore,
|
||||
});
|
||||
expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]);
|
||||
expect(alertsClient.logAlerts).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw error if alertsClient.logAlerts throws error', async () => {
|
||||
alertsClient.logAlerts.mockImplementationOnce(() => {
|
||||
throw new Error('log alerts failed');
|
||||
});
|
||||
|
||||
ruleType.executor.mockResolvedValueOnce({ state: { foo: 'bar' } });
|
||||
|
||||
await expect(
|
||||
ruleTypeRunner.run({
|
||||
context: {
|
||||
alertingEventLogger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
queryDelaySettings: DEFAULT_QUERY_DELAY_SETTINGS,
|
||||
ruleId: RULE_ID,
|
||||
ruleLogPrefix: `${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`,
|
||||
ruleRunMetricsStore,
|
||||
spaceId: 'default',
|
||||
},
|
||||
alertsClient,
|
||||
executionId: 'abc',
|
||||
executorServices: {
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
uiSettingsClient,
|
||||
wrappedScopedClusterClient,
|
||||
wrappedSearchSourceClient,
|
||||
},
|
||||
rule: mockedRule,
|
||||
startedAt: new Date(DATE_1970),
|
||||
state: mockTaskInstance().state,
|
||||
validatedParams: mockedRule.params,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"log alerts failed"`);
|
||||
|
||||
expect(ruleType.executor).toHaveBeenCalledWith({
|
||||
executionId: 'abc',
|
||||
services: {
|
||||
alertFactory: alertsClient.factory(),
|
||||
alertsClient: alertsClient.client(),
|
||||
dataViews,
|
||||
ruleMonitoringService: publicRuleMonitoringService,
|
||||
ruleResultService: publicRuleResultService,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient: wrappedScopedClusterClient.client(),
|
||||
searchSourceClient: wrappedSearchSourceClient.searchSourceClient,
|
||||
share: {},
|
||||
shouldStopExecution: expect.any(Function),
|
||||
shouldWriteAlerts: expect.any(Function),
|
||||
uiSettingsClient,
|
||||
},
|
||||
params: mockedRule.params,
|
||||
state: mockTaskInstance().state,
|
||||
startedAt: new Date(DATE_1970),
|
||||
previousStartedAt: null,
|
||||
spaceId: 'default',
|
||||
rule: {
|
||||
id: RULE_ID,
|
||||
name: mockedRule.name,
|
||||
tags: mockedRule.tags,
|
||||
consumer: mockedRule.consumer,
|
||||
producer: ruleType.producer,
|
||||
revision: mockedRule.revision,
|
||||
ruleTypeId: mockedRule.alertTypeId,
|
||||
ruleTypeName: ruleType.name,
|
||||
enabled: mockedRule.enabled,
|
||||
schedule: mockedRule.schedule,
|
||||
actions: mockedRule.actions,
|
||||
createdBy: mockedRule.createdBy,
|
||||
updatedBy: mockedRule.updatedBy,
|
||||
createdAt: mockedRule.createdAt,
|
||||
updatedAt: mockedRule.updatedAt,
|
||||
throttle: mockedRule.throttle,
|
||||
notifyWhen: mockedRule.notifyWhen,
|
||||
muteAll: mockedRule.muteAll,
|
||||
snoozeSchedule: mockedRule.snoozeSchedule,
|
||||
alertDelay: mockedRule.alertDelay,
|
||||
},
|
||||
logger,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
getTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(alertsClient.hasReachedAlertLimit).toHaveBeenCalled();
|
||||
expect(alertsClient.checkLimitUsage).toHaveBeenCalled();
|
||||
expect(alertingEventLogger.setExecutionSucceeded).toHaveBeenCalledWith(
|
||||
`rule executed: ${RULE_TYPE_ID}:${RULE_ID}: '${RULE_NAME}'`
|
||||
);
|
||||
expect(ruleRunMetricsStore.setSearchMetrics).toHaveBeenCalled();
|
||||
expect(alertsClient.processAlerts).toHaveBeenCalledWith({
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
notifyOnActionGroupChange: false,
|
||||
maintenanceWindowIds: [],
|
||||
alertDelay: 0,
|
||||
ruleRunMetricsStore,
|
||||
});
|
||||
expect(alertsClient.persistAlerts).toHaveBeenCalledWith([]);
|
||||
expect(alertsClient.logAlerts).toHaveBeenCalledWith({
|
||||
eventLogger: alertingEventLogger,
|
||||
ruleRunMetricsStore,
|
||||
shouldLogAlerts: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// return enough of TaskRunnerContext that RuleTypeRunner needs
|
||||
function getTaskRunnerContext() {
|
||||
return {
|
||||
maxAlerts: 100,
|
||||
executionContext: executionContextServiceMock.createInternalStartContract(),
|
||||
share: {} as SharePluginStart,
|
||||
};
|
||||
}
|
327
x-pack/plugins/alerting/server/task_runner/rule_type_runner.ts
Normal file
327
x-pack/plugins/alerting/server/task_runner/rule_type_runner.ts
Normal file
|
@ -0,0 +1,327 @@
|
|||
/*
|
||||
* 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 { AlertInstanceContext, AlertInstanceState, RuleTaskState } from '@kbn/alerting-state-types';
|
||||
import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server';
|
||||
import { some } from 'lodash';
|
||||
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';
|
||||
import {
|
||||
RuleAlertData,
|
||||
RuleExecutionStatusErrorReasons,
|
||||
RuleNotifyWhen,
|
||||
RuleTypeParams,
|
||||
RuleTypeState,
|
||||
SanitizedRule,
|
||||
} from '../types';
|
||||
import { ExecutorServices } from './get_executor_services';
|
||||
import { StackTraceLog } from './task_runner';
|
||||
import { TaskRunnerTimer, TaskRunnerTimerSpan } from './task_runner_timer';
|
||||
import { RuleTypeRunnerContext, TaskRunnerContext } from './types';
|
||||
|
||||
interface ConstructorOpts<
|
||||
Params extends RuleTypeParams,
|
||||
ExtractedParams extends RuleTypeParams,
|
||||
RuleState extends RuleTypeState,
|
||||
State extends AlertInstanceState,
|
||||
Context extends AlertInstanceContext,
|
||||
ActionGroupIds extends string,
|
||||
RecoveryActionGroupId extends string,
|
||||
AlertData extends RuleAlertData
|
||||
> {
|
||||
context: TaskRunnerContext;
|
||||
timer: TaskRunnerTimer;
|
||||
logger: Logger;
|
||||
ruleType: NormalizedRuleType<
|
||||
Params,
|
||||
ExtractedParams,
|
||||
RuleState,
|
||||
State,
|
||||
Context,
|
||||
ActionGroupIds,
|
||||
RecoveryActionGroupId,
|
||||
AlertData
|
||||
>;
|
||||
}
|
||||
|
||||
interface RunOpts<
|
||||
Params extends RuleTypeParams,
|
||||
State extends AlertInstanceState,
|
||||
Context extends AlertInstanceContext,
|
||||
ActionGroupIds extends string,
|
||||
RecoveryActionGroupId extends string,
|
||||
AlertData extends RuleAlertData
|
||||
> {
|
||||
context: RuleTypeRunnerContext;
|
||||
alertsClient: IAlertsClient<AlertData, State, Context, ActionGroupIds, RecoveryActionGroupId>;
|
||||
executionId: string;
|
||||
executorServices: ExecutorServices & {
|
||||
getTimeRangeFn?: (
|
||||
timeWindow: string,
|
||||
nowDate?: string
|
||||
) => { dateStart: string; dateEnd: string };
|
||||
};
|
||||
maintenanceWindows?: MaintenanceWindow[];
|
||||
maintenanceWindowsWithoutScopedQueryIds?: string[];
|
||||
rule: SanitizedRule<Params>;
|
||||
startedAt: Date | null;
|
||||
state: RuleTaskState;
|
||||
validatedParams: Params;
|
||||
}
|
||||
|
||||
interface RunResult {
|
||||
state: RuleTypeState | undefined;
|
||||
error?: Error;
|
||||
stackTrace?: StackTraceLog | null;
|
||||
}
|
||||
|
||||
export class RuleTypeRunner<
|
||||
Params extends RuleTypeParams,
|
||||
ExtractedParams extends RuleTypeParams,
|
||||
RuleState extends RuleTypeState,
|
||||
State extends AlertInstanceState,
|
||||
Context extends AlertInstanceContext,
|
||||
ActionGroupIds extends string,
|
||||
RecoveryActionGroupId extends string,
|
||||
AlertData extends RuleAlertData
|
||||
> {
|
||||
private cancelled: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly options: ConstructorOpts<
|
||||
Params,
|
||||
ExtractedParams,
|
||||
RuleState,
|
||||
State,
|
||||
Context,
|
||||
ActionGroupIds,
|
||||
RecoveryActionGroupId,
|
||||
AlertData
|
||||
>
|
||||
) {}
|
||||
|
||||
public cancelRun() {
|
||||
this.cancelled = true;
|
||||
}
|
||||
|
||||
public async run({
|
||||
context,
|
||||
alertsClient,
|
||||
executionId,
|
||||
executorServices,
|
||||
maintenanceWindows = [],
|
||||
maintenanceWindowsWithoutScopedQueryIds = [],
|
||||
rule,
|
||||
startedAt,
|
||||
state,
|
||||
validatedParams,
|
||||
}: RunOpts<
|
||||
Params,
|
||||
State,
|
||||
Context,
|
||||
ActionGroupIds,
|
||||
RecoveryActionGroupId,
|
||||
AlertData
|
||||
>): Promise<RunResult> {
|
||||
const {
|
||||
alertTypeId: ruleTypeId,
|
||||
consumer,
|
||||
schedule,
|
||||
throttle = null,
|
||||
notifyWhen = null,
|
||||
name,
|
||||
tags,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
enabled,
|
||||
actions,
|
||||
muteAll,
|
||||
revision,
|
||||
snoozeSchedule,
|
||||
alertDelay,
|
||||
} = rule;
|
||||
|
||||
const { alertTypeState: ruleTypeState = {}, previousStartedAt } = state;
|
||||
|
||||
const { updatedRuleTypeState, error, stackTrace } = await this.options.timer.runWithTimer(
|
||||
TaskRunnerTimerSpan.RuleTypeRun,
|
||||
async () => {
|
||||
const checkHasReachedAlertLimit = () => {
|
||||
const reachedLimit = alertsClient.hasReachedAlertLimit() || false;
|
||||
if (reachedLimit) {
|
||||
this.options.logger.warn(
|
||||
`rule execution generated greater than ${this.options.context.maxAlerts} alerts: ${context.ruleLogPrefix}`
|
||||
);
|
||||
context.ruleRunMetricsStore.setHasReachedAlertLimit(true);
|
||||
}
|
||||
return reachedLimit;
|
||||
};
|
||||
|
||||
let executorResult: { state: RuleState } | undefined;
|
||||
try {
|
||||
const ctx = {
|
||||
type: 'alert',
|
||||
name: `execute ${ruleTypeId}`,
|
||||
id: context.ruleId,
|
||||
description: `execute [${ruleTypeId}] with name [${name}] in [${
|
||||
context.namespace ?? DEFAULT_NAMESPACE_STRING
|
||||
}] namespace`,
|
||||
};
|
||||
executorResult = await this.options.context.executionContext.withContext(ctx, () =>
|
||||
this.options.ruleType.executor({
|
||||
executionId,
|
||||
services: {
|
||||
alertFactory: alertsClient.factory(),
|
||||
alertsClient: alertsClient.client(),
|
||||
dataViews: executorServices.dataViews,
|
||||
ruleMonitoringService: executorServices.ruleMonitoringService,
|
||||
ruleResultService: executorServices.ruleResultService,
|
||||
savedObjectsClient: executorServices.savedObjectsClient,
|
||||
scopedClusterClient: executorServices.wrappedScopedClusterClient.client(),
|
||||
searchSourceClient: executorServices.wrappedSearchSourceClient.searchSourceClient,
|
||||
share: this.options.context.share,
|
||||
shouldStopExecution: () => this.cancelled,
|
||||
shouldWriteAlerts: () => this.shouldLogAndScheduleActionsForAlerts(),
|
||||
uiSettingsClient: executorServices.uiSettingsClient,
|
||||
},
|
||||
params: validatedParams,
|
||||
state: ruleTypeState as RuleState,
|
||||
startedAt: startedAt!,
|
||||
previousStartedAt: previousStartedAt ? new Date(previousStartedAt) : null,
|
||||
spaceId: context.spaceId,
|
||||
namespace: context.namespace,
|
||||
rule: {
|
||||
id: context.ruleId,
|
||||
name,
|
||||
tags,
|
||||
consumer,
|
||||
producer: this.options.ruleType.producer,
|
||||
revision,
|
||||
ruleTypeId,
|
||||
ruleTypeName: this.options.ruleType.name,
|
||||
enabled,
|
||||
schedule,
|
||||
actions,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
throttle,
|
||||
notifyWhen,
|
||||
muteAll,
|
||||
snoozeSchedule,
|
||||
alertDelay,
|
||||
},
|
||||
logger: this.options.logger,
|
||||
flappingSettings: context.flappingSettings,
|
||||
// passed in so the rule registry knows about maintenance windows
|
||||
...(maintenanceWindowsWithoutScopedQueryIds.length
|
||||
? { maintenanceWindowIds: maintenanceWindowsWithoutScopedQueryIds }
|
||||
: {}),
|
||||
getTimeRange: (timeWindow) =>
|
||||
getTimeRange(this.options.logger, context.queryDelaySettings, timeWindow),
|
||||
})
|
||||
);
|
||||
// Rule type execution has successfully completed
|
||||
// Check that the rule type either never requested the max alerts limit
|
||||
// or requested it and then reported back whether it exceeded the limit
|
||||
// If neither of these apply, this check will throw an error
|
||||
// These errors should show up during rule type development
|
||||
alertsClient.checkLimitUsage();
|
||||
} catch (err) {
|
||||
// Check if this error is due to reaching the alert limit
|
||||
if (!checkHasReachedAlertLimit()) {
|
||||
context.alertingEventLogger.setExecutionFailed(
|
||||
`rule execution failure: ${context.ruleLogPrefix}`,
|
||||
err.message
|
||||
);
|
||||
return {
|
||||
error: createTaskRunError(
|
||||
new ErrorWithReason(RuleExecutionStatusErrorReasons.Execute, err),
|
||||
TaskErrorSource.USER
|
||||
),
|
||||
stackTrace: { message: err, stackTrace: err.stack },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the rule type has reported that it reached the alert limit
|
||||
checkHasReachedAlertLimit();
|
||||
|
||||
context.alertingEventLogger.setExecutionSucceeded(
|
||||
`rule executed: ${context.ruleLogPrefix}`
|
||||
);
|
||||
context.ruleRunMetricsStore.setSearchMetrics([
|
||||
executorServices.wrappedScopedClusterClient.getMetrics(),
|
||||
executorServices.wrappedSearchSourceClient.getMetrics(),
|
||||
]);
|
||||
|
||||
return {
|
||||
updatedRuleTypeState: executorResult?.state || undefined,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { state: undefined, error, stackTrace };
|
||||
}
|
||||
|
||||
await this.options.timer.runWithTimer(TaskRunnerTimerSpan.ProcessAlerts, async () => {
|
||||
alertsClient.processAlerts({
|
||||
flappingSettings: context.flappingSettings,
|
||||
notifyOnActionGroupChange:
|
||||
notifyWhen === RuleNotifyWhen.CHANGE ||
|
||||
some(actions, (action) => action.frequency?.notifyWhen === RuleNotifyWhen.CHANGE),
|
||||
maintenanceWindowIds: maintenanceWindowsWithoutScopedQueryIds,
|
||||
alertDelay: alertDelay?.active ?? 0,
|
||||
ruleRunMetricsStore: context.ruleRunMetricsStore,
|
||||
});
|
||||
});
|
||||
|
||||
await this.options.timer.runWithTimer(TaskRunnerTimerSpan.PersistAlerts, async () => {
|
||||
const updateAlertsMaintenanceWindowResult = await alertsClient.persistAlerts(
|
||||
maintenanceWindows
|
||||
);
|
||||
|
||||
// Set the event log MW ids again, this time including the ids that matched alerts with
|
||||
// scoped query
|
||||
if (updateAlertsMaintenanceWindowResult?.maintenanceWindowIds) {
|
||||
context.alertingEventLogger.setMaintenanceWindowIds(
|
||||
updateAlertsMaintenanceWindowResult.maintenanceWindowIds
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
alertsClient.logAlerts({
|
||||
eventLogger: context.alertingEventLogger,
|
||||
ruleRunMetricsStore: context.ruleRunMetricsStore,
|
||||
shouldLogAlerts: this.shouldLogAndScheduleActionsForAlerts(),
|
||||
});
|
||||
|
||||
return { state: updatedRuleTypeState };
|
||||
}
|
||||
|
||||
private shouldLogAndScheduleActionsForAlerts() {
|
||||
// if execution hasn't been cancelled, return true
|
||||
if (!this.cancelled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if execution has been cancelled, return true if EITHER alerting config or rule type indicate to proceed with scheduling actions
|
||||
return (
|
||||
!this.options.context.cancelAlertsOnRuleTimeout ||
|
||||
!this.options.ruleType.cancelAlertsOnRuleTimeout
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ import {
|
|||
isUnrecoverableError,
|
||||
TaskErrorSource,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { TaskRunnerContext } from './types';
|
||||
import { TaskRunner } from './task_runner';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import {
|
||||
|
@ -834,7 +834,7 @@ describe('Task Runner', () => {
|
|||
mockMaintenanceWindows
|
||||
);
|
||||
|
||||
alertsClient.updateAlertsMaintenanceWindowIdByScopedQuery.mockResolvedValue({
|
||||
alertsClient.persistAlerts.mockResolvedValue({
|
||||
alertIds: [],
|
||||
maintenanceWindowIds: ['test-id-1', 'test-id-2'],
|
||||
});
|
||||
|
@ -855,12 +855,7 @@ describe('Task Runner', () => {
|
|||
await taskRunner.run();
|
||||
expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(alertsClient.updateAlertsMaintenanceWindowIdByScopedQuery).toHaveBeenLastCalledWith({
|
||||
executionUuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
ruleId: '1',
|
||||
spaceId: 'default',
|
||||
});
|
||||
expect(alertsClient.persistAlerts).toHaveBeenLastCalledWith(mockMaintenanceWindows);
|
||||
|
||||
expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith(['test-id-1']);
|
||||
expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith([
|
||||
|
@ -1883,11 +1878,11 @@ describe('Task Runner', () => {
|
|||
inMemoryMetrics,
|
||||
});
|
||||
expect(AlertingEventLogger).toHaveBeenCalled();
|
||||
rulesClient.getAlertFromRaw.mockReturnValue({
|
||||
...(mockedRuleTypeSavedObject as Rule),
|
||||
schedule: { interval: '30s' },
|
||||
rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({
|
||||
...mockedRawRuleSO,
|
||||
attributes: { ...mockedRawRuleSO.attributes, schedule: { interval: '30s' } },
|
||||
});
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO);
|
||||
|
||||
const runnerResult = await taskRunner.run();
|
||||
expect(runnerResult).toEqual(
|
||||
|
@ -1954,7 +1949,7 @@ describe('Task Runner', () => {
|
|||
const taskRunError = new Error(GENERIC_ERROR_MESSAGE);
|
||||
|
||||
// used in loadIndirectParams() which is called to load rule data
|
||||
rulesClient.getAlertFromRaw.mockImplementation(() => {
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockImplementation(() => {
|
||||
throw taskRunError;
|
||||
});
|
||||
|
||||
|
@ -1967,8 +1962,6 @@ describe('Task Runner', () => {
|
|||
});
|
||||
expect(AlertingEventLogger).toHaveBeenCalled();
|
||||
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO);
|
||||
|
||||
const runnerResult = await taskRunner.run();
|
||||
|
||||
expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0, taskRunError }));
|
||||
|
@ -2755,11 +2748,11 @@ describe('Task Runner', () => {
|
|||
inMemoryMetrics,
|
||||
});
|
||||
expect(AlertingEventLogger).toHaveBeenCalled();
|
||||
rulesClient.getAlertFromRaw.mockReturnValue({
|
||||
...(mockedRuleTypeSavedObject as Rule),
|
||||
schedule: { interval: '50s' },
|
||||
rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({
|
||||
...mockedRawRuleSO,
|
||||
attributes: { ...mockedRawRuleSO.attributes, schedule: { interval: '50s' } },
|
||||
});
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO);
|
||||
|
||||
await taskRunner.run();
|
||||
expect(
|
||||
|
@ -3405,6 +3398,7 @@ describe('Task Runner', () => {
|
|||
claim_to_start_duration_ms: 0,
|
||||
persist_alerts_duration_ms: 0,
|
||||
prepare_rule_duration_ms: 0,
|
||||
prepare_to_run_duration_ms: 0,
|
||||
process_alerts_duration_ms: 0,
|
||||
process_rule_duration_ms: 0,
|
||||
rule_type_run_duration_ms: 0,
|
||||
|
@ -3440,6 +3434,7 @@ describe('Task Runner', () => {
|
|||
claim_to_start_duration_ms: 0,
|
||||
persist_alerts_duration_ms: 0,
|
||||
prepare_rule_duration_ms: 0,
|
||||
prepare_to_run_duration_ms: 0,
|
||||
process_alerts_duration_ms: 0,
|
||||
process_rule_duration_ms: 0,
|
||||
rule_type_run_duration_ms: 0,
|
||||
|
@ -3473,6 +3468,7 @@ describe('Task Runner', () => {
|
|||
claim_to_start_duration_ms: 0,
|
||||
persist_alerts_duration_ms: 0,
|
||||
prepare_rule_duration_ms: 0,
|
||||
prepare_to_run_duration_ms: 0,
|
||||
process_alerts_duration_ms: 0,
|
||||
process_rule_duration_ms: 0,
|
||||
rule_type_run_duration_ms: 0,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,7 +17,7 @@ import {
|
|||
RuleAlertData,
|
||||
} from '../types';
|
||||
import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { TaskRunnerContext } from './types';
|
||||
import { TaskRunner } from './task_runner';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import {
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
RuleAlertData,
|
||||
} from '../types';
|
||||
import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { TaskRunner } from './task_runner';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import {
|
||||
|
@ -59,6 +58,7 @@ import { rulesSettingsClientMock } from '../rules_settings_client.mock';
|
|||
import { maintenanceWindowClientMock } from '../maintenance_window_client.mock';
|
||||
import { alertsServiceMock } from '../alerts_service/alerts_service.mock';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects';
|
||||
import { TaskRunnerContext } from './types';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
|
||||
|
@ -531,6 +531,7 @@ describe('Task Runner Cancel', () => {
|
|||
claim_to_start_duration_ms: 0,
|
||||
persist_alerts_duration_ms: 0,
|
||||
prepare_rule_duration_ms: 0,
|
||||
prepare_to_run_duration_ms: 0,
|
||||
process_alerts_duration_ms: 0,
|
||||
process_rule_duration_ms: 0,
|
||||
rule_type_run_duration_ms: 0,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import sinon from 'sinon';
|
||||
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
|
||||
import { ConcreteTaskInstance, TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory';
|
||||
import { TaskRunnerFactory } from './task_runner_factory';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import {
|
||||
loggingSystemMock,
|
||||
|
@ -33,6 +33,7 @@ import { rulesSettingsClientMock } from '../rules_settings_client.mock';
|
|||
import { maintenanceWindowClientMock } from '../maintenance_window_client.mock';
|
||||
import { alertsServiceMock } from '../alerts_service/alerts_service.mock';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { TaskRunnerContext } from './types';
|
||||
|
||||
const inMemoryMetrics = inMemoryMetricsMock.create();
|
||||
const executionContext = executionContextServiceMock.createSetupContract();
|
||||
|
|
|
@ -5,70 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
||||
import type {
|
||||
Logger,
|
||||
KibanaRequest,
|
||||
ISavedObjectsRepository,
|
||||
IBasePath,
|
||||
ExecutionContextStart,
|
||||
SavedObjectsServiceStart,
|
||||
ElasticsearchServiceStart,
|
||||
UiSettingsServiceStart,
|
||||
} from '@kbn/core/server';
|
||||
import { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server';
|
||||
import { RunContext } from '@kbn/task-manager-plugin/server';
|
||||
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server';
|
||||
import { IEventLogger } from '@kbn/event-log-plugin/server';
|
||||
import { PluginStart as DataPluginStart } from '@kbn/data-plugin/server';
|
||||
import { SharePluginStart } from '@kbn/share-plugin/server';
|
||||
import {
|
||||
RuleAlertData,
|
||||
RuleTypeParams,
|
||||
RuleTypeRegistry,
|
||||
SpaceIdToNamespaceFunction,
|
||||
RuleTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
RulesClientApi,
|
||||
RulesSettingsClientApi,
|
||||
MaintenanceWindowClientApi,
|
||||
} from '../types';
|
||||
import { TaskRunner } from './task_runner';
|
||||
import { NormalizedRuleType } from '../rule_type_registry';
|
||||
import { InMemoryMetrics } from '../monitoring';
|
||||
import { ActionsConfigMap } from '../lib/get_actions_config_map';
|
||||
import { AlertsService } from '../alerts_service/alerts_service';
|
||||
|
||||
export interface TaskRunnerContext {
|
||||
logger: Logger;
|
||||
data: DataPluginStart;
|
||||
dataViews: DataViewsPluginStart;
|
||||
share: SharePluginStart;
|
||||
savedObjects: SavedObjectsServiceStart;
|
||||
uiSettings: UiSettingsServiceStart;
|
||||
elasticsearch: ElasticsearchServiceStart;
|
||||
getRulesClientWithRequest(request: KibanaRequest): RulesClientApi;
|
||||
actionsPlugin: ActionsPluginStartContract;
|
||||
eventLogger: IEventLogger;
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
|
||||
executionContext: ExecutionContextStart;
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
basePathService: IBasePath;
|
||||
internalSavedObjectsRepository: ISavedObjectsRepository;
|
||||
ruleTypeRegistry: RuleTypeRegistry;
|
||||
alertsService: AlertsService | null;
|
||||
kibanaBaseUrl: string | undefined;
|
||||
supportsEphemeralTasks: boolean;
|
||||
maxEphemeralActionsPerRule: number;
|
||||
maxAlerts: number;
|
||||
actionsConfigMap: ActionsConfigMap;
|
||||
cancelAlertsOnRuleTimeout: boolean;
|
||||
usageCounter?: UsageCounter;
|
||||
getRulesSettingsClientWithRequest(request: KibanaRequest): RulesSettingsClientApi;
|
||||
getMaintenanceWindowClientWithRequest(request: KibanaRequest): MaintenanceWindowClientApi;
|
||||
}
|
||||
import { TaskRunnerContext } from './types';
|
||||
|
||||
export class TaskRunnerFactory {
|
||||
private isInitialized = false;
|
||||
|
|
|
@ -34,6 +34,7 @@ describe('TaskRunnerTimer', () => {
|
|||
claim_to_start_duration_ms: 259200000,
|
||||
persist_alerts_duration_ms: 0,
|
||||
prepare_rule_duration_ms: 0,
|
||||
prepare_to_run_duration_ms: 0,
|
||||
process_alerts_duration_ms: 0,
|
||||
process_rule_duration_ms: 0,
|
||||
rule_type_run_duration_ms: 0,
|
||||
|
@ -52,6 +53,7 @@ describe('TaskRunnerTimer', () => {
|
|||
claim_to_start_duration_ms: 432000000,
|
||||
persist_alerts_duration_ms: 0,
|
||||
prepare_rule_duration_ms: 0,
|
||||
prepare_to_run_duration_ms: 0,
|
||||
process_alerts_duration_ms: 0,
|
||||
process_rule_duration_ms: 0,
|
||||
rule_type_run_duration_ms: 0,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Logger } from '@kbn/core/server';
|
|||
export enum TaskRunnerTimerSpan {
|
||||
StartTaskRun = 'claim_to_start_duration_ms',
|
||||
TotalRunDuration = 'total_run_duration_ms',
|
||||
PrepareToRun = 'prepare_to_run_duration_ms',
|
||||
PrepareRule = 'prepare_rule_duration_ms',
|
||||
RuleTypeRun = 'rule_type_run_duration_ms',
|
||||
ProcessAlerts = 'process_alerts_duration_ms',
|
||||
|
@ -60,6 +61,7 @@ export class TaskRunnerTimer {
|
|||
[TaskRunnerTimerSpan.StartTaskRun]: this.timings[TaskRunnerTimerSpan.StartTaskRun] ?? 0,
|
||||
[TaskRunnerTimerSpan.TotalRunDuration]:
|
||||
this.timings[TaskRunnerTimerSpan.TotalRunDuration] ?? 0,
|
||||
[TaskRunnerTimerSpan.PrepareToRun]: this.timings[TaskRunnerTimerSpan.PrepareToRun] ?? 0,
|
||||
[TaskRunnerTimerSpan.PrepareRule]: this.timings[TaskRunnerTimerSpan.PrepareRule] ?? 0,
|
||||
[TaskRunnerTimerSpan.RuleTypeRun]: this.timings[TaskRunnerTimerSpan.RuleTypeRun] ?? 0,
|
||||
[TaskRunnerTimerSpan.ProcessAlerts]: this.timings[TaskRunnerTimerSpan.ProcessAlerts] ?? 0,
|
||||
|
|
|
@ -5,13 +5,29 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KibanaRequest, Logger } from '@kbn/core/server';
|
||||
import type {
|
||||
Logger,
|
||||
KibanaRequest,
|
||||
IBasePath,
|
||||
ExecutionContextStart,
|
||||
SavedObjectsServiceStart,
|
||||
ElasticsearchServiceStart,
|
||||
UiSettingsServiceStart,
|
||||
ISavedObjectsRepository,
|
||||
} from '@kbn/core/server';
|
||||
import { ConcreteTaskInstance, DecoratedError } from '@kbn/task-manager-plugin/server';
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server';
|
||||
import { ActionsClient } from '@kbn/actions-plugin/server/actions_client';
|
||||
import { PluginStart as DataPluginStart } from '@kbn/data-plugin/server';
|
||||
import { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server';
|
||||
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import { IEventLogger } from '@kbn/event-log-plugin/server';
|
||||
import { SharePluginStart } from '@kbn/share-plugin/server';
|
||||
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
||||
import { IAlertsClient } from '../alerts_client/types';
|
||||
import { Alert } from '../alert';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { AlertsService } from '../alerts_service/alerts_service';
|
||||
import {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
|
@ -23,9 +39,20 @@ import {
|
|||
RuleTypeState,
|
||||
RuleAction,
|
||||
RuleAlertData,
|
||||
RulesSettingsFlappingProperties,
|
||||
RulesSettingsQueryDelayProperties,
|
||||
} from '../../common';
|
||||
import { ActionsConfigMap } from '../lib/get_actions_config_map';
|
||||
import { NormalizedRuleType } from '../rule_type_registry';
|
||||
import { RawRule, RulesClientApi, CombinedSummarizedAlerts } from '../types';
|
||||
import {
|
||||
CombinedSummarizedAlerts,
|
||||
MaintenanceWindowClientApi,
|
||||
RawRule,
|
||||
RulesClientApi,
|
||||
RulesSettingsClientApi,
|
||||
RuleTypeRegistry,
|
||||
SpaceIdToNamespaceFunction,
|
||||
} from '../types';
|
||||
import { RuleRunMetrics, RuleRunMetricsStore } from '../lib/rule_run_metrics_store';
|
||||
import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event_logger';
|
||||
|
||||
|
@ -42,11 +69,12 @@ export type RuleTaskStateAndMetrics = RuleTaskState & {
|
|||
};
|
||||
|
||||
export interface RunRuleParams<Params extends RuleTypeParams> {
|
||||
fakeRequest: KibanaRequest;
|
||||
rulesClient: RulesClientApi;
|
||||
rule: SanitizedRule<Params>;
|
||||
apiKey: RawRule['apiKey'];
|
||||
fakeRequest: KibanaRequest;
|
||||
rule: SanitizedRule<Params>;
|
||||
rulesClient: RulesClientApi;
|
||||
validatedParams: Params;
|
||||
version: string | undefined;
|
||||
}
|
||||
|
||||
export interface RuleTaskInstance extends ConcreteTaskInstance {
|
||||
|
@ -107,3 +135,43 @@ export type Executable<
|
|||
summarizedAlerts: CombinedSummarizedAlerts;
|
||||
}
|
||||
);
|
||||
|
||||
export interface RuleTypeRunnerContext {
|
||||
alertingEventLogger: AlertingEventLogger;
|
||||
flappingSettings: RulesSettingsFlappingProperties;
|
||||
namespace?: string;
|
||||
queryDelaySettings: RulesSettingsQueryDelayProperties;
|
||||
ruleId: string;
|
||||
ruleLogPrefix: string;
|
||||
ruleRunMetricsStore: RuleRunMetricsStore;
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
export interface TaskRunnerContext {
|
||||
actionsConfigMap: ActionsConfigMap;
|
||||
actionsPlugin: ActionsPluginStartContract;
|
||||
alertsService: AlertsService | null;
|
||||
basePathService: IBasePath;
|
||||
cancelAlertsOnRuleTimeout: boolean;
|
||||
data: DataPluginStart;
|
||||
dataViews: DataViewsPluginStart;
|
||||
elasticsearch: ElasticsearchServiceStart;
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
|
||||
eventLogger: IEventLogger;
|
||||
executionContext: ExecutionContextStart;
|
||||
getMaintenanceWindowClientWithRequest(request: KibanaRequest): MaintenanceWindowClientApi;
|
||||
getRulesClientWithRequest(request: KibanaRequest): RulesClientApi;
|
||||
getRulesSettingsClientWithRequest(request: KibanaRequest): RulesSettingsClientApi;
|
||||
internalSavedObjectsRepository: ISavedObjectsRepository;
|
||||
kibanaBaseUrl: string | undefined;
|
||||
logger: Logger;
|
||||
maxAlerts: number;
|
||||
maxEphemeralActionsPerRule: number;
|
||||
ruleTypeRegistry: RuleTypeRegistry;
|
||||
savedObjects: SavedObjectsServiceStart;
|
||||
share: SharePluginStart;
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
supportsEphemeralTasks: boolean;
|
||||
uiSettings: UiSettingsServiceStart;
|
||||
usageCounter?: UsageCounter;
|
||||
}
|
||||
|
|
|
@ -66,7 +66,9 @@
|
|||
"@kbn/core-http-browser",
|
||||
"@kbn/core-saved-objects-api-server-mocks",
|
||||
"@kbn/core-ui-settings-server-mocks",
|
||||
"@kbn/core-test-helpers-kbn-server"
|
||||
"@kbn/core-test-helpers-kbn-server",
|
||||
"@kbn/core-http-router-server-internal",
|
||||
"@kbn/core-execution-context-server-mocks"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
|
@ -398,6 +398,9 @@
|
|||
"prepare_rule_duration_ms": {
|
||||
"type": "long"
|
||||
},
|
||||
"prepare_to_run_duration_ms": {
|
||||
"type": "long"
|
||||
},
|
||||
"total_run_duration_ms": {
|
||||
"type": "long"
|
||||
},
|
||||
|
|
|
@ -175,6 +175,7 @@ export const EventSchema = schema.maybe(
|
|||
claim_to_start_duration_ms: ecsStringOrNumber(),
|
||||
persist_alerts_duration_ms: ecsStringOrNumber(),
|
||||
prepare_rule_duration_ms: ecsStringOrNumber(),
|
||||
prepare_to_run_duration_ms: ecsStringOrNumber(),
|
||||
total_run_duration_ms: ecsStringOrNumber(),
|
||||
total_enrichment_duration_ms: ecsStringOrNumber(),
|
||||
})
|
||||
|
|
|
@ -173,6 +173,9 @@ exports.EcsCustomPropertyMappings = {
|
|||
prepare_rule_duration_ms: {
|
||||
type: 'long',
|
||||
},
|
||||
prepare_to_run_duration_ms: {
|
||||
type: 'long',
|
||||
},
|
||||
total_run_duration_ms: {
|
||||
type: 'long',
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue