From 6aa6b280aa10c772e07641f92065f9d0e548cb3b Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Fri, 17 May 2024 14:47:57 +0200 Subject: [PATCH] Add avg_pct_fired test --- .buildkite/ftr_configs.yml | 4 +- .../config/ess/config.base.ts | 8 + .../package.json | 6 +- .../custom_threshold/avg_pct_fired.ts | 258 ++++++++++++++++++ .../custom_threshold/configs/ess.config.ts | 16 ++ .../configs/serverless.config.ts | 15 + .../alerting/custom_threshold/constants.ts | 8 + .../alerting/custom_threshold/index.ts | 14 + .../alerting/custom_threshold/typings.ts | 25 ++ 9 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/avg_pct_fired.ts create mode 100644 x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/ess.config.ts create mode 100644 x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/serverless.config.ts create mode 100644 x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/constants.ts create mode 100644 x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/index.ts create mode 100644 x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/typings.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 38ad51f8ea64c..21c3c79735526 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -567,4 +567,6 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/sources/indices/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/sources/indices/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/observability_solution_api_integration/test_suites/alerting/burn_rate/configs/serverless.config.ts - - x-pack/test/observability_solution_api_integration/test_suites/alerting/burn_rate/configs/ess.config.ts \ No newline at end of file + - x-pack/test/observability_solution_api_integration/test_suites/alerting/burn_rate/configs/ess.config.ts + - x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/serverless.config.ts + - x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/ess.config.ts diff --git a/x-pack/test/observability_solution_api_integration/config/ess/config.base.ts b/x-pack/test/observability_solution_api_integration/config/ess/config.base.ts index 13a6b28c271c8..61b8112bf1d76 100644 --- a/x-pack/test/observability_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/observability_solution_api_integration/config/ess/config.base.ts @@ -10,6 +10,7 @@ import { services } from '../../../api_integration/services'; export interface CreateTestConfigOptions { testFiles: string[]; junit: { reportName: string }; + publicBaseUrl?: boolean; } export function createTestConfig(options: CreateTestConfigOptions) { @@ -37,6 +38,13 @@ export function createTestConfig(options: CreateTestConfigOptions) { mochaOpts: { grep: '/^(?!.*@skipInEss).*@ess.*/', }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + ...(options.publicBaseUrl ? ['--server.publicBaseUrl=http://localhost:5620'] : []), + ], + }, }; }; } diff --git a/x-pack/test/observability_solution_api_integration/package.json b/x-pack/test/observability_solution_api_integration/package.json index 5c9f9326a5656..c67dafb3da6c4 100644 --- a/x-pack/test/observability_solution_api_integration/package.json +++ b/x-pack/test/observability_solution_api_integration/package.json @@ -8,6 +8,10 @@ "alerting_burn_rate:server:serverless": "node ../../../scripts/functional_tests_server.js --config ./test_suites/alerting/burn_rate/configs/serverless.config.ts", "alerting_burn_rate:runner:serverless": "node ../../../scripts/functional_test_runner --config=test_suites/alerting/burn_rate/configs/serverless.config.ts --grep @serverless --grep @skipInServerless --invert", "alerting_burn_rate:server:ess": "node ../../../scripts/functional_tests_server.js --config ./test_suites/alerting/burn_rate/configs/ess.config.ts", - "alerting_burn_rate:runner:ess": "node ../../../scripts/functional_test_runner --config=test_suites/alerting/burn_rate/configs/ess.config.ts --grep @ess --grep @skipInEss --invert" + "alerting_burn_rate:runner:ess": "node ../../../scripts/functional_test_runner --config=test_suites/alerting/burn_rate/configs/ess.config.ts --grep @ess --grep @skipInEss --invert", + "alerting_custom_threshold:server:serverless": "node ../../../scripts/functional_tests_server.js --config ./test_suites/alerting/custom_threshold/configs/serverless.config.ts", + "alerting_custom_threshold:runner:serverless": "node ../../../scripts/functional_test_runner --config=test_suites/alerting/custom_threshold/configs/serverless.config.ts --grep @serverless --grep @skipInServerless --invert", + "alerting_custom_threshold:server:ess": "node ../../../scripts/functional_tests_server.js --config ./test_suites/alerting/custom_threshold/configs/ess.config.ts", + "alerting_custom_threshold:runner:ess": "node ../../../scripts/functional_test_runner --config=test_suites/alerting/custom_threshold/configs/ess.config.ts --grep @ess --grep @skipInEss --invert" } } \ No newline at end of file diff --git a/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/avg_pct_fired.ts b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/avg_pct_fired.ts new file mode 100644 index 0000000000000..d519f166a050d --- /dev/null +++ b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/avg_pct_fired.ts @@ -0,0 +1,258 @@ +/* + * 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 { cleanup, generate, Dataset, PartialConfig } from '@kbn/data-forge'; +import { + Aggregators, + Comparator, +} from '@kbn/observability-plugin/common/custom_threshold_rule/types'; +import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/custom_threshold/constants'; +import expect from '@kbn/expect'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { parseSearchParams } from '@kbn/share-plugin/common/url_service'; +import { omit } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { ISO_DATE_REGEX } from './constants'; +import { ActionDocument, LogsExplorerLocatorParsedParams } from './typings'; + +export default function ({ getService }: FtrProviderContext) { + const esClient = getService('es'); + const supertest = getService('supertest'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); + const alertingApi = getService('alertingApi'); + const dataViewApi = getService('dataViewApi'); + const logger = getService('log'); + const config = getService('config'); + const isServerless = config.get('serverless'); + const expectedConsumer = isServerless ? 'observability' : 'logs'; + + describe('@ess @serverless Custom Threshold rule - AVG - PCT - FIRED', () => { + const CUSTOM_THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; + const ALERT_ACTION_INDEX = 'alert-action-threshold'; + const DATA_VIEW_TITLE = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_NAME = 'data-view-name'; + const DATA_VIEW_ID = 'data-view-id'; + let dataForgeConfig: PartialConfig; + let dataForgeIndices: string[]; + let actionId: string; + let ruleId: string; + let alertId: string; + + before(async () => { + dataForgeConfig = { + schedule: [ + { + template: 'good', + start: 'now-15m', + 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 }, + ], + }, + ], + indexing: { + dataset: 'fake_hosts' as Dataset, + eventsPerCycle: 1, + interval: 10000, + alignEventsToInterval: true, + }, + }; + dataForgeIndices = await generate({ client: esClient, config: dataForgeConfig, logger }); + await alertingApi.waitForDocumentInIndex({ indexName: DATA_VIEW_TITLE, docCountTarget: 360 }); + await dataViewApi.create({ + name: DATA_VIEW_NAME, + id: DATA_VIEW_ID, + title: DATA_VIEW_TITLE, + }); + }); + + after(async () => { + await supertest + .delete(`/api/alerting/rule/${ruleId}`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo'); + await supertest + .delete(`/api/actions/connector/${actionId}`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo'); + await esClient.deleteByQuery({ + index: CUSTOM_THRESHOLD_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 dataViewApi.delete({ + id: DATA_VIEW_ID, + }); + await esDeleteAllIndices([ALERT_ACTION_INDEX, ...dataForgeIndices]); + await cleanup({ client: esClient, config: dataForgeConfig, logger }); + }); + + describe('Rule creation', () => { + it('creates rule successfully', async () => { + actionId = await alertingApi.createIndexConnector({ + name: 'Index Connector: Threshold API test', + indexName: ALERT_ACTION_INDEX, + }); + + const createdRule = await alertingApi.createRule({ + tags: ['observability'], + consumer: expectedConsumer, + name: 'Threshold rule', + ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + params: { + criteria: [ + { + comparator: Comparator.GT, + threshold: [0.5], + timeSize: 5, + timeUnit: 'm', + metrics: [ + { name: 'A', field: 'system.cpu.user.pct', aggType: Aggregators.AVERAGE }, + ], + }, + ], + alertOnNoData: true, + alertOnGroupDisappear: true, + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: DATA_VIEW_ID, + }, + }, + actions: [ + { + group: FIRED_ACTIONS_ID, + id: actionId, + params: { + documents: [ + { + ruleType: '{{rule.type}}', + alertDetailsUrl: '{{context.alertDetailsUrl}}', + reason: '{{context.reason}}', + value: '{{context.value}}', + host: '{{context.host}}', + viewInAppUrl: '{{context.viewInAppUrl}}', + }, + ], + }, + frequency: { + notify_when: 'onActionGroupChange', + throttle: null, + summary: false, + }, + }, + ], + }); + ruleId = createdRule.id; + expect(ruleId).not.to.be(undefined); + }); + + it('should be active', async () => { + const executionStatus = await alertingApi.waitForRuleStatus({ + 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.findRule(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: CUSTOM_THRESHOLD_RULE_ALERT_INDEX, + ruleId, + }); + alertId = (resp.hits.hits[0]._source as any)['kibana.alert.uuid']; + + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.rule.category', + 'Custom threshold' + ); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', expectedConsumer); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.rule.rule_type_id', + 'observability.rules.custom_threshold' + ); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.uuid', ruleId); + expect(resp.hits.hits[0]._source).property('kibana.space_ids').contain('default'); + expect(resp.hits.hits[0]._source) + .property('kibana.alert.rule.tags') + .contain('observability'); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.action_group', + 'custom_threshold.fired' + ); + expect(resp.hits.hits[0]._source).property('tags').contain('observability'); + expect(resp.hits.hits[0]._source).property('kibana.alert.instance.id', '*'); + expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open'); + expect(resp.hits.hits[0]._source).property('event.kind', 'signal'); + expect(resp.hits.hits[0]._source).property('event.action', 'open'); + expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.5]); + expect(resp.hits.hits[0]._source) + .property('kibana.alert.rule.parameters') + .eql({ + criteria: [ + { + comparator: '>', + threshold: [0.5], + timeSize: 5, + timeUnit: 'm', + metrics: [{ name: 'A', field: 'system.cpu.user.pct', aggType: 'avg' }], + }, + ], + alertOnNoData: true, + alertOnGroupDisappear: true, + searchConfiguration: { index: 'data-view-id', query: { query: '', language: 'kuery' } }, + }); + }); + + it('should set correct action variables', async () => { + const resp = await alertingApi.waitForDocumentInIndex({ + indexName: ALERT_ACTION_INDEX, + docCountTarget: 1, + }); + + expect(resp.hits.hits[0]._source?.ruleType).eql('observability.rules.custom_threshold'); + expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql( + `http://localhost:5620/app/observability/alerts/${alertId}` + ); + expect(resp.hits.hits[0]._source?.reason).eql( + `Average system.cpu.user.pct is 250%, above the threshold of 50%. (duration: 5 mins, data view: ${DATA_VIEW_NAME})` + ); + expect(resp.hits.hits[0]._source?.value).eql('250%'); + + const parsedViewInAppUrl = parseSearchParams( + new URL(resp.hits.hits[0]._source?.viewInAppUrl || '').search + ); + + expect(resp.hits.hits[0]._source?.viewInAppUrl).contain('LOGS_EXPLORER_LOCATOR'); + expect(omit(parsedViewInAppUrl.params, 'timeRange.from')).eql({ + dataset: DATA_VIEW_ID, + timeRange: { to: 'now' }, + query: { query: '', language: 'kuery' }, + filters: [], + }); + expect(parsedViewInAppUrl.params.timeRange.from).match(ISO_DATE_REGEX); + }); + }); + }); +} diff --git a/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/ess.config.ts b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/ess.config.ts new file mode 100644 index 0000000000000..5847aa180dd22 --- /dev/null +++ b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/ess.config.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. + */ + +import { createTestConfig } from '../../../../config/ess/config.base'; + +export default createTestConfig({ + testFiles: [require.resolve('..')], + junit: { + reportName: 'Custom threshold - Integration Tests - ESS Env', + }, + publicBaseUrl: true, +}); diff --git a/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/serverless.config.ts b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/serverless.config.ts new file mode 100644 index 0000000000000..407df673fbb7e --- /dev/null +++ b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/configs/serverless.config.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 { createTestConfig } from '../../../../config/serverless/config.base'; + +export default createTestConfig({ + testFiles: [require.resolve('..')], + junit: { + reportName: 'Custom threshold - Integration Tests - Serverless Env', + }, +}); diff --git a/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/constants.ts b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/constants.ts new file mode 100644 index 0000000000000..5cf1e0b4d6614 --- /dev/null +++ b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/constants.ts @@ -0,0 +1,8 @@ +/* + * 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 const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; diff --git a/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/index.ts b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/index.ts new file mode 100644 index 0000000000000..4d8e470a4af9a --- /dev/null +++ b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Custom threshold rule', function () { + loadTestFile(require.resolve('./avg_pct_fired')); + }); +} diff --git a/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/typings.ts b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/typings.ts new file mode 100644 index 0000000000000..9002e9991292f --- /dev/null +++ b/x-pack/test/observability_solution_api_integration/test_suites/alerting/custom_threshold/typings.ts @@ -0,0 +1,25 @@ +/* + * 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 { Query, TimeRange } from '@kbn/es-query'; +import { SerializableRecord } from '@kbn/utility-types'; + +export interface ActionDocument { + ruleType: string; + alertDetailsUrl: string; + reason: string; + value: string; + viewInAppUrl: string; + host?: string; + group?: string; +} + +export interface LogsExplorerLocatorParsedParams extends SerializableRecord { + dataset: string; + timeRange: TimeRange; + query: Query; +}