diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts index b42076952b87a..5ee49912586c5 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts @@ -154,6 +154,7 @@ export async function fetchEsQuery({ esResult: searchResult, resultLimit: alertLimit, sourceFieldsParams: params.sourceFields, + termField: params.termField, }), link, index: params.index, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/query_dsl_only.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/query_dsl_only.ts index 6fcdf9be2f40f..b25368b14ae74 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/query_dsl_only.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/query_dsl_only.ts @@ -33,7 +33,7 @@ export default function ruleTests({ getService }: FtrProviderContext) { const { es, esTestIndexTool, esTestIndexToolOutput, createEsDocumentsInGroups, waitForDocs } = getRuleServices(getService); - describe('rule', () => { + describe('Query DSL only', () => { let endDate: string; let connectorId: string; const objectRemover = new ObjectRemover(supertest); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/index.ts new file mode 100644 index 0000000000000..8afdd96ad6455 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('ElasticSearch query rule', () => { + loadTestFile(require.resolve('./query_dsl')); + loadTestFile(require.resolve('./query_dsl_with_group_by')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query_rule.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl.ts similarity index 97% rename from x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query_rule.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl.ts index 1484158a9a991..58d082497faef 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query_rule.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services'; -import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const esClient = getService('es'); @@ -22,7 +22,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { let adminRoleAuthc: RoleCredentials; let internalReqHeader: InternalRequestHeader; - describe('ElasticSearch query rule', () => { + describe('Query DSL', () => { const RULE_TYPE_ID = '.es-query'; const ALERT_ACTION_INDEX = 'alert-action-es-query'; const RULE_ALERT_INDEX = '.alerts-stack.alerts-default'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl_with_group_by.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl_with_group_by.ts new file mode 100644 index 0000000000000..db91ba5780dad --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/query_dsl_with_group_by.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { cleanup, generate, Dataset, PartialConfig } from '@kbn/data-forge'; +import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ActionDocument } from './types'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const logger = getService('log'); + const samlAuth = getService('samlAuth'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); + const alertingApi = getService('alertingApi'); + const config = getService('config'); + const isServerless = config.get('serverless'); + const expectedConsumer = isServerless ? 'observability' : 'logs'; + + let adminRoleAuthc: RoleCredentials; + let internalReqHeader: InternalRequestHeader; + + describe('Query DQL with group by field', () => { + const RULE_TYPE_ID = '.es-query'; + const ALERT_ACTION_INDEX = 'alert-action-es-query'; + const RULE_ALERT_INDEX = '.alerts-stack.alerts-default'; + const INDEX = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + let dataForgeConfig: PartialConfig; + let dataForgeIndices: string[]; + let actionId: string; + let ruleId: string; + + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + internalReqHeader = samlAuth.getInternalRequestHeader(); + dataForgeConfig = { + schedule: [ + { + template: 'good', + start: 'now-10m', + end: 'now+5m', + metrics: [ + { name: 'system.cpu.user.pct', method: 'linear', start: 2.5, end: 2.5 }, + { name: 'system.cpu.total.pct', method: 'linear', start: 0.5, end: 0.5 }, + { name: 'system.cpu.total.norm.pct', method: 'linear', start: 0.8, end: 0.8 }, + ], + }, + ], + indexing: { + dataset: 'fake_hosts' as Dataset, + eventsPerCycle: 1, + interval: 60000, + alignEventsToInterval: true, + }, + }; + dataForgeIndices = await generate({ client: esClient, config: dataForgeConfig, logger }); + await alertingApi.waitForDocumentInIndex({ + indexName: dataForgeIndices.join(','), + docCountTarget: 45, + }); + }); + + after(async () => { + await supertestWithoutAuth + .delete(`/api/alerting/rule/${ruleId}`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalReqHeader); + await supertestWithoutAuth + .delete(`/api/actions/connector/${actionId}`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalReqHeader); + await esClient.deleteByQuery({ + index: RULE_ALERT_INDEX, + query: { term: { 'kibana.alert.rule.uuid': ruleId } }, + conflicts: 'proceed', + }); + await esClient.deleteByQuery({ + index: '.kibana-event-log-*', + query: { term: { 'rule.id': ruleId } }, + conflicts: 'proceed', + }); + await esDeleteAllIndices([ALERT_ACTION_INDEX, ...dataForgeIndices]); + await cleanup({ client: esClient, config: dataForgeConfig, logger }); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + describe('Rule creation', () => { + it('creates rule successfully', async () => { + actionId = await alertingApi.createIndexConnector({ + roleAuthc: adminRoleAuthc, + name: 'Index Connector: Alerting API test', + indexName: ALERT_ACTION_INDEX, + }); + + const createdRule = await alertingApi.helpers.createEsQueryRule({ + roleAuthc: adminRoleAuthc, + consumer: expectedConsumer, + name: 'always fire', + ruleTypeId: RULE_TYPE_ID, + params: { + size: 100, + thresholdComparator: '>', + threshold: [1], + index: [INDEX], + timeField: '@timestamp', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + timeWindowSize: 1, + timeWindowUnit: 'm', + groupBy: 'top', + termField: 'host.name', + termSize: 1, + }, + actions: [ + { + group: 'query matched', + id: actionId, + params: { + documents: [ + { + ruleId: '{{rule.id}}', + ruleName: '{{rule.name}}', + ruleParams: '{{rule.params}}', + spaceId: '{{rule.spaceId}}', + tags: '{{rule.tags}}', + alertId: '{{alert.id}}', + alertActionGroup: '{{alert.actionGroup}}', + instanceContextValue: '{{context.instanceContextValue}}', + instanceStateValue: '{{state.instanceStateValue}}', + }, + ], + }, + frequency: { + notify_when: 'onActiveAlert', + throttle: null, + summary: false, + }, + }, + ], + }); + ruleId = createdRule.id; + expect(ruleId).not.to.be(undefined); + }); + + it('should be active', async () => { + const executionStatus = await alertingApi.waitForRuleStatus({ + roleAuthc: adminRoleAuthc, + ruleId, + expectedStatus: 'active', + }); + expect(executionStatus).to.be('active'); + }); + + it('should find the created rule with correct information about the consumer', async () => { + const match = await alertingApi.findInRules(adminRoleAuthc, ruleId); + expect(match).not.to.be(undefined); + expect(match.consumer).to.be(expectedConsumer); + }); + + it('should set correct information in the alert document', async () => { + const resp = await alertingApi.waitForAlertInIndex({ + indexName: RULE_ALERT_INDEX, + ruleId, + }); + + expect(resp.hits.hits[0]._source).property('host.name', 'host-0'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', expectedConsumer); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.reason', + 'Document count is 3 in the last 1m for host-0 in kbn-data-forge-fake_hosts.fake_hosts-* index. Alert when greater than 1.' + ); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.evaluation.conditions', + 'Number of matching documents for group "host-0" is greater than 1' + ); + expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.value', '3'); + expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold', 1); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'always fire'); + expect(resp.hits.hits[0]._source).property('event.action', 'open'); + expect(resp.hits.hits[0]._source).property('kibana.alert.action_group', 'query matched'); + expect(resp.hits.hits[0]._source).property('kibana.alert.status', 'active'); + expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open'); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.rule.category', + 'Elasticsearch query' + ); + }); + + it('should set correct action variables', async () => { + const resp = await alertingApi.waitForDocumentInIndex({ + indexName: ALERT_ACTION_INDEX, + }); + + expect(resp.hits.hits[0]._source?.ruleId).eql(ruleId); + expect(resp.hits.hits[0]._source?.ruleName).eql('always fire'); + expect(resp.hits.hits[0]._source?.ruleParams).eql( + '{"size":100,"thresholdComparator":">","threshold":[1],"index":["kbn-data-forge-fake_hosts.fake_hosts-*"],"timeField":"@timestamp","esQuery":"{\\n \\"query\\":{\\n \\"match_all\\" : {}\\n }\\n}","timeWindowSize":1,"timeWindowUnit":"m","groupBy":"top","termField":"host.name","termSize":1,"excludeHitsFromPreviousRun":true,"aggType":"count","searchType":"esQuery"}' + ); + expect(resp.hits.hits[0]._source?.alertActionGroup).eql('query matched'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/types.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/types.ts new file mode 100644 index 0000000000000..939ff17b775d7 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/es_query/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface ActionDocument { + ruleId: string; + ruleName: string; + ruleParams: string; + spaceId: string; + tags: string; + alertId: string; + alertActionGroup: string; +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts index e425aa1010a6d..d8061694e7be4 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts @@ -10,7 +10,7 @@ import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_cont export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('Observability Alerting', () => { loadTestFile(require.resolve('./burn_rate_rule')); - loadTestFile(require.resolve('./es_query_rule')); + loadTestFile(require.resolve('./es_query')); loadTestFile(require.resolve('./custom_threshold')); }); }