[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
commit
8c64963757
- in the `run()` function, consolidated everything that occurs after
calling `this.runRule()` into the `processRunResults()` function - in
this commit
856a78484e
- moved around some maintenance window handling code in this commit
205e3e3f87
- extract initialization of alerts client into helper function in this
commit
b057e0a617
- 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 commit
b057e0a617

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ying Mao 2024-03-18 10:31:16 -04:00 committed by GitHub
parent 187a8f5fab
commit 80ff1a75f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 2871 additions and 815 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
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,
};

View file

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

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
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,
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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/**/*"

View file

@ -398,6 +398,9 @@
"prepare_rule_duration_ms": {
"type": "long"
},
"prepare_to_run_duration_ms": {
"type": "long"
},
"total_run_duration_ms": {
"type": "long"
},

View file

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

View file

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