[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:
Ying Mao 2022-06-04 09:57:47 -04:00 committed by GitHub
parent eadedd12c9
commit a5a287b383
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 473 additions and 296 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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でなければなりません",

View file

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

View file

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

View file

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

View file

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