mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Response Ops] Adds recovery context for ES query rule type (#132839)
* Renaming alert to rule for es query rule type * adding recovery context * Updating unit tests * Fixing i18n * Adding functional test * Adding functional test * Fixing functional test * Adding space id to link Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
eadedd12c9
commit
a5a287b383
21 changed files with 473 additions and 296 deletions
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EsQueryAlertActionContext, addMessages } from './action_context';
|
||||
import { EsQueryAlertParamsSchema } from './alert_type_params';
|
||||
import { OnlyEsQueryAlertParams } from './types';
|
||||
import { EsQueryRuleActionContext, addMessages } from './action_context';
|
||||
import { EsQueryRuleParamsSchema } from './rule_type_params';
|
||||
import { OnlyEsQueryRuleParams } from './types';
|
||||
|
||||
describe('ActionContext', () => {
|
||||
it('generates expected properties', async () => {
|
||||
const params = EsQueryAlertParamsSchema.validate({
|
||||
const params = EsQueryRuleParamsSchema.validate({
|
||||
index: ['[index]'],
|
||||
timeField: '[timeField]',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -21,18 +21,18 @@ describe('ActionContext', () => {
|
|||
thresholdComparator: '>',
|
||||
threshold: [4],
|
||||
searchType: 'esQuery',
|
||||
}) as OnlyEsQueryAlertParams;
|
||||
const base: EsQueryAlertActionContext = {
|
||||
}) as OnlyEsQueryRuleParams;
|
||||
const base: EsQueryRuleActionContext = {
|
||||
date: '2020-01-01T00:00:00.000Z',
|
||||
value: 42,
|
||||
conditions: 'count greater than 4',
|
||||
hits: [],
|
||||
link: 'link-mock',
|
||||
};
|
||||
const context = addMessages({ name: '[alert-name]' }, base, params);
|
||||
expect(context.title).toMatchInlineSnapshot(`"alert '[alert-name]' matched query"`);
|
||||
const context = addMessages({ name: '[rule-name]' }, base, params);
|
||||
expect(context.title).toMatchInlineSnapshot(`"rule '[rule-name]' matched query"`);
|
||||
expect(context.message).toEqual(
|
||||
`alert '[alert-name]' is active:
|
||||
`rule '[rule-name]' is active:
|
||||
|
||||
- Value: 42
|
||||
- Conditions Met: count greater than 4 over 5m
|
||||
|
@ -41,8 +41,39 @@ describe('ActionContext', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('generates expected properties when isRecovered is true', async () => {
|
||||
const params = EsQueryRuleParamsSchema.validate({
|
||||
index: ['[index]'],
|
||||
timeField: '[timeField]',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
thresholdComparator: '>',
|
||||
threshold: [4],
|
||||
searchType: 'esQuery',
|
||||
}) as OnlyEsQueryRuleParams;
|
||||
const base: EsQueryRuleActionContext = {
|
||||
date: '2020-01-01T00:00:00.000Z',
|
||||
value: 42,
|
||||
conditions: 'count not greater than 4',
|
||||
hits: [],
|
||||
link: 'link-mock',
|
||||
};
|
||||
const context = addMessages({ name: '[rule-name]' }, base, params, true);
|
||||
expect(context.title).toMatchInlineSnapshot(`"rule '[rule-name]' recovered"`);
|
||||
expect(context.message).toEqual(
|
||||
`rule '[rule-name]' is recovered:
|
||||
|
||||
- Value: 42
|
||||
- Conditions Met: count not greater than 4 over 5m
|
||||
- Timestamp: 2020-01-01T00:00:00.000Z
|
||||
- Link: link-mock`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates expected properties if comparator is between', async () => {
|
||||
const params = EsQueryAlertParamsSchema.validate({
|
||||
const params = EsQueryRuleParamsSchema.validate({
|
||||
index: ['[index]'],
|
||||
timeField: '[timeField]',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -52,18 +83,18 @@ describe('ActionContext', () => {
|
|||
thresholdComparator: 'between',
|
||||
threshold: [4, 5],
|
||||
searchType: 'esQuery',
|
||||
}) as OnlyEsQueryAlertParams;
|
||||
const base: EsQueryAlertActionContext = {
|
||||
}) as OnlyEsQueryRuleParams;
|
||||
const base: EsQueryRuleActionContext = {
|
||||
date: '2020-01-01T00:00:00.000Z',
|
||||
value: 4,
|
||||
conditions: 'count between 4 and 5',
|
||||
hits: [],
|
||||
link: 'link-mock',
|
||||
};
|
||||
const context = addMessages({ name: '[alert-name]' }, base, params);
|
||||
expect(context.title).toMatchInlineSnapshot(`"alert '[alert-name]' matched query"`);
|
||||
const context = addMessages({ name: '[rule-name]' }, base, params);
|
||||
expect(context.title).toMatchInlineSnapshot(`"rule '[rule-name]' matched query"`);
|
||||
expect(context.message).toEqual(
|
||||
`alert '[alert-name]' is active:
|
||||
`rule '[rule-name]' is active:
|
||||
|
||||
- Value: 4
|
||||
- Conditions Met: count between 4 and 5 over 5m
|
||||
|
|
|
@ -8,21 +8,21 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { RuleExecutorOptions, AlertInstanceContext } from '@kbn/alerting-plugin/server';
|
||||
import { OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types';
|
||||
import { OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types';
|
||||
|
||||
// alert type context provided to actions
|
||||
// rule type context provided to actions
|
||||
|
||||
type AlertInfo = Pick<RuleExecutorOptions, 'name'>;
|
||||
type RuleInfo = Pick<RuleExecutorOptions, 'name'>;
|
||||
|
||||
export interface ActionContext extends EsQueryAlertActionContext {
|
||||
export interface ActionContext extends EsQueryRuleActionContext {
|
||||
// a short pre-constructed message which may be used in an action field
|
||||
title: string;
|
||||
// a longer pre-constructed message which may be used in an action field
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface EsQueryAlertActionContext extends AlertInstanceContext {
|
||||
// the date the alert was run as an ISO date
|
||||
export interface EsQueryRuleActionContext extends AlertInstanceContext {
|
||||
// the date the rule was run as an ISO date
|
||||
date: string;
|
||||
// the value that met the threshold
|
||||
value: number;
|
||||
|
@ -30,38 +30,41 @@ export interface EsQueryAlertActionContext extends AlertInstanceContext {
|
|||
conditions: string;
|
||||
// query matches
|
||||
hits: estypes.SearchHit[];
|
||||
// a link to see records that triggered the alert for Discover alert
|
||||
// a link which navigates to stack management in case of Elastic query alert
|
||||
// a link to see records that triggered the rule for Discover rule
|
||||
// a link which navigates to stack management in case of Elastic query rule
|
||||
link: string;
|
||||
}
|
||||
|
||||
export function addMessages(
|
||||
alertInfo: AlertInfo,
|
||||
baseContext: EsQueryAlertActionContext,
|
||||
params: OnlyEsQueryAlertParams | OnlySearchSourceAlertParams
|
||||
ruleInfo: RuleInfo,
|
||||
baseContext: EsQueryRuleActionContext,
|
||||
params: OnlyEsQueryRuleParams | OnlySearchSourceRuleParams,
|
||||
isRecovered: boolean = false
|
||||
): ActionContext {
|
||||
const title = i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle', {
|
||||
defaultMessage: `alert '{name}' matched query`,
|
||||
defaultMessage: `rule '{name}' {verb}`,
|
||||
values: {
|
||||
name: alertInfo.name,
|
||||
name: ruleInfo.name,
|
||||
verb: isRecovered ? 'recovered' : 'matched query',
|
||||
},
|
||||
});
|
||||
|
||||
const window = `${params.timeWindowSize}${params.timeWindowUnit}`;
|
||||
const message = i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextMessageDescription', {
|
||||
defaultMessage: `alert '{name}' is active:
|
||||
defaultMessage: `rule '{name}' is {verb}:
|
||||
|
||||
- Value: {value}
|
||||
- Conditions Met: {conditions} over {window}
|
||||
- Timestamp: {date}
|
||||
- Link: {link}`,
|
||||
values: {
|
||||
name: alertInfo.name,
|
||||
name: ruleInfo.name,
|
||||
value: baseContext.value,
|
||||
conditions: baseContext.conditions,
|
||||
window,
|
||||
date: baseContext.date,
|
||||
link: baseContext.link,
|
||||
verb: isRecovered ? 'recovered' : 'active',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -5,8 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getSearchParams, getValidTimefieldSort, tryToParseAsDate } from './executor';
|
||||
import { OnlyEsQueryAlertParams } from './types';
|
||||
import {
|
||||
getSearchParams,
|
||||
getValidTimefieldSort,
|
||||
tryToParseAsDate,
|
||||
getContextConditionsDescription,
|
||||
} from './executor';
|
||||
import { OnlyEsQueryRuleParams } from './types';
|
||||
import { Comparator } from '../../../common/comparator_types';
|
||||
|
||||
describe('es_query executor', () => {
|
||||
const defaultProps = {
|
||||
|
@ -49,13 +55,13 @@ describe('es_query executor', () => {
|
|||
|
||||
describe('getSearchParams', () => {
|
||||
it('should return search params correctly', () => {
|
||||
const result = getSearchParams(defaultProps as OnlyEsQueryAlertParams);
|
||||
const result = getSearchParams(defaultProps as OnlyEsQueryRuleParams);
|
||||
expect(result.parsedQuery.query).toBe('test-query');
|
||||
});
|
||||
|
||||
it('should throw invalid query error', () => {
|
||||
expect(() =>
|
||||
getSearchParams({ ...defaultProps, esQuery: '' } as OnlyEsQueryAlertParams)
|
||||
getSearchParams({ ...defaultProps, esQuery: '' } as OnlyEsQueryRuleParams)
|
||||
).toThrow('invalid query specified: "" - query must be JSON');
|
||||
});
|
||||
|
||||
|
@ -64,7 +70,7 @@ describe('es_query executor', () => {
|
|||
getSearchParams({
|
||||
...defaultProps,
|
||||
esQuery: '{ "someProperty": "test-query" }',
|
||||
} as OnlyEsQueryAlertParams)
|
||||
} as OnlyEsQueryRuleParams)
|
||||
).toThrow('invalid query specified: "{ "someProperty": "test-query" }" - query must be JSON');
|
||||
});
|
||||
|
||||
|
@ -74,8 +80,25 @@ describe('es_query executor', () => {
|
|||
...defaultProps,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'r',
|
||||
} as OnlyEsQueryAlertParams)
|
||||
} as OnlyEsQueryRuleParams)
|
||||
).toThrow('invalid format for windowSize: "5r"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContextConditionsDescription', () => {
|
||||
it('should return conditions correctly', () => {
|
||||
const result = getContextConditionsDescription(Comparator.GT, [10]);
|
||||
expect(result).toBe(`Number of matching documents is greater than 10`);
|
||||
});
|
||||
|
||||
it('should return conditions correctly when isRecovered is true', () => {
|
||||
const result = getContextConditionsDescription(Comparator.GT, [10], true);
|
||||
expect(result).toBe(`Number of matching documents is NOT greater than 10`);
|
||||
});
|
||||
|
||||
it('should return conditions correctly when multiple thresholds provided', () => {
|
||||
const result = getContextConditionsDescription(Comparator.BETWEEN, [10, 20], true);
|
||||
expect(result).toBe(`Number of matching documents is NOT between 10 and 20`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,23 +8,23 @@ import { sha256 } from 'js-sha256';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreSetup, Logger } from '@kbn/core/server';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/server';
|
||||
import { addMessages, EsQueryAlertActionContext } from './action_context';
|
||||
import { addMessages, EsQueryRuleActionContext } from './action_context';
|
||||
import { ComparatorFns, getHumanReadableComparator } from '../lib';
|
||||
import { ExecutorOptions, OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types';
|
||||
import { ExecutorOptions, OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types';
|
||||
import { ActionGroupId, ConditionMetAlertInstanceId } from './constants';
|
||||
import { fetchEsQuery } from './lib/fetch_es_query';
|
||||
import { EsQueryAlertParams } from './alert_type_params';
|
||||
import { EsQueryRuleParams } from './rule_type_params';
|
||||
import { fetchSearchSourceQuery } from './lib/fetch_search_source_query';
|
||||
import { Comparator } from '../../../common/comparator_types';
|
||||
import { isEsQueryAlert } from './util';
|
||||
import { isEsQueryRule } from './util';
|
||||
|
||||
export async function executor(
|
||||
logger: Logger,
|
||||
core: CoreSetup,
|
||||
options: ExecutorOptions<EsQueryAlertParams>
|
||||
options: ExecutorOptions<EsQueryRuleParams>
|
||||
) {
|
||||
const esQueryAlert = isEsQueryAlert(options.params.searchType);
|
||||
const { alertId, name, services, params, state } = options;
|
||||
const esQueryRule = isEsQueryRule(options.params.searchType);
|
||||
const { alertId: ruleId, name, services, params, state, spaceId } = options;
|
||||
const { alertFactory, scopedClusterClient, searchSourceClient } = services;
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
const publicBaseUrl = core.http.basePath.publicBaseUrl ?? '';
|
||||
|
@ -35,51 +35,49 @@ export async function executor(
|
|||
}
|
||||
let latestTimestamp: string | undefined = tryToParseAsDate(state.latestTimestamp);
|
||||
|
||||
// During each alert execution, we run the configured query, get a hit count
|
||||
// During each rule execution, we run the configured query, get a hit count
|
||||
// (hits.total) and retrieve up to params.size hits. We
|
||||
// evaluate the threshold condition using the value of hits.total. If the threshold
|
||||
// condition is met, the hits are counted toward the query match and we update
|
||||
// the alert state with the timestamp of the latest hit. In the next execution
|
||||
// of the alert, the latestTimestamp will be used to gate the query in order to
|
||||
// the rule state with the timestamp of the latest hit. In the next execution
|
||||
// of the rule, the latestTimestamp will be used to gate the query in order to
|
||||
// avoid counting a document multiple times.
|
||||
|
||||
const { numMatches, searchResult, dateStart, dateEnd } = esQueryAlert
|
||||
? await fetchEsQuery(alertId, name, params as OnlyEsQueryAlertParams, latestTimestamp, {
|
||||
const { numMatches, searchResult, dateStart, dateEnd } = esQueryRule
|
||||
? await fetchEsQuery(ruleId, name, params as OnlyEsQueryRuleParams, latestTimestamp, {
|
||||
scopedClusterClient,
|
||||
logger,
|
||||
})
|
||||
: await fetchSearchSourceQuery(
|
||||
alertId,
|
||||
params as OnlySearchSourceAlertParams,
|
||||
latestTimestamp,
|
||||
{ searchSourceClient, logger }
|
||||
);
|
||||
: await fetchSearchSourceQuery(ruleId, params as OnlySearchSourceRuleParams, latestTimestamp, {
|
||||
searchSourceClient,
|
||||
logger,
|
||||
});
|
||||
|
||||
// apply the alert condition
|
||||
// apply the rule condition
|
||||
const conditionMet = compareFn(numMatches, params.threshold);
|
||||
|
||||
const base = publicBaseUrl;
|
||||
const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : '';
|
||||
const link = esQueryRule
|
||||
? `${base}${spacePrefix}/app/management/insightsAndAlerting/triggersActions/rule/${ruleId}`
|
||||
: `${base}${spacePrefix}/app/discover#/viewAlert/${ruleId}?from=${dateStart}&to=${dateEnd}&checksum=${getChecksum(
|
||||
params as OnlyEsQueryRuleParams
|
||||
)}`;
|
||||
const baseContext: Omit<EsQueryRuleActionContext, 'conditions'> = {
|
||||
title: name,
|
||||
date: currentTimestamp,
|
||||
value: numMatches,
|
||||
hits: searchResult.hits.hits,
|
||||
link,
|
||||
};
|
||||
|
||||
if (conditionMet) {
|
||||
const base = publicBaseUrl;
|
||||
const link = esQueryAlert
|
||||
? `${base}/app/management/insightsAndAlerting/triggersActions/rule/${alertId}`
|
||||
: `${base}/app/discover#/viewAlert/${alertId}?from=${dateStart}&to=${dateEnd}&checksum=${getChecksum(
|
||||
params
|
||||
)}`;
|
||||
const baseActiveContext: EsQueryRuleActionContext = {
|
||||
...baseContext,
|
||||
conditions: getContextConditionsDescription(params.thresholdComparator, params.threshold),
|
||||
} as EsQueryRuleActionContext;
|
||||
|
||||
const conditions = getContextConditionsDescription(
|
||||
params.thresholdComparator,
|
||||
params.threshold
|
||||
);
|
||||
const baseContext: EsQueryAlertActionContext = {
|
||||
title: name,
|
||||
date: currentTimestamp,
|
||||
value: numMatches,
|
||||
conditions,
|
||||
hits: searchResult.hits.hits,
|
||||
link,
|
||||
};
|
||||
|
||||
const actionContext = addMessages(options, baseContext, params);
|
||||
const actionContext = addMessages(options, baseActiveContext, params);
|
||||
const alertInstance = alertFactory.create(ConditionMetAlertInstanceId);
|
||||
alertInstance
|
||||
// store the params we would need to recreate the query that led to this alert instance
|
||||
|
@ -95,6 +93,20 @@ export async function executor(
|
|||
}
|
||||
}
|
||||
|
||||
const { getRecoveredAlerts } = alertFactory.done();
|
||||
for (const alert of getRecoveredAlerts()) {
|
||||
const baseRecoveryContext: EsQueryRuleActionContext = {
|
||||
...baseContext,
|
||||
conditions: getContextConditionsDescription(
|
||||
params.thresholdComparator,
|
||||
params.threshold,
|
||||
true
|
||||
),
|
||||
} as EsQueryRuleActionContext;
|
||||
const recoveryContext = addMessages(options, baseRecoveryContext, params, true);
|
||||
alert.setContext(recoveryContext);
|
||||
}
|
||||
|
||||
return { latestTimestamp };
|
||||
}
|
||||
|
||||
|
@ -116,7 +128,7 @@ function getInvalidQueryError(query: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getSearchParams(queryParams: OnlyEsQueryAlertParams) {
|
||||
export function getSearchParams(queryParams: OnlyEsQueryRuleParams) {
|
||||
const date = Date.now();
|
||||
const { esQuery, timeWindowSize, timeWindowUnit } = queryParams;
|
||||
|
||||
|
@ -163,7 +175,7 @@ export function tryToParseAsDate(sortValue?: string | number | null): undefined
|
|||
}
|
||||
}
|
||||
|
||||
export function getChecksum(params: EsQueryAlertParams) {
|
||||
export function getChecksum(params: OnlyEsQueryRuleParams) {
|
||||
return sha256.create().update(JSON.stringify(params));
|
||||
}
|
||||
|
||||
|
@ -176,12 +188,17 @@ export function getInvalidComparatorError(comparator: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getContextConditionsDescription(comparator: Comparator, threshold: number[]) {
|
||||
export function getContextConditionsDescription(
|
||||
comparator: Comparator,
|
||||
threshold: number[],
|
||||
isRecovered: boolean = false
|
||||
) {
|
||||
return i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', {
|
||||
defaultMessage: 'Number of matching documents is {thresholdComparator} {threshold}',
|
||||
defaultMessage: 'Number of matching documents is {negation}{thresholdComparator} {threshold}',
|
||||
values: {
|
||||
thresholdComparator: getHumanReadableComparator(comparator),
|
||||
threshold: threshold.join(' and '),
|
||||
negation: isRecovered ? 'NOT ' : '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { CoreSetup, Logger } from '@kbn/core/server';
|
||||
import { AlertingSetup } from '../../types';
|
||||
import { getAlertType } from './alert_type';
|
||||
import { getRuleType } from './rule_type';
|
||||
|
||||
interface RegisterParams {
|
||||
logger: Logger;
|
||||
|
@ -17,5 +17,5 @@ interface RegisterParams {
|
|||
|
||||
export function register(params: RegisterParams) {
|
||||
const { logger, alerting, core } = params;
|
||||
alerting.registerType(getAlertType(logger, core));
|
||||
alerting.registerType(getRuleType(logger, core));
|
||||
}
|
||||
|
|
|
@ -6,18 +6,18 @@
|
|||
*/
|
||||
import { IScopedClusterClient, Logger } from '@kbn/core/server';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { OnlyEsQueryAlertParams } from '../types';
|
||||
import { OnlyEsQueryRuleParams } from '../types';
|
||||
import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query';
|
||||
import { ES_QUERY_ID } from '../constants';
|
||||
import { getSearchParams } from './get_search_params';
|
||||
|
||||
/**
|
||||
* Fetching matching documents for a given alert from elasticsearch by a given index and query
|
||||
* Fetching matching documents for a given rule from elasticsearch by a given index and query
|
||||
*/
|
||||
export async function fetchEsQuery(
|
||||
alertId: string,
|
||||
ruleId: string,
|
||||
name: string,
|
||||
params: OnlyEsQueryAlertParams,
|
||||
params: OnlyEsQueryRuleParams,
|
||||
timestamp: string | undefined,
|
||||
services: {
|
||||
scopedClusterClient: IScopedClusterClient;
|
||||
|
@ -70,14 +70,12 @@ export async function fetchEsQuery(
|
|||
track_total_hits: true,
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
`es query alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}`
|
||||
);
|
||||
logger.debug(`es query rule ${ES_QUERY_ID}:${ruleId} "${name}" query - ${JSON.stringify(query)}`);
|
||||
|
||||
const { body: searchResult } = await esClient.search(query, { meta: true });
|
||||
|
||||
logger.debug(
|
||||
` es query alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}`
|
||||
` es query rule ${ES_QUERY_ID}:${ruleId} "${name}" result - ${JSON.stringify(searchResult)}`
|
||||
);
|
||||
return {
|
||||
numMatches: (searchResult.hits.total as estypes.SearchTotalHits).value,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { OnlySearchSourceAlertParams } from '../types';
|
||||
import { OnlySearchSourceRuleParams } from '../types';
|
||||
import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
import { updateSearchSource } from './fetch_search_source_query';
|
||||
import { stubbedSavedObjectIndexPattern } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
|
@ -29,7 +29,7 @@ const createDataView = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const defaultParams: OnlySearchSourceAlertParams = {
|
||||
const defaultParams: OnlySearchSourceRuleParams = {
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
|
|
|
@ -12,11 +12,11 @@ import {
|
|||
ISearchStartSearchSource,
|
||||
SortDirection,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { OnlySearchSourceAlertParams } from '../types';
|
||||
import { OnlySearchSourceRuleParams } from '../types';
|
||||
|
||||
export async function fetchSearchSourceQuery(
|
||||
alertId: string,
|
||||
params: OnlySearchSourceAlertParams,
|
||||
ruleId: string,
|
||||
params: OnlySearchSourceRuleParams,
|
||||
latestTimestamp: string | undefined,
|
||||
services: {
|
||||
logger: Logger;
|
||||
|
@ -34,7 +34,7 @@ export async function fetchSearchSourceQuery(
|
|||
);
|
||||
|
||||
logger.debug(
|
||||
`search source query alert (${alertId}) query: ${JSON.stringify(
|
||||
`search source query rule (${ruleId}) query: ${JSON.stringify(
|
||||
searchSource.getSearchRequestBody()
|
||||
)}`
|
||||
);
|
||||
|
@ -51,7 +51,7 @@ export async function fetchSearchSourceQuery(
|
|||
|
||||
export function updateSearchSource(
|
||||
searchSource: ISearchSource,
|
||||
params: OnlySearchSourceAlertParams,
|
||||
params: OnlySearchSourceRuleParams,
|
||||
latestTimestamp: string | undefined
|
||||
) {
|
||||
const index = searchSource.getField('index');
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/common';
|
||||
import { OnlyEsQueryAlertParams } from '../types';
|
||||
import { OnlyEsQueryRuleParams } from '../types';
|
||||
|
||||
export function getSearchParams(queryParams: OnlyEsQueryAlertParams) {
|
||||
export function getSearchParams(queryParams: OnlyEsQueryRuleParams) {
|
||||
const date = Date.now();
|
||||
const { esQuery, timeWindowSize, timeWindowUnit } = queryParams;
|
||||
|
||||
|
|
|
@ -14,29 +14,29 @@ import {
|
|||
AlertInstanceMock,
|
||||
} from '@kbn/alerting-plugin/server/mocks';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { getAlertType } from './alert_type';
|
||||
import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params';
|
||||
import { getRuleType } from './rule_type';
|
||||
import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params';
|
||||
import { ActionContext } from './action_context';
|
||||
import { ESSearchResponse, ESSearchRequest } from '@kbn/core/types/elasticsearch';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from '@kbn/core/server/elasticsearch/client/mocks';
|
||||
import { coreMock } from '@kbn/core/server/mocks';
|
||||
import { ActionGroupId, ConditionMetAlertInstanceId } from './constants';
|
||||
import { OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types';
|
||||
import { OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types';
|
||||
import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
import { Comparator } from '../../../common/comparator_types';
|
||||
|
||||
const logger = loggingSystemMock.create().get();
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const alertType = getAlertType(logger, coreSetup);
|
||||
const ruleType = getRuleType(logger, coreSetup);
|
||||
|
||||
describe('alertType', () => {
|
||||
it('alert type creation structure is the expected value', async () => {
|
||||
expect(alertType.id).toBe('.es-query');
|
||||
expect(alertType.name).toBe('Elasticsearch query');
|
||||
expect(alertType.actionGroups).toEqual([{ id: 'query matched', name: 'Query matched' }]);
|
||||
describe('ruleType', () => {
|
||||
it('rule type creation structure is the expected value', async () => {
|
||||
expect(ruleType.id).toBe('.es-query');
|
||||
expect(ruleType.name).toBe('Elasticsearch query');
|
||||
expect(ruleType.actionGroups).toEqual([{ id: 'query matched', name: 'Query matched' }]);
|
||||
|
||||
expect(alertType.actionVariables).toMatchInlineSnapshot(`
|
||||
expect(ruleType.actionVariables).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"context": Array [
|
||||
Object {
|
||||
|
@ -101,7 +101,7 @@ describe('alertType', () => {
|
|||
|
||||
describe('elasticsearch query', () => {
|
||||
it('validator succeeds with valid es query params', async () => {
|
||||
const params: Partial<Writable<OnlyEsQueryAlertParams>> = {
|
||||
const params: Partial<Writable<OnlyEsQueryRuleParams>> = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -113,14 +113,14 @@ describe('alertType', () => {
|
|||
searchType: 'esQuery',
|
||||
};
|
||||
|
||||
expect(alertType.validate?.params?.validate(params)).toBeTruthy();
|
||||
expect(ruleType.validate?.params?.validate(params)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('validator fails with invalid es query params - threshold', async () => {
|
||||
const paramsSchema = alertType.validate?.params;
|
||||
const paramsSchema = ruleType.validate?.params;
|
||||
if (!paramsSchema) throw new Error('params validator not set');
|
||||
|
||||
const params: Partial<Writable<OnlyEsQueryAlertParams>> = {
|
||||
const params: Partial<Writable<OnlyEsQueryRuleParams>> = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -137,8 +137,8 @@ describe('alertType', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('alert executor handles no documents returned by ES', async () => {
|
||||
const params: OnlyEsQueryAlertParams = {
|
||||
it('rule executor handles no documents returned by ES', async () => {
|
||||
const params: OnlyEsQueryRuleParams = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -149,16 +149,16 @@ describe('alertType', () => {
|
|||
threshold: [0],
|
||||
searchType: 'esQuery',
|
||||
};
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
const searchResult: ESSearchResponse<unknown, {}> = generateResults([]);
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult)
|
||||
);
|
||||
|
||||
const result = await invokeExecutor({ params, alertServices });
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
expect(alertServices.alertFactory.create).not.toHaveBeenCalled();
|
||||
expect(ruleServices.alertFactory.create).not.toHaveBeenCalled();
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -167,8 +167,8 @@ describe('alertType', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('alert executor returns the latestTimestamp of the newest detected document', async () => {
|
||||
const params: OnlyEsQueryAlertParams = {
|
||||
it('rule executor returns the latestTimestamp of the newest detected document', async () => {
|
||||
const params: OnlyEsQueryRuleParams = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -179,7 +179,7 @@ describe('alertType', () => {
|
|||
threshold: [0],
|
||||
searchType: 'esQuery',
|
||||
};
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
const newestDocumentTimestamp = Date.now();
|
||||
|
||||
|
@ -194,14 +194,14 @@ describe('alertType', () => {
|
|||
'time-field': newestDocumentTimestamp - 2000,
|
||||
},
|
||||
]);
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult)
|
||||
);
|
||||
|
||||
const result = await invokeExecutor({ params, alertServices });
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
expect(alertServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId);
|
||||
const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value;
|
||||
expect(ruleServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId);
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
|
@ -213,8 +213,8 @@ describe('alertType', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('alert executor correctly handles numeric time fields that were stored by legacy rules prior to v7.12.1', async () => {
|
||||
const params: OnlyEsQueryAlertParams = {
|
||||
it('rule executor correctly handles numeric time fields that were stored by legacy rules prior to v7.12.1', async () => {
|
||||
const params: OnlyEsQueryRuleParams = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -225,12 +225,12 @@ describe('alertType', () => {
|
|||
threshold: [0],
|
||||
searchType: 'esQuery',
|
||||
};
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
const previousTimestamp = Date.now();
|
||||
const newestDocumentTimestamp = previousTimestamp + 1000;
|
||||
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
generateResults([
|
||||
{
|
||||
|
@ -242,14 +242,14 @@ describe('alertType', () => {
|
|||
|
||||
const result = await invokeExecutor({
|
||||
params,
|
||||
alertServices,
|
||||
ruleServices,
|
||||
state: {
|
||||
// @ts-expect-error previousTimestamp is numeric, but should be string (this was a bug prior to v7.12.1)
|
||||
latestTimestamp: previousTimestamp,
|
||||
},
|
||||
});
|
||||
|
||||
const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value;
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
// ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward
|
||||
latestTimestamp: new Date(previousTimestamp).toISOString(),
|
||||
|
@ -262,8 +262,8 @@ describe('alertType', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('alert executor ignores previous invalid latestTimestamp values stored by legacy rules prior to v7.12.1', async () => {
|
||||
const params: OnlyEsQueryAlertParams = {
|
||||
it('rule executor ignores previous invalid latestTimestamp values stored by legacy rules prior to v7.12.1', async () => {
|
||||
const params: OnlyEsQueryRuleParams = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -274,11 +274,11 @@ describe('alertType', () => {
|
|||
threshold: [0],
|
||||
searchType: 'esQuery',
|
||||
};
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
const oldestDocumentTimestamp = Date.now();
|
||||
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
generateResults([
|
||||
{
|
||||
|
@ -291,9 +291,9 @@ describe('alertType', () => {
|
|||
)
|
||||
);
|
||||
|
||||
const result = await invokeExecutor({ params, alertServices });
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value;
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
|
@ -305,8 +305,8 @@ describe('alertType', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('alert executor carries over the queried latestTimestamp in the alert state', async () => {
|
||||
const params: OnlyEsQueryAlertParams = {
|
||||
it('rule executor carries over the queried latestTimestamp in the rule state', async () => {
|
||||
const params: OnlyEsQueryRuleParams = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -317,11 +317,11 @@ describe('alertType', () => {
|
|||
threshold: [0],
|
||||
searchType: 'esQuery',
|
||||
};
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
const oldestDocumentTimestamp = Date.now();
|
||||
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
generateResults([
|
||||
{
|
||||
|
@ -331,9 +331,9 @@ describe('alertType', () => {
|
|||
)
|
||||
);
|
||||
|
||||
const result = await invokeExecutor({ params, alertServices });
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value;
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
|
@ -345,7 +345,7 @@ describe('alertType', () => {
|
|||
});
|
||||
|
||||
const newestDocumentTimestamp = oldestDocumentTimestamp + 5000;
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
generateResults([
|
||||
{
|
||||
|
@ -360,12 +360,12 @@ describe('alertType', () => {
|
|||
|
||||
const secondResult = await invokeExecutor({
|
||||
params,
|
||||
alertServices,
|
||||
state: result as EsQueryAlertState,
|
||||
ruleServices,
|
||||
state: result as EsQueryRuleState,
|
||||
});
|
||||
|
||||
const existingInstance: AlertInstanceMock =
|
||||
alertServices.alertFactory.create.mock.results[1].value;
|
||||
ruleServices.alertFactory.create.mock.results[1].value;
|
||||
expect(existingInstance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(),
|
||||
dateStart: expect.any(String),
|
||||
|
@ -377,8 +377,8 @@ describe('alertType', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('alert executor ignores tie breaker sort values', async () => {
|
||||
const params: OnlyEsQueryAlertParams = {
|
||||
it('rule executor ignores tie breaker sort values', async () => {
|
||||
const params: OnlyEsQueryRuleParams = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -389,11 +389,11 @@ describe('alertType', () => {
|
|||
threshold: [0],
|
||||
searchType: 'esQuery',
|
||||
};
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
const oldestDocumentTimestamp = Date.now();
|
||||
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
generateResults(
|
||||
[
|
||||
|
@ -409,9 +409,9 @@ describe('alertType', () => {
|
|||
)
|
||||
);
|
||||
|
||||
const result = await invokeExecutor({ params, alertServices });
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value;
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
|
@ -423,8 +423,8 @@ describe('alertType', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('alert executor ignores results with no sort values', async () => {
|
||||
const params: OnlyEsQueryAlertParams = {
|
||||
it('rule executor ignores results with no sort values', async () => {
|
||||
const params: OnlyEsQueryRuleParams = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -435,11 +435,11 @@ describe('alertType', () => {
|
|||
threshold: [0],
|
||||
searchType: 'esQuery',
|
||||
};
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
const oldestDocumentTimestamp = Date.now();
|
||||
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
generateResults(
|
||||
[
|
||||
|
@ -456,9 +456,9 @@ describe('alertType', () => {
|
|||
)
|
||||
);
|
||||
|
||||
const result = await invokeExecutor({ params, alertServices });
|
||||
const result = await invokeExecutor({ params, ruleServices });
|
||||
|
||||
const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value;
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.replaceState).toHaveBeenCalledWith({
|
||||
latestTimestamp: undefined,
|
||||
dateStart: expect.any(String),
|
||||
|
@ -495,7 +495,7 @@ describe('alertType', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
const defaultParams: OnlySearchSourceAlertParams = {
|
||||
const defaultParams: OnlySearchSourceRuleParams = {
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
|
@ -510,12 +510,12 @@ describe('alertType', () => {
|
|||
});
|
||||
|
||||
it('validator succeeds with valid search source params', async () => {
|
||||
expect(alertType.validate?.params?.validate(defaultParams)).toBeTruthy();
|
||||
expect(ruleType.validate?.params?.validate(defaultParams)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('validator fails with invalid search source params - esQuery provided', async () => {
|
||||
const paramsSchema = alertType.validate?.params!;
|
||||
const params: Partial<Writable<EsQueryAlertParams>> = {
|
||||
const paramsSchema = ruleType.validate?.params!;
|
||||
const params: Partial<Writable<EsQueryRuleParams>> = {
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
|
@ -530,10 +530,10 @@ describe('alertType', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('alert executor handles no documents returned by ES', async () => {
|
||||
it('rule executor handles no documents returned by ES', async () => {
|
||||
const params = defaultParams;
|
||||
const searchResult: ESSearchResponse<unknown, {}> = generateResults([]);
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
(searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => {
|
||||
if (name === 'index') {
|
||||
|
@ -542,14 +542,14 @@ describe('alertType', () => {
|
|||
});
|
||||
(searchSourceInstanceMock.fetch as jest.Mock).mockResolvedValueOnce(searchResult);
|
||||
|
||||
await invokeExecutor({ params, alertServices });
|
||||
await invokeExecutor({ params, ruleServices });
|
||||
|
||||
expect(alertServices.alertFactory.create).not.toHaveBeenCalled();
|
||||
expect(ruleServices.alertFactory.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('alert executor throws an error when index does not have time field', async () => {
|
||||
it('rule executor throws an error when index does not have time field', async () => {
|
||||
const params = defaultParams;
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
(searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => {
|
||||
if (name === 'index') {
|
||||
|
@ -557,14 +557,14 @@ describe('alertType', () => {
|
|||
}
|
||||
});
|
||||
|
||||
await expect(invokeExecutor({ params, alertServices })).rejects.toThrow(
|
||||
await expect(invokeExecutor({ params, ruleServices })).rejects.toThrow(
|
||||
'Invalid data view without timeFieldName.'
|
||||
);
|
||||
});
|
||||
|
||||
it('alert executor schedule actions when condition met', async () => {
|
||||
it('rule executor schedule actions when condition met', async () => {
|
||||
const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] };
|
||||
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
|
||||
|
||||
(searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => {
|
||||
if (name === 'index') {
|
||||
|
@ -576,9 +576,9 @@ describe('alertType', () => {
|
|||
hits: { total: 3, hits: [{}, {}, {}] },
|
||||
});
|
||||
|
||||
await invokeExecutor({ params, alertServices });
|
||||
await invokeExecutor({ params, ruleServices });
|
||||
|
||||
const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value;
|
||||
const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value;
|
||||
expect(instance.scheduleActions).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -625,24 +625,24 @@ function generateResults(
|
|||
|
||||
async function invokeExecutor({
|
||||
params,
|
||||
alertServices,
|
||||
ruleServices,
|
||||
state,
|
||||
}: {
|
||||
params: OnlySearchSourceAlertParams | OnlyEsQueryAlertParams;
|
||||
alertServices: RuleExecutorServicesMock;
|
||||
state?: EsQueryAlertState;
|
||||
params: OnlySearchSourceRuleParams | OnlyEsQueryRuleParams;
|
||||
ruleServices: RuleExecutorServicesMock;
|
||||
state?: EsQueryRuleState;
|
||||
}) {
|
||||
return await alertType.executor({
|
||||
return await ruleType.executor({
|
||||
alertId: uuid.v4(),
|
||||
executionId: uuid.v4(),
|
||||
startedAt: new Date(),
|
||||
previousStartedAt: new Date(),
|
||||
services: alertServices as unknown as RuleExecutorServices<
|
||||
EsQueryAlertState,
|
||||
services: ruleServices as unknown as RuleExecutorServices<
|
||||
EsQueryRuleState,
|
||||
ActionContext,
|
||||
typeof ActionGroupId
|
||||
>,
|
||||
params: params as EsQueryAlertParams,
|
||||
params: params as EsQueryRuleParams,
|
||||
state: {
|
||||
latestTimestamp: undefined,
|
||||
...state,
|
|
@ -11,29 +11,29 @@ import { extractReferences, injectReferences } from '@kbn/data-plugin/common';
|
|||
import { RuleType } from '../../types';
|
||||
import { ActionContext } from './action_context';
|
||||
import {
|
||||
EsQueryAlertParams,
|
||||
EsQueryAlertParamsExtractedParams,
|
||||
EsQueryAlertParamsSchema,
|
||||
EsQueryAlertState,
|
||||
} from './alert_type_params';
|
||||
EsQueryRuleParams,
|
||||
EsQueryRuleParamsExtractedParams,
|
||||
EsQueryRuleParamsSchema,
|
||||
EsQueryRuleState,
|
||||
} from './rule_type_params';
|
||||
import { STACK_ALERTS_FEATURE_ID } from '../../../common';
|
||||
import { ExecutorOptions } from './types';
|
||||
import { ActionGroupId, ES_QUERY_ID } from './constants';
|
||||
import { executor } from './executor';
|
||||
import { isEsQueryAlert } from './util';
|
||||
import { isEsQueryRule } from './util';
|
||||
|
||||
export function getAlertType(
|
||||
export function getRuleType(
|
||||
logger: Logger,
|
||||
core: CoreSetup
|
||||
): RuleType<
|
||||
EsQueryAlertParams,
|
||||
EsQueryAlertParamsExtractedParams,
|
||||
EsQueryAlertState,
|
||||
EsQueryRuleParams,
|
||||
EsQueryRuleParamsExtractedParams,
|
||||
EsQueryRuleState,
|
||||
{},
|
||||
ActionContext,
|
||||
typeof ActionGroupId
|
||||
> {
|
||||
const alertTypeName = i18n.translate('xpack.stackAlerts.esQuery.alertTypeTitle', {
|
||||
const ruleTypeName = i18n.translate('xpack.stackAlerts.esQuery.alertTypeTitle', {
|
||||
defaultMessage: 'Elasticsearch query',
|
||||
});
|
||||
|
||||
|
@ -137,11 +137,11 @@ export function getAlertType(
|
|||
|
||||
return {
|
||||
id: ES_QUERY_ID,
|
||||
name: alertTypeName,
|
||||
name: ruleTypeName,
|
||||
actionGroups: [{ id: ActionGroupId, name: actionGroupName }],
|
||||
defaultActionGroupId: ActionGroupId,
|
||||
validate: {
|
||||
params: EsQueryAlertParamsSchema,
|
||||
params: EsQueryRuleParamsSchema,
|
||||
},
|
||||
actionVariables: {
|
||||
context: [
|
||||
|
@ -164,15 +164,15 @@ export function getAlertType(
|
|||
},
|
||||
useSavedObjectReferences: {
|
||||
extractReferences: (params) => {
|
||||
if (isEsQueryAlert(params.searchType)) {
|
||||
return { params: params as EsQueryAlertParamsExtractedParams, references: [] };
|
||||
if (isEsQueryRule(params.searchType)) {
|
||||
return { params: params as EsQueryRuleParamsExtractedParams, references: [] };
|
||||
}
|
||||
const [searchConfiguration, references] = extractReferences(params.searchConfiguration);
|
||||
const newParams = { ...params, searchConfiguration } as EsQueryAlertParamsExtractedParams;
|
||||
const newParams = { ...params, searchConfiguration } as EsQueryRuleParamsExtractedParams;
|
||||
return { params: newParams, references };
|
||||
},
|
||||
injectReferences: (params, references) => {
|
||||
if (isEsQueryAlert(params.searchType)) {
|
||||
if (isEsQueryRule(params.searchType)) {
|
||||
return params;
|
||||
}
|
||||
return {
|
||||
|
@ -183,9 +183,10 @@ export function getAlertType(
|
|||
},
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
executor: async (options: ExecutorOptions<EsQueryAlertParams>) => {
|
||||
executor: async (options: ExecutorOptions<EsQueryRuleParams>) => {
|
||||
return await executor(logger, core, options);
|
||||
},
|
||||
producer: STACK_ALERTS_FEATURE_ID,
|
||||
doesSetRecoveryContext: true,
|
||||
};
|
||||
}
|
|
@ -9,12 +9,12 @@ import { TypeOf } from '@kbn/config-schema';
|
|||
import type { Writable } from '@kbn/utility-types';
|
||||
import { Comparator } from '../../../common/comparator_types';
|
||||
import {
|
||||
EsQueryAlertParamsSchema,
|
||||
EsQueryAlertParams,
|
||||
EsQueryRuleParamsSchema,
|
||||
EsQueryRuleParams,
|
||||
ES_QUERY_MAX_HITS_PER_EXECUTION,
|
||||
} from './alert_type_params';
|
||||
} from './rule_type_params';
|
||||
|
||||
const DefaultParams: Writable<Partial<EsQueryAlertParams>> = {
|
||||
const DefaultParams: Writable<Partial<EsQueryRuleParams>> = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
|
@ -220,7 +220,7 @@ describe('alertType Params validate()', () => {
|
|||
return () => validate();
|
||||
}
|
||||
|
||||
function validate(): TypeOf<typeof EsQueryAlertParamsSchema> {
|
||||
return EsQueryAlertParamsSchema.validate(params);
|
||||
function validate(): TypeOf<typeof EsQueryRuleParamsSchema> {
|
||||
return EsQueryRuleParamsSchema.validate(params);
|
||||
}
|
||||
});
|
|
@ -16,19 +16,19 @@ import { getComparatorSchemaType } from '../lib/comparator';
|
|||
|
||||
export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000;
|
||||
|
||||
// alert type parameters
|
||||
export type EsQueryAlertParams = TypeOf<typeof EsQueryAlertParamsSchema>;
|
||||
export interface EsQueryAlertState extends RuleTypeState {
|
||||
// rule type parameters
|
||||
export type EsQueryRuleParams = TypeOf<typeof EsQueryRuleParamsSchema>;
|
||||
export interface EsQueryRuleState extends RuleTypeState {
|
||||
latestTimestamp: string | undefined;
|
||||
}
|
||||
|
||||
export type EsQueryAlertParamsExtractedParams = Omit<EsQueryAlertParams, 'searchConfiguration'> & {
|
||||
export type EsQueryRuleParamsExtractedParams = Omit<EsQueryRuleParams, 'searchConfiguration'> & {
|
||||
searchConfiguration: SerializedSearchSourceFields & {
|
||||
indexRefName: string;
|
||||
};
|
||||
};
|
||||
|
||||
const EsQueryAlertParamsSchemaProperties = {
|
||||
const EsQueryRuleParamsSchemaProperties = {
|
||||
size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }),
|
||||
timeWindowSize: schema.number({ min: 1 }),
|
||||
timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }),
|
||||
|
@ -37,14 +37,14 @@ const EsQueryAlertParamsSchemaProperties = {
|
|||
searchType: schema.oneOf([schema.literal('searchSource'), schema.literal('esQuery')], {
|
||||
defaultValue: 'esQuery',
|
||||
}),
|
||||
// searchSource alert param only
|
||||
// searchSource rule param only
|
||||
searchConfiguration: schema.conditional(
|
||||
schema.siblingRef('searchType'),
|
||||
schema.literal('searchSource'),
|
||||
schema.object({}, { unknowns: 'allow' }),
|
||||
schema.never()
|
||||
),
|
||||
// esQuery alert params only
|
||||
// esQuery rule params only
|
||||
esQuery: schema.conditional(
|
||||
schema.siblingRef('searchType'),
|
||||
schema.literal('esQuery'),
|
||||
|
@ -65,7 +65,7 @@ const EsQueryAlertParamsSchemaProperties = {
|
|||
),
|
||||
};
|
||||
|
||||
export const EsQueryAlertParamsSchema = schema.object(EsQueryAlertParamsSchemaProperties, {
|
||||
export const EsQueryRuleParamsSchema = schema.object(EsQueryRuleParamsSchemaProperties, {
|
||||
validate: validateParams,
|
||||
});
|
||||
|
||||
|
@ -73,7 +73,7 @@ const betweenComparators = new Set(['between', 'notBetween']);
|
|||
|
||||
// using direct type not allowed, circular reference, so body is typed to any
|
||||
function validateParams(anyParams: unknown): string | undefined {
|
||||
const { esQuery, thresholdComparator, threshold, searchType } = anyParams as EsQueryAlertParams;
|
||||
const { esQuery, thresholdComparator, threshold, searchType } = anyParams as EsQueryRuleParams;
|
||||
|
||||
if (betweenComparators.has(thresholdComparator) && threshold.length === 1) {
|
||||
return i18n.translate('xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage', {
|
|
@ -7,15 +7,15 @@
|
|||
|
||||
import { RuleExecutorOptions, RuleTypeParams } from '../../types';
|
||||
import { ActionContext } from './action_context';
|
||||
import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params';
|
||||
import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params';
|
||||
import { ActionGroupId } from './constants';
|
||||
|
||||
export type OnlyEsQueryAlertParams = Omit<EsQueryAlertParams, 'searchConfiguration'> & {
|
||||
export type OnlyEsQueryRuleParams = Omit<EsQueryRuleParams, 'searchConfiguration'> & {
|
||||
searchType: 'esQuery';
|
||||
};
|
||||
|
||||
export type OnlySearchSourceAlertParams = Omit<
|
||||
EsQueryAlertParams,
|
||||
export type OnlySearchSourceRuleParams = Omit<
|
||||
EsQueryRuleParams,
|
||||
'esQuery' | 'index' | 'timeField'
|
||||
> & {
|
||||
searchType: 'searchSource';
|
||||
|
@ -23,7 +23,7 @@ export type OnlySearchSourceAlertParams = Omit<
|
|||
|
||||
export type ExecutorOptions<P extends RuleTypeParams> = RuleExecutorOptions<
|
||||
P,
|
||||
EsQueryAlertState,
|
||||
EsQueryRuleState,
|
||||
{},
|
||||
ActionContext,
|
||||
typeof ActionGroupId
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EsQueryAlertParams } from './alert_type_params';
|
||||
import { EsQueryRuleParams } from './rule_type_params';
|
||||
|
||||
export function isEsQueryAlert(searchType: EsQueryAlertParams['searchType']) {
|
||||
export function isEsQueryRule(searchType: EsQueryRuleParams['searchType']) {
|
||||
return searchType !== 'searchSource';
|
||||
}
|
||||
|
|
|
@ -27747,8 +27747,6 @@
|
|||
"xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel": "Tableau de valeurs à utiliser comme seuil ; \"between\" et \"notBetween\" requièrent deux valeurs, les autres n'en requièrent qu'une seule.",
|
||||
"xpack.stackAlerts.esQuery.actionVariableContextTitleLabel": "Titre pour l'alerte.",
|
||||
"xpack.stackAlerts.esQuery.actionVariableContextValueLabel": "Valeur ayant rempli la condition de seuil.",
|
||||
"xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription": "Le nombre de documents correspondants est {thresholdComparator} {threshold}",
|
||||
"xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle": "l'alerte \"{name}\" correspond à la recherche",
|
||||
"xpack.stackAlerts.esQuery.alertTypeTitle": "Recherche Elasticsearch",
|
||||
"xpack.stackAlerts.esQuery.invalidComparatorErrorMessage": "thresholdComparator spécifié non valide : {comparator}",
|
||||
"xpack.stackAlerts.esQuery.invalidEsQueryErrorMessage": "[esQuery] : doit être au format JSON valide",
|
||||
|
|
|
@ -27907,8 +27907,6 @@
|
|||
"xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel": "しきい値として使用する値の配列。「between」と「notBetween」には2つの値が必要です。その他は1つの値が必要です。",
|
||||
"xpack.stackAlerts.esQuery.actionVariableContextTitleLabel": "アラートのタイトル。",
|
||||
"xpack.stackAlerts.esQuery.actionVariableContextValueLabel": "しきい値条件を満たした値。",
|
||||
"xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription": "一致するドキュメント数は{thresholdComparator} {threshold}です",
|
||||
"xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle": "アラート'{name}'はクエリと一致しました",
|
||||
"xpack.stackAlerts.esQuery.alertTypeTitle": "Elasticsearch クエリ",
|
||||
"xpack.stackAlerts.esQuery.invalidComparatorErrorMessage": "無効な thresholdComparator が指定されました:{comparator}",
|
||||
"xpack.stackAlerts.esQuery.invalidEsQueryErrorMessage": "[esQuery]:有効なJSONでなければなりません",
|
||||
|
|
|
@ -27940,8 +27940,6 @@
|
|||
"xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel": "用作阈值的值数组;“between”和“notBetween”需要两个值,其他则需要一个值。",
|
||||
"xpack.stackAlerts.esQuery.actionVariableContextTitleLabel": "告警的标题。",
|
||||
"xpack.stackAlerts.esQuery.actionVariableContextValueLabel": "满足阈值条件的值。",
|
||||
"xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription": "匹配文档的数目{thresholdComparator} {threshold}",
|
||||
"xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle": "告警“{name}”已匹配查询",
|
||||
"xpack.stackAlerts.esQuery.alertTypeTitle": "Elasticsearch 查询",
|
||||
"xpack.stackAlerts.esQuery.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}",
|
||||
"xpack.stackAlerts.esQuery.invalidEsQueryErrorMessage": "[esQuery]:必须是有效的 JSON",
|
||||
|
|
|
@ -10,6 +10,6 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function alertingTests({ loadTestFile }: FtrProviderContext) {
|
||||
describe('es_query', () => {
|
||||
loadTestFile(require.resolve('./alert'));
|
||||
loadTestFile(require.resolve('./rule'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,19 +17,19 @@ import {
|
|||
} from '../../../../../common/lib';
|
||||
import { createEsDocuments } from '../lib/create_test_data';
|
||||
|
||||
const ALERT_TYPE_ID = '.es-query';
|
||||
const ACTION_TYPE_ID = '.index';
|
||||
const ES_TEST_INDEX_SOURCE = 'builtin-alert:es-query';
|
||||
const RULE_TYPE_ID = '.es-query';
|
||||
const CONNECTOR_TYPE_ID = '.index';
|
||||
const ES_TEST_INDEX_SOURCE = 'builtin-rule:es-query';
|
||||
const ES_TEST_INDEX_REFERENCE = '-na-';
|
||||
const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-output`;
|
||||
|
||||
const ALERT_INTERVALS_TO_WRITE = 5;
|
||||
const ALERT_INTERVAL_SECONDS = 4;
|
||||
const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000;
|
||||
const RULE_INTERVALS_TO_WRITE = 5;
|
||||
const RULE_INTERVAL_SECONDS = 4;
|
||||
const RULE_INTERVAL_MILLIS = RULE_INTERVAL_SECONDS * 1000;
|
||||
const ES_GROUPS_TO_WRITE = 3;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function alertTests({ getService }: FtrProviderContext) {
|
||||
export default function ruleTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
const indexPatterns = getService('indexPatterns');
|
||||
|
@ -37,9 +37,9 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME);
|
||||
|
||||
describe('alert', async () => {
|
||||
describe('rule', async () => {
|
||||
let endDate: string;
|
||||
let actionId: string;
|
||||
let connectorId: string;
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -49,10 +49,10 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
await esTestIndexToolOutput.destroy();
|
||||
await esTestIndexToolOutput.setup();
|
||||
|
||||
actionId = await createAction(supertest, objectRemover);
|
||||
connectorId = await createConnector(supertest, objectRemover);
|
||||
|
||||
// write documents in the future, figure out the end date
|
||||
const endDateMillis = Date.now() + (ALERT_INTERVALS_TO_WRITE - 1) * ALERT_INTERVAL_MILLIS;
|
||||
const endDateMillis = Date.now() + (RULE_INTERVALS_TO_WRITE - 1) * RULE_INTERVAL_MILLIS;
|
||||
endDate = new Date(endDateMillis).toISOString();
|
||||
});
|
||||
|
||||
|
@ -66,14 +66,14 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
[
|
||||
'esQuery',
|
||||
async () => {
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'never fire',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
threshold: [0],
|
||||
});
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'always fire',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
|
@ -90,7 +90,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
{ override: true },
|
||||
getUrlPrefix(Spaces.space1.id)
|
||||
);
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'never fire',
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
|
@ -105,7 +105,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
filter: [],
|
||||
},
|
||||
});
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'always fire',
|
||||
size: 100,
|
||||
thresholdComparator: '>',
|
||||
|
@ -125,7 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
].forEach(([searchType, initData]) =>
|
||||
it(`runs correctly: threshold on hit count < > for ${searchType} search type`, async () => {
|
||||
// write documents from now to the future end date in groups
|
||||
createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
|
||||
await initData();
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
|
@ -135,14 +135,14 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
const { name, title, message } = doc._source.params;
|
||||
|
||||
expect(name).to.be('always fire');
|
||||
expect(title).to.be(`alert 'always fire' matched query`);
|
||||
expect(title).to.be(`rule 'always fire' matched query`);
|
||||
const messagePattern =
|
||||
/alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
/rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
expect(message).to.match(messagePattern);
|
||||
expect(hits).not.to.be.empty();
|
||||
|
||||
// during the first execution, the latestTimestamp value should be empty
|
||||
// since this alert always fires, the latestTimestamp value should be updated each execution
|
||||
// since this rule always fires, the latestTimestamp value should be updated each execution
|
||||
if (!i) {
|
||||
expect(previousTimestamp).to.be.empty();
|
||||
} else {
|
||||
|
@ -156,7 +156,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
[
|
||||
'esQuery',
|
||||
async () => {
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'never fire',
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
|
@ -164,7 +164,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
timeField: 'date_epoch_millis',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
});
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'always fire',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
|
@ -182,7 +182,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
{ override: true },
|
||||
getUrlPrefix(Spaces.space1.id)
|
||||
);
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'never fire',
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
|
@ -197,7 +197,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
filter: [],
|
||||
},
|
||||
});
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'always fire',
|
||||
size: 100,
|
||||
thresholdComparator: '>',
|
||||
|
@ -217,7 +217,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
].forEach(([searchType, initData]) =>
|
||||
it(`runs correctly: use epoch millis - threshold on hit count < > for ${searchType} search type`, async () => {
|
||||
// write documents from now to the future end date in groups
|
||||
createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
|
||||
await initData();
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
|
@ -227,14 +227,14 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
const { name, title, message } = doc._source.params;
|
||||
|
||||
expect(name).to.be('always fire');
|
||||
expect(title).to.be(`alert 'always fire' matched query`);
|
||||
expect(title).to.be(`rule 'always fire' matched query`);
|
||||
const messagePattern =
|
||||
/alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
/rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
expect(message).to.match(messagePattern);
|
||||
expect(hits).not.to.be.empty();
|
||||
|
||||
// during the first execution, the latestTimestamp value should be empty
|
||||
// since this alert always fires, the latestTimestamp value should be updated each execution
|
||||
// since this rule always fires, the latestTimestamp value should be updated each execution
|
||||
if (!i) {
|
||||
expect(previousTimestamp).to.be.empty();
|
||||
} else {
|
||||
|
@ -265,17 +265,17 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
},
|
||||
};
|
||||
};
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'never fire',
|
||||
esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)),
|
||||
esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * RULE_INTERVALS_TO_WRITE + 1)),
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
threshold: [-1],
|
||||
});
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'fires once',
|
||||
esQuery: JSON.stringify(
|
||||
rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2))
|
||||
rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * RULE_INTERVALS_TO_WRITE) / 2))
|
||||
),
|
||||
size: 100,
|
||||
thresholdComparator: '>=',
|
||||
|
@ -291,7 +291,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
{ override: true },
|
||||
getUrlPrefix(Spaces.space1.id)
|
||||
);
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'never fire',
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
|
@ -299,14 +299,14 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
searchType: 'searchSource',
|
||||
searchConfiguration: {
|
||||
query: {
|
||||
query: `testedValue > ${ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1}`,
|
||||
query: `testedValue > ${ES_GROUPS_TO_WRITE * RULE_INTERVALS_TO_WRITE + 1}`,
|
||||
language: 'kuery',
|
||||
},
|
||||
index: esTestDataView.id,
|
||||
filter: [],
|
||||
},
|
||||
});
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'fires once',
|
||||
size: 100,
|
||||
thresholdComparator: '>=',
|
||||
|
@ -315,7 +315,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
searchConfiguration: {
|
||||
query: {
|
||||
query: `testedValue > ${Math.floor(
|
||||
(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2
|
||||
(ES_GROUPS_TO_WRITE * RULE_INTERVALS_TO_WRITE) / 2
|
||||
)}`,
|
||||
language: 'kuery',
|
||||
},
|
||||
|
@ -328,7 +328,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
].forEach(([searchType, initData]) =>
|
||||
it(`runs correctly with query: threshold on hit count < > for ${searchType}`, async () => {
|
||||
// write documents from now to the future end date in groups
|
||||
createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
|
||||
await initData();
|
||||
|
||||
const docs = await waitForDocs(1);
|
||||
|
@ -337,9 +337,9 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
const { name, title, message } = doc._source.params;
|
||||
|
||||
expect(name).to.be('fires once');
|
||||
expect(title).to.be(`alert 'fires once' matched query`);
|
||||
expect(title).to.be(`rule 'fires once' matched query`);
|
||||
const messagePattern =
|
||||
/alert 'fires once' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than or equal to 0 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
/rule 'fires once' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than or equal to 0 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
expect(message).to.match(messagePattern);
|
||||
expect(hits).not.to.be.empty();
|
||||
expect(previousTimestamp).to.be.empty();
|
||||
|
@ -351,7 +351,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
[
|
||||
'esQuery',
|
||||
async () => {
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'always fire',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
|
@ -369,7 +369,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
getUrlPrefix(Spaces.space1.id)
|
||||
);
|
||||
|
||||
await createAlert({
|
||||
await createRule({
|
||||
name: 'always fire',
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
|
@ -397,14 +397,14 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
const { name, title, message } = doc._source.params;
|
||||
|
||||
expect(name).to.be('always fire');
|
||||
expect(title).to.be(`alert 'always fire' matched query`);
|
||||
expect(title).to.be(`rule 'always fire' matched query`);
|
||||
const messagePattern =
|
||||
/alert 'always fire' is active:\n\n- Value: 0+\n- Conditions Met: Number of matching documents is less than 1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
/rule 'always fire' is active:\n\n- Value: 0+\n- Conditions Met: Number of matching documents is less than 1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
|
||||
expect(message).to.match(messagePattern);
|
||||
expect(hits).to.be.empty();
|
||||
|
||||
// during the first execution, the latestTimestamp value should be empty
|
||||
// since this alert always fires, the latestTimestamp value should be updated each execution
|
||||
// since this rule always fires, the latestTimestamp value should be updated each execution
|
||||
if (!i) {
|
||||
expect(previousTimestamp).to.be.empty();
|
||||
} else {
|
||||
|
@ -414,13 +414,98 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
})
|
||||
);
|
||||
|
||||
[
|
||||
[
|
||||
'esQuery',
|
||||
async () => {
|
||||
// This rule should be active initially when the number of documents is below the threshold
|
||||
// and then recover when we add more documents.
|
||||
await createRule({
|
||||
name: 'fire then recovers',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
threshold: [1],
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
timeWindowSize: RULE_INTERVAL_SECONDS,
|
||||
});
|
||||
},
|
||||
] as const,
|
||||
[
|
||||
'searchSource',
|
||||
async () => {
|
||||
const esTestDataView = await indexPatterns.create(
|
||||
{ title: ES_TEST_INDEX_NAME, timeFieldName: 'date' },
|
||||
{ override: true },
|
||||
getUrlPrefix(Spaces.space1.id)
|
||||
);
|
||||
// This rule should be active initially when the number of documents is below the threshold
|
||||
// and then recover when we add more documents.
|
||||
await createRule({
|
||||
name: 'fire then recovers',
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
threshold: [1],
|
||||
searchType: 'searchSource',
|
||||
searchConfiguration: {
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
},
|
||||
index: esTestDataView.id,
|
||||
filter: [],
|
||||
},
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
timeWindowSize: RULE_INTERVAL_SECONDS,
|
||||
});
|
||||
},
|
||||
] as const,
|
||||
].forEach(([searchType, initData]) =>
|
||||
it(`runs correctly and populates recovery context for ${searchType} search type`, async () => {
|
||||
await initData();
|
||||
|
||||
// delay to let rule run once before adding data
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
await createEsDocumentsInGroups(1);
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
const activeDoc = docs[0];
|
||||
const {
|
||||
name: activeName,
|
||||
title: activeTitle,
|
||||
value: activeValue,
|
||||
message: activeMessage,
|
||||
} = activeDoc._source.params;
|
||||
|
||||
expect(activeName).to.be('fire then recovers');
|
||||
expect(activeTitle).to.be(`rule 'fire then recovers' matched query`);
|
||||
expect(activeValue).to.be('0');
|
||||
expect(activeMessage).to.match(
|
||||
/rule 'fire then recovers' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is less than 1 over 4s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/
|
||||
);
|
||||
|
||||
const recoveredDoc = docs[1];
|
||||
const {
|
||||
name: recoveredName,
|
||||
title: recoveredTitle,
|
||||
message: recoveredMessage,
|
||||
} = recoveredDoc._source.params;
|
||||
|
||||
expect(recoveredName).to.be('fire then recovers');
|
||||
expect(recoveredTitle).to.be(`rule 'fire then recovers' recovered`);
|
||||
expect(recoveredMessage).to.match(
|
||||
/rule 'fire then recovers' is recovered:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is NOT less than 1 over 4s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
async function createEsDocumentsInGroups(groups: number) {
|
||||
await createEsDocuments(
|
||||
es,
|
||||
esTestIndexTool,
|
||||
endDate,
|
||||
ALERT_INTERVALS_TO_WRITE,
|
||||
ALERT_INTERVAL_MILLIS,
|
||||
RULE_INTERVALS_TO_WRITE,
|
||||
RULE_INTERVAL_MILLIS,
|
||||
groups
|
||||
);
|
||||
}
|
||||
|
@ -433,7 +518,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
);
|
||||
}
|
||||
|
||||
interface CreateAlertParams {
|
||||
interface CreateRuleParams {
|
||||
name: string;
|
||||
size: number;
|
||||
thresholdComparator: string;
|
||||
|
@ -443,11 +528,12 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
timeField?: string;
|
||||
searchConfiguration?: unknown;
|
||||
searchType?: 'searchSource';
|
||||
notifyWhen?: string;
|
||||
}
|
||||
|
||||
async function createAlert(params: CreateAlertParams): Promise<string> {
|
||||
async function createRule(params: CreateRuleParams): Promise<string> {
|
||||
const action = {
|
||||
id: actionId,
|
||||
id: connectorId,
|
||||
group: 'query matched',
|
||||
params: {
|
||||
documents: [
|
||||
|
@ -455,7 +541,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
source: ES_TEST_INDEX_SOURCE,
|
||||
reference: ES_TEST_INDEX_REFERENCE,
|
||||
params: {
|
||||
name: '{{{alertName}}}',
|
||||
name: '{{{rule.name}}}',
|
||||
value: '{{{context.value}}}',
|
||||
title: '{{{context.title}}}',
|
||||
message: '{{{context.message}}}',
|
||||
|
@ -468,7 +554,28 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
},
|
||||
};
|
||||
|
||||
const alertParams =
|
||||
const recoveryAction = {
|
||||
id: connectorId,
|
||||
group: 'recovered',
|
||||
params: {
|
||||
documents: [
|
||||
{
|
||||
source: ES_TEST_INDEX_SOURCE,
|
||||
reference: ES_TEST_INDEX_REFERENCE,
|
||||
params: {
|
||||
name: '{{{rule.name}}}',
|
||||
value: '{{{context.value}}}',
|
||||
title: '{{{context.title}}}',
|
||||
message: '{{{context.message}}}',
|
||||
},
|
||||
hits: '{{context.hits}}',
|
||||
date: '{{{context.date}}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const ruleParams =
|
||||
params.searchType === 'searchSource'
|
||||
? {
|
||||
searchConfiguration: params.searchConfiguration,
|
||||
|
@ -479,44 +586,44 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
esQuery: params.esQuery,
|
||||
};
|
||||
|
||||
const { body: createdAlert } = await supertest
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: params.name,
|
||||
consumer: 'alerts',
|
||||
enabled: true,
|
||||
rule_type_id: ALERT_TYPE_ID,
|
||||
schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` },
|
||||
actions: [action],
|
||||
notify_when: 'onActiveAlert',
|
||||
rule_type_id: RULE_TYPE_ID,
|
||||
schedule: { interval: `${RULE_INTERVAL_SECONDS}s` },
|
||||
actions: [action, recoveryAction],
|
||||
notify_when: params.notifyWhen || 'onActiveAlert',
|
||||
params: {
|
||||
size: params.size,
|
||||
timeWindowSize: params.timeWindowSize || ALERT_INTERVAL_SECONDS * 5,
|
||||
timeWindowSize: params.timeWindowSize || RULE_INTERVAL_SECONDS * 5,
|
||||
timeWindowUnit: 's',
|
||||
thresholdComparator: params.thresholdComparator,
|
||||
threshold: params.threshold,
|
||||
searchType: params.searchType,
|
||||
...alertParams,
|
||||
...ruleParams,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const alertId = createdAlert.id;
|
||||
objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting');
|
||||
const ruleId = createdRule.id;
|
||||
objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting');
|
||||
|
||||
return alertId;
|
||||
return ruleId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createAction(supertest: any, objectRemover: ObjectRemover): Promise<string> {
|
||||
const { body: createdAction } = await supertest
|
||||
async function createConnector(supertest: any, objectRemover: ObjectRemover): Promise<string> {
|
||||
const { body: createdConnector } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'index action for es query FT',
|
||||
connector_type_id: ACTION_TYPE_ID,
|
||||
connector_type_id: CONNECTOR_TYPE_ID,
|
||||
config: {
|
||||
index: ES_TEST_OUTPUT_INDEX_NAME,
|
||||
},
|
||||
|
@ -524,8 +631,8 @@ async function createAction(supertest: any, objectRemover: ObjectRemover): Promi
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
const actionId = createdAction.id;
|
||||
objectRemover.add(Spaces.space1.id, actionId, 'connector', 'actions');
|
||||
const connectorId = createdConnector.id;
|
||||
objectRemover.add(Spaces.space1.id, connectorId, 'connector', 'actions');
|
||||
|
||||
return actionId;
|
||||
return connectorId;
|
||||
}
|
|
@ -26,14 +26,16 @@ export async function createEsDocuments(
|
|||
const endDateMillis = Date.parse(endDate) - intervalMillis / 2;
|
||||
|
||||
let testedValue = 0;
|
||||
const promises: Array<Promise<unknown>> = [];
|
||||
times(intervals, (interval) => {
|
||||
const date = endDateMillis - interval * intervalMillis;
|
||||
|
||||
// don't need await on these, wait at the end of the function
|
||||
times(groups, () => {
|
||||
createEsDocument(es, date, testedValue++);
|
||||
promises.push(createEsDocument(es, date, testedValue++));
|
||||
});
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
const totalDocuments = intervals * groups;
|
||||
await esTestIndexTool.waitForDocs(DOCUMENT_SOURCE, DOCUMENT_REFERENCE, totalDocuments);
|
||||
|
@ -51,6 +53,7 @@ async function createEsDocument(es: Client, epochMillis: number, testedValue: nu
|
|||
const response = await es.index({
|
||||
id: uuid(),
|
||||
index: ES_TEST_INDEX_NAME,
|
||||
refresh: 'wait_for',
|
||||
body: document,
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue