From 3c2023c725cd9334b9f76f7bbbbaa4066622b2af Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Sat, 4 Jun 2022 09:57:47 -0400 Subject: [PATCH] [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> --- .../es_query/action_context.test.ts | 61 +++-- .../alert_types/es_query/action_context.ts | 33 +-- .../alert_types/es_query/executor.test.ts | 35 ++- .../server/alert_types/es_query/executor.ts | 107 ++++---- .../server/alert_types/es_query/index.ts | 4 +- .../es_query/lib/fetch_es_query.ts | 14 +- .../lib/fetch_search_source_query.test.ts | 4 +- .../es_query/lib/fetch_search_source_query.ts | 10 +- .../es_query/lib/get_search_params.ts | 4 +- .../{alert_type.test.ts => rule_type.test.ts} | 168 ++++++------ .../es_query/{alert_type.ts => rule_type.ts} | 37 +-- ...arams.test.ts => rule_type_params.test.ts} | 12 +- ...ert_type_params.ts => rule_type_params.ts} | 18 +- .../server/alert_types/es_query/types.ts | 10 +- .../server/alert_types/es_query/util.ts | 4 +- .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../builtin_alert_types/es_query/index.ts | 2 +- .../es_query/{alert.ts => rule.ts} | 239 +++++++++++++----- .../lib/create_test_data.ts | 5 +- 21 files changed, 475 insertions(+), 298 deletions(-) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type.test.ts => rule_type.test.ts} (73%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type.ts => rule_type.ts} (88%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type_params.test.ts => rule_type_params.test.ts} (96%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type_params.ts => rule_type_params.ts} (87%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/{alert.ts => rule.ts} (63%) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts index 884bf606d2f90cb..c19d4ed5c3d1f7b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts @@ -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 diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts index 68367e5ec81045a..f25b35c6c63d697 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts @@ -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; +type RuleInfo = Pick; -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', }, }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts index 7b4cc7521654bb0..97b02a4dc723ed4 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts @@ -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`); + }); + }); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts index 6e47c5f471d884b..5f33eeb0af845f2 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts @@ -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 + options: ExecutorOptions ) { - 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 } - ); - - // apply the alert condition + : await fetchSearchSourceQuery(ruleId, params as OnlySearchSourceRuleParams, latestTimestamp, { + searchSourceClient, + logger, + }); + + // 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 = { + 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 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 baseActiveContext: EsQueryRuleActionContext = { + ...baseContext, + conditions: getContextConditionsDescription(params.thresholdComparator, params.threshold), + } as EsQueryRuleActionContext; + + 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 ' : '', }, }); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts index 82f8297a85bb58e..54bfabdf49ad605 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts @@ -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)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts index b4d74412b7f83d7..97acd154166892c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts @@ -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, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts index 48082f565afb32d..6b177d1b94a861d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts @@ -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', diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts index 66e5ae8023a47f0..e3922adf1e15c39 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts @@ -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'); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts index 29bb7ad54480498..126ddb3009287c7 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts @@ -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; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts similarity index 73% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts index 3304ca5e902f738..8e54a9ac9da8f68 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts @@ -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> = { + const params: Partial> = { 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> = { + const params: Partial> = { 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 = 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> = { + const paramsSchema = ruleType.validate?.params!; + const params: Partial> = { 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 = 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, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts similarity index 88% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts index dfab69f445629e3..27e79e86fb3c3f0 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts @@ -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) => { + executor: async (options: ExecutorOptions) => { return await executor(logger, core, options); }, producer: STACK_ALERTS_FEATURE_ID, + doesSetRecoveryContext: true, }; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.test.ts similarity index 96% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.test.ts index a1155fedb7a0294..865cf330b1c430d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.test.ts @@ -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> = { +const DefaultParams: Writable> = { 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 { - return EsQueryAlertParamsSchema.validate(params); + function validate(): TypeOf { + return EsQueryRuleParamsSchema.validate(params); } }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.ts similarity index 87% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.ts index d32fce9debbc2e2..a705e84ae54c7ec 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.ts @@ -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; -export interface EsQueryAlertState extends RuleTypeState { +// rule type parameters +export type EsQueryRuleParams = TypeOf; +export interface EsQueryRuleState extends RuleTypeState { latestTimestamp: string | undefined; } -export type EsQueryAlertParamsExtractedParams = Omit & { +export type EsQueryRuleParamsExtractedParams = Omit & { 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', { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts index 8595870a849405a..2b0f0f7a7407c6c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts @@ -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 & { +export type OnlyEsQueryRuleParams = Omit & { 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

= RuleExecutorOptions< P, - EsQueryAlertState, + EsQueryRuleState, {}, ActionContext, typeof ActionGroupId diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts index b58a362cd27e9a4..064a7f64b4c3222 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts @@ -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'; } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 51a995ab3c01366..475b8e93266588c 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3977b71638cce10..6723931c5ee6861 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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でなければなりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 90c0a92e7b610c8..0c8178d08e9ee51 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.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", diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts index 2f2ccc4f4f3825f..8f10635975ba09b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts @@ -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')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/rule.ts similarity index 63% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/rule.ts index 274c3f06b5d363f..35a6f296565ed2c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/rule.ts @@ -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 { + async function createRule(params: CreateRuleParams): Promise { 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 { - const { body: createdAction } = await supertest +async function createConnector(supertest: any, objectRemover: ObjectRemover): Promise { + 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; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts index 73a81904d0cc0d6..a234625f01824a4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts @@ -26,14 +26,16 @@ export async function createEsDocuments( const endDateMillis = Date.parse(endDate) - intervalMillis / 2; let testedValue = 0; + const promises: Array> = []; 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, });