From fc9f19e3a235847e08155dc73cef117d547700a6 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Tue, 16 May 2023 10:11:53 +0200 Subject: [PATCH] [AO] Add metric threshold integration test (#157489) ## Summary Closes #157189 This PR adds a metric threshold integration test. This is the first step in adding more test coverage for observability rules. **Steps during the test** 1. Generating fake host data by using a similar implementation as https://github.com/elastic/high-cardinality-cluster - Data is generated for the last 15 mins - Implementation was simplified only to cover fake hosts and was converted to typescript 2. Creating an action using an index connector 3. Creating a metric threshold rule containing step number 2 action 4. Checking the status of the rule to be active 5. Checking the triggered action to have the correct parameters 6. Checking the generated alert to have the correct information 7. Clean up **How to run locally** - Run server ``` node scripts/functional_tests_server --config x-pack/test/api_integration/apis/metrics_ui/config.ts ``` - Then run the test ``` node scripts/functional_tests__runner --include-pack/test/api_integration/apis/metrics_ui/cometric_threshold_rule.ts --config x-pack/test/api_integration/apis/metrics_ui/config.ts ``` **Reference** I created https://github.com/elastic/integrations/issues/6168 to find a better way to generate data and make sure that data matches what metricbeats generates --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + package.json | 1 + tsconfig.base.json | 2 + x-pack/packages/kbn-infra-forge/README.md | 6 + x-pack/packages/kbn-infra-forge/index.ts | 8 + .../packages/kbn-infra-forge/jest.config.js | 12 ++ x-pack/packages/kbn-infra-forge/kibana.jsonc | 5 + x-pack/packages/kbn-infra-forge/package.json | 6 + .../src/data_sources/fake_hosts/index.ts | 140 ++++++++++++++ .../src/data_sources/fake_hosts/template.ts | 175 ++++++++++++++++++ .../src/lib/install_template.ts | 28 +++ .../packages/kbn-infra-forge/src/lib/queue.ts | 59 ++++++ x-pack/packages/kbn-infra-forge/src/run.ts | 72 +++++++ x-pack/packages/kbn-infra-forge/tsconfig.json | 20 ++ .../infra/common/alerting/metrics/types.ts | 21 +++ .../metrics_ui/helpers/alerting_api_helper.ts | 64 +++++++ .../helpers/alerting_wait_for_helpers.ts | 87 +++++++++ .../api_integration/apis/metrics_ui/index.js | 1 + .../apis/metrics_ui/metric_threshold_rule.ts | 174 +++++++++++++++++ x-pack/test/tsconfig.json | 1 + yarn.lock | 4 + 21 files changed, 887 insertions(+) create mode 100644 x-pack/packages/kbn-infra-forge/README.md create mode 100644 x-pack/packages/kbn-infra-forge/index.ts create mode 100644 x-pack/packages/kbn-infra-forge/jest.config.js create mode 100644 x-pack/packages/kbn-infra-forge/kibana.jsonc create mode 100644 x-pack/packages/kbn-infra-forge/package.json create mode 100644 x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/index.ts create mode 100644 x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/template.ts create mode 100644 x-pack/packages/kbn-infra-forge/src/lib/install_template.ts create mode 100644 x-pack/packages/kbn-infra-forge/src/lib/queue.ts create mode 100644 x-pack/packages/kbn-infra-forge/src/run.ts create mode 100644 x-pack/packages/kbn-infra-forge/tsconfig.json create mode 100644 x-pack/test/api_integration/apis/metrics_ui/helpers/alerting_api_helper.ts create mode 100644 x-pack/test/api_integration/apis/metrics_ui/helpers/alerting_wait_for_helpers.ts create mode 100644 x-pack/test/api_integration/apis/metrics_ui/metric_threshold_rule.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1be2087cc3501..8dd91e1213fba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -413,6 +413,7 @@ packages/kbn-import-resolver @elastic/kibana-operations x-pack/plugins/index_lifecycle_management @elastic/platform-deployment-management x-pack/plugins/index_management @elastic/platform-deployment-management test/plugin_functional/plugins/index_patterns @elastic/kibana-app-services +x-pack/packages/kbn-infra-forge @elastic/actionable-observability x-pack/plugins/infra @elastic/infra-monitoring-ui x-pack/plugins/ingest_pipelines @elastic/platform-deployment-management src/plugins/input_control_vis @elastic/kibana-presentation diff --git a/package.json b/package.json index ff193d3d9391f..cfc7f7f80e9d2 100644 --- a/package.json +++ b/package.json @@ -436,6 +436,7 @@ "@kbn/index-lifecycle-management-plugin": "link:x-pack/plugins/index_lifecycle_management", "@kbn/index-management-plugin": "link:x-pack/plugins/index_management", "@kbn/index-patterns-test-plugin": "link:test/plugin_functional/plugins/index_patterns", + "@kbn/infra-forge": "link:x-pack/packages/kbn-infra-forge", "@kbn/infra-plugin": "link:x-pack/plugins/infra", "@kbn/ingest-pipelines-plugin": "link:x-pack/plugins/ingest_pipelines", "@kbn/input-control-vis-plugin": "link:src/plugins/input_control_vis", diff --git a/tsconfig.base.json b/tsconfig.base.json index 04a43a915a586..043370f90f1ad 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -820,6 +820,8 @@ "@kbn/index-management-plugin/*": ["x-pack/plugins/index_management/*"], "@kbn/index-patterns-test-plugin": ["test/plugin_functional/plugins/index_patterns"], "@kbn/index-patterns-test-plugin/*": ["test/plugin_functional/plugins/index_patterns/*"], + "@kbn/infra-forge": ["x-pack/packages/kbn-infra-forge"], + "@kbn/infra-forge/*": ["x-pack/packages/kbn-infra-forge/*"], "@kbn/infra-plugin": ["x-pack/plugins/infra"], "@kbn/infra-plugin/*": ["x-pack/plugins/infra/*"], "@kbn/ingest-pipelines-plugin": ["x-pack/plugins/ingest_pipelines"], diff --git a/x-pack/packages/kbn-infra-forge/README.md b/x-pack/packages/kbn-infra-forge/README.md new file mode 100644 index 0000000000000..6f1a67aa51f25 --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/README.md @@ -0,0 +1,6 @@ +# @kbn/infra-forge + +`@kbn/data-forge` is a tool to generate infra data for integration testing. + +**Datasets** +- fake hosts diff --git a/x-pack/packages/kbn-infra-forge/index.ts b/x-pack/packages/kbn-infra-forge/index.ts new file mode 100644 index 0000000000000..2c1e5600c36b0 --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/index.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 { cleanup, generate } from './src/run'; diff --git a/x-pack/packages/kbn-infra-forge/jest.config.js b/x-pack/packages/kbn-infra-forge/jest.config.js new file mode 100644 index 0000000000000..d5bd842dee334 --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/jest.config.js @@ -0,0 +1,12 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../..', + roots: ['/x-pack/packages/kbn-infra-forge'], +}; diff --git a/x-pack/packages/kbn-infra-forge/kibana.jsonc b/x-pack/packages/kbn-infra-forge/kibana.jsonc new file mode 100644 index 0000000000000..a66a733662735 --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/infra-forge", + "owner": "@elastic/actionable-observability" +} diff --git a/x-pack/packages/kbn-infra-forge/package.json b/x-pack/packages/kbn-infra-forge/package.json new file mode 100644 index 0000000000000..4a50361b0d33f --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/infra-forge", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/index.ts b/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/index.ts new file mode 100644 index 0000000000000..5610a8e0f18b9 --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/index.ts @@ -0,0 +1,140 @@ +/* + * 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 lodash from 'lodash'; +import type { Moment } from 'moment'; + +export { template } from './template'; + +const createGroupIndex = (index: number) => Math.floor(index / 1000) * 1000; + +const randomBetween = (start = 0, end = 1, step = 0.1) => + lodash.sample(lodash.range(start, end, step)); + +let networkDataCount = 0; +const generateNetworkData = lodash.memoize(() => { + networkDataCount += 10000; + return networkDataCount; +}); + +export const generateEvent = (index: number, timestamp: Moment, interval: number) => { + const groupIndex = createGroupIndex(index); + return [ + { + '@timestamp': timestamp.toISOString(), + tags: [`group-${groupIndex}`, `event-${index}`], + host: { + name: `host-${index}`, + mac: ['00-00-5E-00-53-23', '00-00-5E-00-53-24'], + network: { + name: `network-${index}`, + }, + }, + event: { + module: 'system', + dataset: 'system.cpu', + }, + labels: { + groupId: `group-${groupIndex}`, + eventId: `event-${index}`, + }, + system: { + cpu: { + cores: 4, + total: { + norm: { + pct: randomBetween(), + }, + }, + user: { + pct: randomBetween(1, 4), + }, + system: { + pct: randomBetween(1, 4), + }, + }, + }, + metricset: { + period: interval, + }, + container: { + id: `container-${index}`, + name: 'container-name', + }, + }, + { + '@timestamp': timestamp.toISOString(), + host: { + name: `host-${index}`, + network: { + name: `network-${index}`, + }, + }, + event: { + module: 'system', + dataset: 'system.network', + }, + labels: { + groupId: `group-${groupIndex}`, + eventId: `event-${index}`, + }, + system: { + network: { + name: 'eth0', + in: { + bytes: generateNetworkData(), + }, + out: { + bytes: generateNetworkData(), + }, + }, + }, + metricset: { + period: interval, + }, + container: { + id: `container-${index}`, + name: 'container-name', + }, + }, + { + '@timestamp': timestamp.toISOString(), + host: { + name: `host-${index}`, + network: { + name: `network-${index}`, + }, + }, + event: { + module: 'system', + dataset: 'system.network', + }, + labels: { + groupId: `group-${groupIndex}`, + eventId: `event-${index}`, + }, + system: { + network: { + name: 'eth1', + in: { + bytes: generateNetworkData(), + }, + out: { + bytes: generateNetworkData(), + }, + }, + }, + metricset: { + period: interval, + }, + container: { + id: `container-${index}`, + name: 'container-name', + }, + }, + ]; +}; diff --git a/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/template.ts b/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/template.ts new file mode 100644 index 0000000000000..cb8e65b347bc7 --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/template.ts @@ -0,0 +1,175 @@ +/* + * 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 template = { + order: 1, + index_patterns: ['kbn-data-forge*'], + settings: { + index: { + mapping: { + total_fields: { + limit: '10000', + }, + }, + number_of_shards: '1', + number_of_replicas: '0', + query: { + default_field: ['message', 'labels.*', 'event.*'], + }, + }, + }, + mappings: { + dynamic_templates: [ + { + labels: { + path_match: 'labels.*', + mapping: { + type: 'keyword', + }, + match_mapping_type: 'string', + }, + }, + { + strings_as_keyword: { + mapping: { + ignore_above: 1024, + type: 'keyword', + }, + match_mapping_type: 'string', + }, + }, + ], + date_detection: false, + properties: { + '@timestamp': { + type: 'date', + }, + tags: { + type: 'keyword', + }, + metricset: { + properties: { + period: { + type: 'long', + }, + }, + }, + host: { + properties: { + name: { + type: 'keyword', + ignore_above: 256, + }, + network: { + properties: { + name: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, + event: { + properties: { + dataset: { + type: 'keyword', + ignore_above: 256, + }, + module: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + system: { + properties: { + cpu: { + properties: { + cores: { + type: 'long', + }, + total: { + properties: { + norm: { + properties: { + pct: { + scaling_factor: 1000, + type: 'scaled_float', + }, + }, + }, + }, + }, + user: { + properties: { + pct: { + scaling_factor: 1000, + type: 'scaled_float', + }, + norm: { + properties: { + pct: { + scaling_factor: 1000, + type: 'scaled_float', + }, + }, + }, + }, + }, + system: { + properties: { + pct: { + scaling_factor: 1000, + type: 'scaled_float', + }, + }, + }, + }, + }, + network: { + properties: { + name: { + type: 'keyword', + ignore_above: 256, + }, + in: { + properties: { + bytes: { + type: 'long', + }, + }, + }, + out: { + properties: { + bytes: { + type: 'long', + }, + }, + }, + }, + }, + }, + }, + container: { + properties: { + id: { + type: 'keyword', + ignore_above: 256, + }, + name: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, + aliases: { + 'metrics-fake_hosts': {}, + }, +}; diff --git a/x-pack/packages/kbn-infra-forge/src/lib/install_template.ts b/x-pack/packages/kbn-infra-forge/src/lib/install_template.ts new file mode 100644 index 0000000000000..02e9f1cb8442d --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/src/lib/install_template.ts @@ -0,0 +1,28 @@ +/* + * 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 type { Client } from '@elastic/elasticsearch'; +import type { ToolingLog } from '@kbn/tooling-log'; + +export function installTemplate( + client: Client, + template: object, + namespace: string, + logger: ToolingLog +) { + logger.debug(`installTemplate > template name: kbn-data-forge-${namespace}`); + return client.indices + .putTemplate({ name: `kbn-data-forge-${namespace}`, body: template }) + .catch((error: any) => logger.error(`installTemplate > ${JSON.stringify(error)}`)); +} + +export function deleteTemplate(client: Client, namespace: string, logger: ToolingLog) { + logger.debug(`deleteTemplate > template name: kbn-data-forge-${namespace}`); + return client.indices + .deleteTemplate({ name: `kbn-data-forge-${namespace}` }) + .catch((error: any) => logger.error(`deleteTemplate > ${JSON.stringify(error)}`)); +} diff --git a/x-pack/packages/kbn-infra-forge/src/lib/queue.ts b/x-pack/packages/kbn-infra-forge/src/lib/queue.ts new file mode 100644 index 0000000000000..bf6553fe01b88 --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/src/lib/queue.ts @@ -0,0 +1,59 @@ +/* + * 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 async from 'async'; +import { omit } from 'lodash'; + +import type { Client } from '@elastic/elasticsearch'; +import type { ToolingLog } from '@kbn/tooling-log'; + +export const getIndexName = (namespace: string) => `kbn-data-forge-${namespace}`; + +export const createQueue = ( + esClient: Client, + namespace: string, + payloadSize = 1000, + concurrency = 5, + logger: ToolingLog +) => { + const indexName = getIndexName(namespace); + logger.debug(`createQueue > index name: ${indexName}`); + return async.cargoQueue( + (docs: object[], callback) => { + const body: any[] = []; + docs.forEach((doc) => { + body.push({ + create: { + _index: indexName, + }, + }); + body.push(omit(doc, 'namespace')); + }); + esClient + .bulk({ body }) + .then((resp) => { + if (resp.errors) { + logger.error( + `createQueue > Failed to index document to ${indexName} index: ${JSON.stringify( + resp.errors + )}` + ); + return callback(new Error('Failed to index')); + } + return callback(); + }) + .catch((error) => { + logger.error( + `createQueue > Error while indexing document to ${indexName} index: ${error}` + ); + callback(error); + }); + }, + concurrency, + payloadSize + ); +}; diff --git a/x-pack/packages/kbn-infra-forge/src/run.ts b/x-pack/packages/kbn-infra-forge/src/run.ts new file mode 100644 index 0000000000000..470c0afbdccc6 --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/src/run.ts @@ -0,0 +1,72 @@ +/* + * 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 { range } from 'lodash'; +import moment from 'moment'; +import datemath from '@kbn/datemath'; +import type { Moment } from 'moment'; +import type { ToolingLog } from '@kbn/tooling-log'; +import type { Client } from '@elastic/elasticsearch'; +import { createQueue, getIndexName } from './lib/queue'; +import { deleteTemplate, installTemplate } from './lib/install_template'; +import * as fakeHosts from './data_sources/fake_hosts'; + +const generateEventsFns = { + fake_hosts: fakeHosts.generateEvent, +}; + +const templates = { + fake_hosts: fakeHosts.template, +}; + +const EVENTS_PER_CYCLE = 1; +const PAYLOAD_SIZE = 10000; +const CONCURRENCY = 5; +const INDEX_INTERVAL = 60000; +const DATASET = 'fake_hosts'; + +const createEvents = (size: number, timestamp: Moment) => + range(size) + .map((i) => { + const generateEvent = generateEventsFns[DATASET]; + return generateEvent(i, timestamp, INDEX_INTERVAL); + }) + .flat(); + +function indexHistory(lookback: string, queue: any, logger: ToolingLog, nextTimestamp?: Moment) { + const now = moment(); + const startTs = datemath.parse(lookback, { momentInstance: moment }); + const timestamp = nextTimestamp || moment(startTs); + createEvents(EVENTS_PER_CYCLE, timestamp).forEach((event) => queue.push(event)); + return queue.drain().then(() => { + if (timestamp.isBefore(now)) { + return indexHistory(lookback, queue, logger, timestamp.add(INDEX_INTERVAL, 'ms')); + } + return Promise.resolve(); + }); +} + +export const generate = async ({ + esClient, + lookback, + logger, +}: { + esClient: Client; + lookback: string; + logger: ToolingLog; +}) => { + const queue = createQueue(esClient, DATASET, PAYLOAD_SIZE, CONCURRENCY, logger); + const template = templates[DATASET]; + await installTemplate(esClient, template, DATASET, logger).then(() => + indexHistory(lookback, queue, logger) + ); + return getIndexName(DATASET); +}; + +export const cleanup = async ({ esClient, logger }: { esClient: Client; logger: ToolingLog }) => { + await deleteTemplate(esClient, DATASET, logger); +}; diff --git a/x-pack/packages/kbn-infra-forge/tsconfig.json b/x-pack/packages/kbn-infra-forge/tsconfig.json new file mode 100644 index 0000000000000..58254ba94f8f4 --- /dev/null +++ b/x-pack/packages/kbn-infra-forge/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/tooling-log", + "@kbn/datemath", + ] +} diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 5a9105ed2e19d..151c804bf413f 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -14,6 +14,18 @@ export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; export const METRIC_ANOMALY_ALERT_TYPE_ID = 'metrics.alert.anomaly'; +export enum InfraRuleType { + MetricThreshold = 'metrics.alert.threshold', + InventoryThreshold = 'metrics.alert.inventory.threshold', + Anomaly = 'metrics.alert.anomaly', +} + +export interface InfraRuleTypeParams { + [InfraRuleType.MetricThreshold]: MetricThresholdParams; + [InfraRuleType.InventoryThreshold]: InventoryMetricConditions; + [InfraRuleType.Anomaly]: MetricAnomalyParams; +} + export enum Comparator { GT = '>', LT = '<', @@ -88,6 +100,15 @@ export interface InventoryMetricThresholdParams { alertOnNoData?: boolean; } +export interface MetricThresholdParams { + criteria: MetricExpressionParams[]; + filterQuery?: string; + filterQueryText?: string; + sourceId?: string; + alertOnNoData?: boolean; + alertOnGroupDisappear?: boolean; +} + interface BaseMetricExpressionParams { timeSize: number; timeUnit: TimeUnitChar; diff --git a/x-pack/test/api_integration/apis/metrics_ui/helpers/alerting_api_helper.ts b/x-pack/test/api_integration/apis/metrics_ui/helpers/alerting_api_helper.ts new file mode 100644 index 0000000000000..d89908b9bef72 --- /dev/null +++ b/x-pack/test/api_integration/apis/metrics_ui/helpers/alerting_api_helper.ts @@ -0,0 +1,64 @@ +/* + * 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 { InfraRuleType, InfraRuleTypeParams } from '@kbn/infra-plugin/common/alerting/metrics'; +import type { SuperTest, Test } from 'supertest'; + +export async function createIndexConnector({ + supertest, + name, + indexName, +}: { + supertest: SuperTest; + name: string; + indexName: string; +}) { + const { body } = await supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name, + config: { + index: indexName, + refresh: true, + }, + connector_type_id: '.index', + }); + return body.id as string; +} + +export async function createMetricThresholdRule({ + supertest, + name, + ruleTypeId, + params, + actions = [], + schedule, +}: { + supertest: SuperTest; + ruleTypeId: T; + name: string; + params: InfraRuleTypeParams[T]; + actions?: any[]; + schedule?: { interval: string }; +}) { + const { body } = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send({ + params, + consumer: 'infrastructure', + schedule: schedule || { + interval: '5m', + }, + tags: ['infrastructure'], + name, + rule_type_id: ruleTypeId, + actions, + }); + return body; +} diff --git a/x-pack/test/api_integration/apis/metrics_ui/helpers/alerting_wait_for_helpers.ts b/x-pack/test/api_integration/apis/metrics_ui/helpers/alerting_wait_for_helpers.ts new file mode 100644 index 0000000000000..80432a87185e6 --- /dev/null +++ b/x-pack/test/api_integration/apis/metrics_ui/helpers/alerting_wait_for_helpers.ts @@ -0,0 +1,87 @@ +/* + * 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 pRetry from 'p-retry'; + +import type SuperTest from 'supertest'; +import type { Client } from '@elastic/elasticsearch'; +import type { + AggregationsAggregate, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +export async function waitForRuleStatus({ + id, + expectedStatus, + supertest, +}: { + id: string; + expectedStatus: string; + supertest: SuperTest.SuperTest; +}): Promise> { + return pRetry( + async () => { + const response = await supertest.get(`/api/alerting/rule/${id}`); + const { execution_status: executionStatus } = response.body || {}; + const { status } = executionStatus || {}; + if (status !== expectedStatus) { + throw new Error(`waitForStatus(${expectedStatus}): got ${status}`); + } + return executionStatus; + }, + { retries: 10 } + ); +} + +export async function waitForDocumentInIndex({ + esClient, + indexName, +}: { + esClient: Client; + indexName: string; +}): Promise>> { + return pRetry( + async () => { + const response = await esClient.search({ index: indexName }); + if (response.hits.hits.length === 0) { + throw new Error('No hits found'); + } + return response; + }, + { retries: 10 } + ); +} + +export async function waitForAlertInIndex({ + esClient, + indexName, + ruleId, +}: { + esClient: Client; + indexName: string; + ruleId: string; +}): Promise>> { + return pRetry( + async () => { + const response = await esClient.search({ + index: indexName, + body: { + query: { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + }, + }); + if (response.hits.hits.length === 0) { + throw new Error('No hits found'); + } + return response; + }, + { retries: 10 } + ); +} diff --git a/x-pack/test/api_integration/apis/metrics_ui/index.js b/x-pack/test/api_integration/apis/metrics_ui/index.js index 8a4b7d3398d1c..523813e9efe69 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/index.js +++ b/x-pack/test/api_integration/apis/metrics_ui/index.js @@ -18,6 +18,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./ip_to_hostname')); loadTestFile(require.resolve('./http_source')); loadTestFile(require.resolve('./metric_threshold_alert')); + loadTestFile(require.resolve('./metric_threshold_rule')); loadTestFile(require.resolve('./metrics_overview_top')); loadTestFile(require.resolve('./metrics_process_list')); loadTestFile(require.resolve('./metrics_process_list_chart')); diff --git a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_rule.ts b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_rule.ts new file mode 100644 index 0000000000000..18776ac959dd8 --- /dev/null +++ b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_rule.ts @@ -0,0 +1,174 @@ +/* + * 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 } from '@kbn/infra-forge'; +import { Aggregators, Comparator, InfraRuleType } from '@kbn/infra-plugin/common/alerting/metrics'; +import { + waitForDocumentInIndex, + waitForAlertInIndex, + waitForRuleStatus, +} from './helpers/alerting_wait_for_helpers'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { createIndexConnector, createMetricThresholdRule } from './helpers/alerting_api_helper'; + +export default function ({ getService }: FtrProviderContext) { + const esClient = getService('es'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); + const supertest = getService('supertest'); + const logger = getService('log'); + + describe('Metric threshold rule >', () => { + let ruleId: string; + let actionId: string | undefined; + let infraDataIndex: string; + + const METRICS_ALERTS_INDEX = '.alerts-observability.metrics.alerts-default'; + const ALERT_ACTION_INDEX = 'alert-action-metric-threshold'; + + describe('alert and action creation', () => { + before(async () => { + infraDataIndex = await generate({ esClient, lookback: 'now-15m', logger }); + actionId = await createIndexConnector({ + supertest, + name: 'Index Connector: Metric threshold API test', + indexName: ALERT_ACTION_INDEX, + }); + const createdRule = await createMetricThresholdRule({ + supertest, + ruleTypeId: InfraRuleType.MetricThreshold, + name: 'Metric threshold rule', + params: { + criteria: [ + { + aggType: Aggregators.AVERAGE, + comparator: Comparator.GT, + threshold: [0.5], + timeSize: 5, + timeUnit: 'm', + metric: 'system.cpu.user.pct', + }, + ], + sourceId: 'default', + alertOnNoData: true, + alertOnGroupDisappear: true, + }, + actions: [ + { + group: 'metrics.threshold.fired', + id: actionId, + params: { + documents: [ + { + ruleType: '{{rule.type}}', + }, + ], + }, + frequency: { + notify_when: 'onActionGroupChange', + throttle: null, + summary: false, + }, + }, + ], + schedule: { + interval: '1m', + }, + }); + ruleId = createdRule.id; + }); + + after(async () => { + await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo'); + await supertest.delete(`/api/actions/connector/${actionId}`).set('kbn-xsrf', 'foo'); + await esDeleteAllIndices([ALERT_ACTION_INDEX, infraDataIndex]); + await esClient.deleteByQuery({ + index: METRICS_ALERTS_INDEX, + query: { term: { 'kibana.alert.rule.uuid': ruleId } }, + }); + await esClient.deleteByQuery({ + index: '.kibana-event-log-*', + query: { term: { 'kibana.alert.rule.consumer': 'infrastructure' } }, + }); + await cleanup({ esClient, logger }); + }); + + it('rule should be active', async () => { + const executionStatus = await waitForRuleStatus({ + id: ruleId, + expectedStatus: 'active', + supertest, + }); + expect(executionStatus.status).to.be('active'); + }); + + it('should set correct action parameter: ruleType', async () => { + const resp = await waitForDocumentInIndex<{ ruleType: string }>({ + esClient, + indexName: ALERT_ACTION_INDEX, + }); + + expect(resp.hits.hits[0]._source?.ruleType).eql('metrics.alert.threshold'); + }); + + it('should set correct information in the alert document', async () => { + const resp = await waitForAlertInIndex({ + esClient, + indexName: METRICS_ALERTS_INDEX, + ruleId, + }); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.rule.category', + 'Metric threshold' + ); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'infrastructure'); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.rule.name', + 'Metric threshold rule' + ); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'infrastructure'); + 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', + 'metrics.alert.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('infrastructure'); + expect(resp.hits.hits[0]._source).property( + 'kibana.alert.action_group', + 'metrics.threshold.fired' + ); + expect(resp.hits.hits[0]._source).property('tags').contain('infrastructure'); + 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.rule.parameters') + .eql({ + criteria: [ + { + aggType: 'avg', + comparator: '>', + threshold: [0.5], + timeSize: 5, + timeUnit: 'm', + metric: 'system.cpu.user.pct', + }, + ], + sourceId: 'default', + alertOnNoData: true, + alertOnGroupDisappear: true, + }); + }); + }); + }); +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 91a8e991fcdda..2114f8ea12890 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -125,5 +125,6 @@ "@kbn/guided-onboarding-plugin", "@kbn/field-formats-plugin", "@kbn/ml-anomaly-utils", + "@kbn/infra-forge", ] } diff --git a/yarn.lock b/yarn.lock index f6e00388c7b16..c2e4a893a64d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4378,6 +4378,10 @@ version "0.0.0" uid "" +"@kbn/infra-forge@link:x-pack/packages/kbn-infra-forge": + version "0.0.0" + uid "" + "@kbn/infra-plugin@link:x-pack/plugins/infra": version "0.0.0" uid ""