From 9ad201efb1d6e4b855799380dcbb427c2f629214 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 4 Nov 2024 09:42:11 +0100 Subject: [PATCH 1/9] Add foundation to run apm tests Migrate agent_explorer test Update latest_agent_versions test --- .../apm/agent_explorer/agent_explorer.spec.ts | 2 +- .../serverless/oblt.serverless.config.ts | 1 + .../configs/stateful/oblt.stateful.config.ts | 1 + .../default_configs/serverless.config.base.ts | 2 + .../default_configs/stateful.config.base.ts | 2 + .../deployment_agnostic/services/registry.ts | 97 +++++++++++++++++++ 6 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/api_integration/deployment_agnostic/services/registry.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/agent_explorer.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/agent_explorer.spec.ts index 95d438ba87cad..28471d2055926 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/agent_explorer.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/agent_explorer.spec.ts @@ -27,7 +27,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon APIClientRequestParamsOf<'GET /internal/apm/get_agents_per_service'>['params'] > ) { - return await apmApiClient.readUser({ + return apmApiClient.readUser({ endpoint: 'GET /internal/apm/get_agents_per_service', params: { query: { diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts index 245663416243f..e1864f0444ab0 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts @@ -10,6 +10,7 @@ import { createServerlessTestConfig } from '../../default_configs/serverless.con export default createServerlessTestConfig({ serverlessProject: 'oblt', testFiles: [require.resolve('./oblt.index.ts')], + servicesRequiredForTestAnalysis: ['registry'], junit: { reportName: 'Serverless Observability - Deployment-agnostic API Integration Tests', }, diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts index 7b3cf3a7f1818..2c2e33a126b54 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts @@ -9,6 +9,7 @@ import { createStatefulTestConfig } from '../../default_configs/stateful.config. export default createStatefulTestConfig({ testFiles: [require.resolve('./oblt.index.ts')], + servicesRequiredForTestAnalysis: ['registry'], junit: { reportName: 'Stateful Observability - Deployment-agnostic API Integration Tests', }, diff --git a/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts b/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts index e7df37f5aa312..353021aa343a3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts +++ b/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts @@ -16,6 +16,7 @@ interface CreateTestConfigOptions { esServerArgs?: string[]; kbnServerArgs?: string[]; services?: T; + servicesRequiredForTestAnalysis?: string[]; testFiles: string[]; junit: { reportName: string }; suiteTags?: { include?: string[]; exclude?: string[] }; @@ -85,6 +86,7 @@ export function createServerlessTestConfig { kbnServerArgs?: string[]; services?: T; testFiles: string[]; + servicesRequiredForTestAnalysis?: string[]; junit: { reportName: string }; suiteTags?: { include?: string[]; exclude?: string[] }; } @@ -100,6 +101,7 @@ export function createStatefulTestConfig void; + }>; + }> = []; + + let running: boolean = false; + + function when(title: string, callback: () => void, skip?: boolean) { + if (running) { + throw new Error("Can't add tests when running"); + } + + const frame = maybe(callsites()[1]); + + const file = frame?.getFileName(); + + if (!file) { + throw new Error('Could not infer file for suite'); + } + + callbacks.push({ + runs: [ + { + cb: () => { + const suite: ReturnType = (skip ? describe.skip : describe)( + title, + () => { + callback(); + } + ) as any; + + suite.file = file; + suite.eachTest((test) => { + test.file = file; + }); + }, + }, + ], + }); + } + + when.skip = (title: string, callback: () => void) => { + when(title, callback, true); + }; + + const registry = { + when, + run: () => { + running = true; + + const groups = joinByKey(callbacks, [], (a, b) => ({ + ...a, + ...b, + runs: a.runs.concat(b.runs), + })); + + callbacks.length = 0; + + const byConfig = groupBy(groups, 'config'); + + Object.keys(byConfig).forEach((config) => { + const groupsForConfig = byConfig[config]; + // register suites for other configs, but skip them so tests are marked as such + // and their snapshots are not marked as obsolete + describe(config, () => { + groupsForConfig.forEach((group) => { + const { runs } = group; + + describe(config, () => { + runs.forEach((run) => { + run.cb(); + }); + }); + }); + }); + }); + + running = false; + }, + }; + + return registry; +} From 78c8243de051e4e9c6ff91a46a13b2c040a0c672 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 4 Nov 2024 14:27:21 +0100 Subject: [PATCH 2/9] Force Git to recognize latest_agent_versions.spec.ts as moved --- .../apm/agent_explorer/latest_agent_versions.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/latest_agent_versions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/latest_agent_versions.spec.ts index 7d40993885255..ced2e8939356c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/latest_agent_versions.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/latest_agent_versions.spec.ts @@ -14,7 +14,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon const unlistedAgentName = 'unlistedAgent'; async function callApi() { - return await apmApiClient.readUser({ + return apmApiClient.readUser({ endpoint: 'GET /internal/apm/get_latest_agent_versions', }); } From 2f348eb8d372f77f5fd374b7f24d810766603fdf Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:47:20 +0000 Subject: [PATCH 3/9] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/test/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 9db41aecbb612..2ba14ceb1218c 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -187,6 +187,6 @@ "@kbn/alerting-types", "@kbn/ai-assistant-common", "@kbn/core-deprecations-common", - "@kbn/usage-collection-plugin", + "@kbn/usage-collection-plugin" ] } From e1bb1266594311aaaa4bc0d3abb316ddec256fc4 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 5 Nov 2024 19:36:35 +0100 Subject: [PATCH 4/9] Remove registry service --- .../serverless/oblt.serverless.config.ts | 1 - .../configs/stateful/oblt.stateful.config.ts | 1 - .../default_configs/serverless.config.base.ts | 2 - .../default_configs/stateful.config.base.ts | 2 - .../deployment_agnostic/services/registry.ts | 97 ------------------- 5 files changed, 103 deletions(-) delete mode 100644 x-pack/test/api_integration/deployment_agnostic/services/registry.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts index e1864f0444ab0..245663416243f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts @@ -10,7 +10,6 @@ import { createServerlessTestConfig } from '../../default_configs/serverless.con export default createServerlessTestConfig({ serverlessProject: 'oblt', testFiles: [require.resolve('./oblt.index.ts')], - servicesRequiredForTestAnalysis: ['registry'], junit: { reportName: 'Serverless Observability - Deployment-agnostic API Integration Tests', }, diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts index 2c2e33a126b54..7b3cf3a7f1818 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts @@ -9,7 +9,6 @@ import { createStatefulTestConfig } from '../../default_configs/stateful.config. export default createStatefulTestConfig({ testFiles: [require.resolve('./oblt.index.ts')], - servicesRequiredForTestAnalysis: ['registry'], junit: { reportName: 'Stateful Observability - Deployment-agnostic API Integration Tests', }, diff --git a/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts b/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts index 353021aa343a3..e7df37f5aa312 100644 --- a/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts +++ b/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts @@ -16,7 +16,6 @@ interface CreateTestConfigOptions { esServerArgs?: string[]; kbnServerArgs?: string[]; services?: T; - servicesRequiredForTestAnalysis?: string[]; testFiles: string[]; junit: { reportName: string }; suiteTags?: { include?: string[]; exclude?: string[] }; @@ -86,7 +85,6 @@ export function createServerlessTestConfig { kbnServerArgs?: string[]; services?: T; testFiles: string[]; - servicesRequiredForTestAnalysis?: string[]; junit: { reportName: string }; suiteTags?: { include?: string[]; exclude?: string[] }; } @@ -101,7 +100,6 @@ export function createStatefulTestConfig void; - }>; - }> = []; - - let running: boolean = false; - - function when(title: string, callback: () => void, skip?: boolean) { - if (running) { - throw new Error("Can't add tests when running"); - } - - const frame = maybe(callsites()[1]); - - const file = frame?.getFileName(); - - if (!file) { - throw new Error('Could not infer file for suite'); - } - - callbacks.push({ - runs: [ - { - cb: () => { - const suite: ReturnType = (skip ? describe.skip : describe)( - title, - () => { - callback(); - } - ) as any; - - suite.file = file; - suite.eachTest((test) => { - test.file = file; - }); - }, - }, - ], - }); - } - - when.skip = (title: string, callback: () => void) => { - when(title, callback, true); - }; - - const registry = { - when, - run: () => { - running = true; - - const groups = joinByKey(callbacks, [], (a, b) => ({ - ...a, - ...b, - runs: a.runs.concat(b.runs), - })); - - callbacks.length = 0; - - const byConfig = groupBy(groups, 'config'); - - Object.keys(byConfig).forEach((config) => { - const groupsForConfig = byConfig[config]; - // register suites for other configs, but skip them so tests are marked as such - // and their snapshots are not marked as obsolete - describe(config, () => { - groupsForConfig.forEach((group) => { - const { runs } = group; - - describe(config, () => { - runs.forEach((run) => { - run.cb(); - }); - }); - }); - }); - }); - - running = false; - }, - }; - - return registry; -} From 670ee8299e99f394a4d79efb8fbe325cb6d36cb0 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 6 Nov 2024 10:52:02 +0100 Subject: [PATCH 5/9] Migrate APM alerts test --- .../get_apm_service_summary/index.ts | 5 +- .../get_service_group_alerts.ts | 9 +- .../get_service_transaction_groups_alerts.ts | 11 +- .../get_services/get_service_alerts.ts | 11 +- .../apm}/alerts/error_count_threshold.spec.ts | 46 +- .../apm}/alerts/generate_data.ts | 0 .../apm/alerts/helpers/alerting_api_helper.ts | 256 ++++++++ .../helpers/cleanup_rule_and_alert_state.ts | 39 ++ .../helpers/wait_for_active_apm_alerts.ts | 0 .../alerts/helpers/wait_for_active_rule.ts | 44 ++ .../helpers/wait_for_alerts_for_rule.ts | 52 ++ .../wait_for_index_connector_results.ts | 0 .../apis/observability/apm/alerts/index.ts | 19 + .../alerts/preview_chart_error_count.spec.ts | 478 +++++++------- .../alerts/preview_chart_error_rate.spec.ts | 607 ++++++++++++++++++ ...preview_chart_transaction_duration.spec.ts | 564 ++++++++++++++++ .../apm}/alerts/transaction_duration.spec.ts | 37 +- .../alerts/transaction_error_rate.spec.ts | 37 +- .../apis/observability/apm/index.ts | 1 + .../deployment_agnostic/services/apm_api.ts | 2 + .../tests/alerts/anomaly_alert.spec.ts | 8 +- .../alerts/helpers/alerting_api_helper.ts | 144 +---- .../helpers/cleanup_rule_and_alert_state.ts | 7 +- .../alerts/preview_chart_error_rate.spec.ts | 599 ----------------- ...preview_chart_transaction_duration.spec.ts | 556 ---------------- .../service_group_count.spec.ts | 2 +- 26 files changed, 1947 insertions(+), 1587 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/error_count_threshold.spec.ts (86%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/generate_data.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/helpers/wait_for_active_apm_alerts.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/helpers/wait_for_index_connector_results.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/preview_chart_error_count.spec.ts (50%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_transaction_duration.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/transaction_duration.spec.ts (87%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/transaction_error_rate.spec.ts (88%) delete mode 100644 x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts index d28152127648b..5c9d40cc22772 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts @@ -6,13 +6,14 @@ */ import datemath from '@elastic/datemath'; import { ElasticsearchClient, Logger } from '@kbn/core/server'; -import { rangeQuery, ScopedAnnotationsClient } from '@kbn/observability-plugin/server'; +import { rangeQuery, ScopedAnnotationsClient, termsQuery } from '@kbn/observability-plugin/server'; import { ALERT_RULE_PRODUCER, ALERT_STATUS, ALERT_STATUS_ACTIVE, } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; import * as t from 'io-ts'; +import { observabilityFeatureId } from '@kbn/observability-shared-plugin/common'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { Environment } from '../../../../common/environment_rt'; import { SERVICE_NAME } from '../../../../common/es_fields/apm'; @@ -139,7 +140,7 @@ export async function getApmServiceSummary({ query: { bool: { filter: [ - ...termQuery(ALERT_RULE_PRODUCER, 'apm'), + ...termsQuery(ALERT_RULE_PRODUCER, 'apm', observabilityFeatureId), ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...rangeQuery(start, end), ...termQuery(SERVICE_NAME, serviceName), diff --git a/x-pack/plugins/observability_solution/apm/server/routes/service_groups/get_service_group_alerts.ts b/x-pack/plugins/observability_solution/apm/server/routes/service_groups/get_service_group_alerts.ts index 8516527d82b9a..d934863f37e9c 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/service_groups/get_service_group_alerts.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/service_groups/get_service_group_alerts.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { kqlQuery } from '@kbn/observability-plugin/server'; -import { ALERT_RULE_PRODUCER, ALERT_STATUS } from '@kbn/rule-data-utils'; +import { kqlQuery, termQuery, termsQuery } from '@kbn/observability-plugin/server'; +import { ALERT_RULE_PRODUCER, ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { Logger } from '@kbn/core/server'; +import { observabilityFeatureId } from '@kbn/observability-shared-plugin/common'; import { ApmPluginRequestHandlerContext } from '../typings'; import { SavedServiceGroup } from '../../../common/service_groups'; import { ApmAlertsClient } from '../../lib/helpers/get_apm_alerts_client'; @@ -42,8 +43,8 @@ export async function getServiceGroupAlerts({ query: { bool: { filter: [ - { term: { [ALERT_RULE_PRODUCER]: 'apm' } }, - { term: { [ALERT_STATUS]: 'active' } }, + ...termsQuery(ALERT_RULE_PRODUCER, 'apm', observabilityFeatureId), + ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ], }, }, diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_transaction_groups_alerts.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_transaction_groups_alerts.ts index 5bdb47cbb1f50..de9c73a30a186 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_transaction_groups_alerts.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_transaction_groups_alerts.ts @@ -5,13 +5,20 @@ * 2.0. */ -import { kqlQuery, termQuery, rangeQuery, wildcardQuery } from '@kbn/observability-plugin/server'; +import { + kqlQuery, + termQuery, + rangeQuery, + wildcardQuery, + termsQuery, +} from '@kbn/observability-plugin/server'; import { ALERT_RULE_PRODUCER, ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_RULE_PARAMETERS, } from '@kbn/rule-data-utils'; +import { observabilityFeatureId } from '@kbn/observability-shared-plugin/common'; import { SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_TYPE } from '../../../common/es_fields/apm'; import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; import { AggregationType } from '../../../common/rules/apm_rule_types'; @@ -59,7 +66,7 @@ export async function getServiceTransactionGroupsAlerts({ query: { bool: { filter: [ - ...termQuery(ALERT_RULE_PRODUCER, 'apm'), + ...termsQuery(ALERT_RULE_PRODUCER, 'apm', observabilityFeatureId), ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...rangeQuery(start, end), ...kqlQuery(kuery), diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts index b225f3ab70ef1..01a125f456443 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts @@ -5,13 +5,20 @@ * 2.0. */ -import { kqlQuery, termQuery, rangeQuery, wildcardQuery } from '@kbn/observability-plugin/server'; +import { + kqlQuery, + termQuery, + rangeQuery, + wildcardQuery, + termsQuery, +} from '@kbn/observability-plugin/server'; import { ALERT_RULE_PRODUCER, ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_UUID, } from '@kbn/rule-data-utils'; +import { observabilityFeatureId } from '@kbn/observability-shared-plugin/common'; import { SERVICE_NAME } from '../../../../common/es_fields/apm'; import { ServiceGroup } from '../../../../common/service_groups'; import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; @@ -51,7 +58,7 @@ export async function getServicesAlerts({ query: { bool: { filter: [ - ...termQuery(ALERT_RULE_PRODUCER, 'apm'), + ...termsQuery(ALERT_RULE_PRODUCER, 'apm', observabilityFeatureId), ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...rangeQuery(start, end), ...kqlQuery(kuery), diff --git a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts similarity index 86% rename from x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts index 8b72afc194e35..d71daed401c2d 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts @@ -10,7 +10,9 @@ import { errorCountActionVariables } from '@kbn/apm-plugin/server/routes/alerts/ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { SupertestWithRoleScopeType } from '../../../../services'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { createApmRule, fetchServiceInventoryAlertCounts, @@ -24,15 +26,17 @@ import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results'; import { waitForActiveRule } from './helpers/wait_for_active_rule'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('supertest'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); const es = getService('es'); const logger = getService('log'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + describe('error count threshold alert', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let supertest: SupertestWithRoleScopeType; - registry.when('error count threshold alert', { config: 'basic', archives: [] }, () => { const javaErrorMessage = 'a java error'; const phpErrorMessage = 'a php error'; @@ -50,7 +54,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { ], }; - before(() => { + before(async () => { + supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + withInternalHeaders: true, + }); + const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) .instance('instance'); @@ -95,13 +103,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]; }); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + return Promise.all([ apmSynthtraceEsClient.index(events), apmSynthtraceEsClient.index(phpEvents), ]); }); - after(() => apmSynthtraceEsClient.clean()); + after(async () => { + await apmSynthtraceEsClient.clean(); + await supertest.destroy(); + }); describe('create rule without kql filter', () => { let ruleId: string; @@ -141,6 +154,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let results: Array>; before(async () => { + await waitForActiveRule({ ruleId, supertest }); results = await waitForIndexConnectorResults({ es, minCount: 2 }); }); @@ -151,6 +165,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); + it('checks if rule is active', async () => { + const ruleStatus = await waitForActiveRule({ ruleId, supertest }); + expect(ruleStatus).to.be('active'); + }); + it('has the right keys', async () => { const phpEntry = results.find((result) => result.serviceName === 'opbeans-php')!; expect(Object.keys(phpEntry).sort()).to.eql([ @@ -170,7 +189,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has the right values', () => { const phpEntry = results.find((result) => result.serviceName === 'opbeans-php')!; - expect(omit(phpEntry, 'alertDetailsUrl')).to.eql({ + expect(omit(phpEntry, 'alertDetailsUrl', 'viewInAppUrl')).to.eql({ environment: 'production', interval: '1 hr', reason: @@ -181,9 +200,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { errorGroupingName: 'a php error', threshold: '1', triggerValue: '30', - viewInAppUrl: - 'http://mockedPublicBaseUrl/app/apm/services/opbeans-php/errors?environment=production', }); + + const url = new URL(phpEntry.viewInAppUrl); + + expect(url.pathname).to.equal('/app/apm/services/opbeans-php/errors'); + expect(url.searchParams.get('environment')).to.equal('production'); }); }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/alerts/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts new file mode 100644 index 0000000000000..27cc2b840d12b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts @@ -0,0 +1,256 @@ +/* + * 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 { Client, errors } from '@elastic/elasticsearch'; +import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import pRetry from 'p-retry'; +import { ApmRuleType } from '@kbn/rule-data-utils'; +import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; +import type { ApmApiClient } from '../../../../../services/apm_api'; +import type { SupertestWithRoleScopeType } from '../../../../../services'; + +export const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-*'; +export const APM_ACTION_VARIABLE_INDEX = 'apm-index-connector-test'; + +export async function createApmRule({ + supertest, + name, + ruleTypeId, + params, + actions = [], +}: { + supertest: SupertestWithRoleScopeType; + ruleTypeId: T; + name: string; + params: ApmRuleParamsType[T]; + actions?: any[]; +}) { + try { + const { body } = await supertest.post(`/api/alerting/rule`).send({ + params, + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], + name, + rule_type_id: ruleTypeId, + actions, + }); + return body; + } catch (error: any) { + throw new Error(`[Rule] Creating a rule failed: ${error}`); + } +} + +function getTimerange() { + return { + start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + end: new Date(Date.now() + 5 * 60 * 1000).toISOString(), + }; +} + +export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) { + const timerange = getTimerange(); + const serviceInventoryResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + kuery: '', + probability: 1, + documentType: ApmDocumentType.ServiceTransactionMetric, + rollupInterval: RollupInterval.SixtyMinutes, + useDurationSummary: true, + }, + }, + }); + + return serviceInventoryResponse.body.items.reduce>((acc, item) => { + return { ...acc, [item.serviceName]: item.alertsCount ?? 0 }; + }, {}); +} + +export async function fetchServiceTabAlertCount({ + apmApiClient, + serviceName, +}: { + apmApiClient: ApmApiClient; + serviceName: string; +}) { + const timerange = getTimerange(); + const alertsCountReponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count', + params: { + path: { + serviceName, + }, + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + return alertsCountReponse.body.alertsCount; +} + +export async function runRuleSoon({ + ruleId, + supertest, +}: { + ruleId: string; + supertest: SupertestWithRoleScopeType; +}): Promise> { + return pRetry( + async () => { + try { + const response = await supertest.post(`/internal/alerting/rule/${ruleId}/_run_soon`); + // Sometimes the rule may already be running, which returns a 200. Try until it isn't + if (response.status !== 204) { + throw new Error(`runRuleSoon got ${response.status} status`); + } + return response; + } catch (error) { + throw new Error(`[Rule] Running a rule ${ruleId} failed: ${error}`); + } + }, + { retries: 10 } + ); +} + +export async function deleteAlertsByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { + await es.deleteByQuery({ + index: APM_ALERTS_INDEX, + query: { term: { 'kibana.alert.rule.uuid': ruleId } }, + }); +} + +export async function deleteRuleById({ + supertest, + ruleId, +}: { + supertest: SupertestWithRoleScopeType; + ruleId: string; +}) { + await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo'); +} + +export async function deleteApmRules(supertest: SupertestWithRoleScopeType) { + const res = await supertest.get( + `/api/alerting/rules/_find?filter=alert.attributes.consumer:apm&per_page=10000` + ); + + return Promise.all( + res.body.data.map((rule: any) => deleteRuleById({ supertest, ruleId: rule.id })) + ); +} + +export function deleteApmAlerts(es: Client) { + return es.deleteByQuery({ + index: APM_ALERTS_INDEX, + conflicts: 'proceed', + query: { match_all: {} }, + }); +} + +export async function clearKibanaApmEventLog(es: Client) { + return es.deleteByQuery({ + index: '.kibana-event-log-*', + query: { term: { 'kibana.alert.rule.consumer': 'apm' } }, + }); +} + +export type ApmAlertFields = ParsedTechnicalFields & ObservabilityApmAlert; + +export async function createIndexConnector({ + supertest, + name, +}: { + supertest: SupertestWithRoleScopeType; + name: string; +}) { + const { body } = await supertest.post(`/api/actions/connector`).send({ + name, + config: { + index: APM_ACTION_VARIABLE_INDEX, + refresh: true, + }, + connector_type_id: '.index', + }); + + return body.id as string; +} + +export function getIndexAction({ + actionId, + actionVariables, +}: { + actionId: string; + actionVariables: Array<{ name: string }>; +}) { + return { + group: 'threshold_met', + id: actionId, + params: { + documents: [ + actionVariables.reduce>((acc, actionVariable) => { + acc[actionVariable.name] = `{{context.${actionVariable.name}}}`; + return acc; + }, {}), + ], + }, + frequency: { + notify_when: 'onActionGroupChange', + throttle: null, + summary: false, + }, + }; +} + +export async function deleteAllActionConnectors({ + supertest, + es, +}: { + supertest: SupertestWithRoleScopeType; + es: Client; +}): Promise { + const res = await supertest.get(`/api/actions/connectors`); + + const body = res.body as Array<{ id: string; connector_type_id: string; name: string }>; + return Promise.all( + body.map(({ id }) => { + return deleteActionConnector({ supertest, actionId: id }); + }) + ); +} + +async function deleteActionConnector({ + supertest, + actionId, +}: { + supertest: SupertestWithRoleScopeType; + actionId: string; +}) { + return supertest.delete(`/api/actions/connector/${actionId}`); +} + +export async function deleteActionConnectorIndex(es: Client) { + try { + await es.indices.delete({ index: APM_ACTION_VARIABLE_INDEX }); + } catch (e) { + if (e instanceof errors.ResponseError && e.statusCode === 404) { + return; + } + + throw e; + } +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts new file mode 100644 index 0000000000000..b94c2bcc52ee3 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts @@ -0,0 +1,39 @@ +/* + * 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 { Client } from '@elastic/elasticsearch'; +import { ToolingLog } from '@kbn/tooling-log'; +import type { SupertestWithRoleScopeType } from '../../../../../services'; +import { + clearKibanaApmEventLog, + deleteApmRules, + deleteApmAlerts, + deleteActionConnectorIndex, + deleteAllActionConnectors, +} from './alerting_api_helper'; + +export async function cleanupRuleAndAlertState({ + es, + supertest, + logger, +}: { + es: Client; + supertest: SupertestWithRoleScopeType; + logger: ToolingLog; +}) { + try { + await Promise.all([ + deleteApmRules(supertest), + deleteApmAlerts(es), + clearKibanaApmEventLog(es), + deleteActionConnectorIndex(es), + deleteAllActionConnectors({ supertest, es }), + ]); + } catch (e) { + logger.error(`An error occured while cleaning up the state: ${e}`); + } +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_active_apm_alerts.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_active_apm_alerts.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts new file mode 100644 index 0000000000000..989875522c720 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts @@ -0,0 +1,44 @@ +/* + * 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 { ToolingLog } from '@kbn/tooling-log'; +import pRetry from 'p-retry'; +import type { SupertestWithRoleScopeType } from '../../../../../services'; + +const RETRIES_COUNT = 10; + +export async function waitForActiveRule({ + ruleId, + supertest, + logger, +}: { + ruleId: string; + supertest: SupertestWithRoleScopeType; + logger?: ToolingLog; +}): Promise> { + return pRetry( + async () => { + const response = await supertest.get(`/api/alerting/rule/${ruleId}`); + const status = response.body?.execution_status?.status; + const expectedStatus = 'active'; + + if (status !== expectedStatus) { + throw new Error(`Expected: ${expectedStatus}: got ${status}`); + } + + return status; + }, + { + retries: RETRIES_COUNT, + onFailedAttempt: (error) => { + if (logger) { + logger.info(`Attempt ${error.attemptNumber}/${RETRIES_COUNT}: Waiting for active rule`); + } + }, + } + ); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts new file mode 100644 index 0000000000000..334631b354cd1 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts @@ -0,0 +1,52 @@ +/* + * 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 { + AggregationsAggregate, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import pRetry from 'p-retry'; +import { ApmAlertFields, APM_ALERTS_INDEX } from './alerting_api_helper'; + +async function getAlertByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { + const response = (await es.search({ + index: APM_ALERTS_INDEX, + body: { + query: { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + }, + })) as SearchResponse>; + + return response.hits.hits.map((hit) => hit._source) as ApmAlertFields[]; +} + +export async function waitForAlertsForRule({ + es, + ruleId, + minimumAlertCount = 1, +}: { + es: Client; + ruleId: string; + minimumAlertCount?: number; +}) { + return pRetry( + async () => { + const alerts = await getAlertByRuleId({ es, ruleId }); + const actualAlertCount = alerts.length; + if (actualAlertCount < minimumAlertCount) { + throw new Error(`Expected ${minimumAlertCount} but got ${actualAlertCount} alerts`); + } + + return alerts; + }, + { retries: 5 } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_index_connector_results.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_index_connector_results.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_index_connector_results.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_index_connector_results.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts new file mode 100644 index 0000000000000..71661e4cbc8bc --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts @@ -0,0 +1,19 @@ +/* + * 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('alerts', () => { + loadTestFile(require.resolve('./error_count_threshold.spec.ts')); + loadTestFile(require.resolve('./preview_chart_error_count.spec.ts')); + loadTestFile(require.resolve('./preview_chart_error_rate.spec.ts')); + loadTestFile(require.resolve('./preview_chart_transaction_duration.spec.ts')); + loadTestFile(require.resolve('./transaction_duration.spec.ts')); + loadTestFile(require.resolve('./transaction_error_rate.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts similarity index 50% rename from x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts index f09dbf1ad9184..d6792400fc2bc 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts @@ -5,21 +5,21 @@ * 2.0. */ +import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; +import expect from '@kbn/expect'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { + ERROR_GROUP_ID, SERVICE_ENVIRONMENT, SERVICE_NAME, - ERROR_GROUP_ID, -} from '@kbn/apm-plugin/common/es_fields/apm'; -import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; -import { getErrorGroupingKey } from '@kbn/apm-synthtrace-client/src/lib/apm/instance'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +} from '@kbn/observability-shared-plugin/common'; +import { generateLongIdWithSeed } from '@kbn/apm-synthtrace-client/src/lib/utils/generate_id'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateErrorData } from './generate_data'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -54,31 +54,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); - registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => { - it('error_count (without data)', async () => { - const options = getOptions(); - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series).to.eql([]); - }); - }); - - registry.when.skip(`with data loaded`, { config: 'basic', archives: [] }, () => { - // FLAKY: https://github.com/elastic/kibana/issues/172769 - describe('error_count: with data loaded', () => { - beforeEach(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - afterEach(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { + describe('preview chart error count', () => { + describe(`without data loaded`, () => { + it('error_count (without data)', async () => { const options = getOptions(); const response = await apmApiClient.readUser({ @@ -87,230 +65,254 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); expect(response.status).to.be(200); - expect( - response.body.errorCountChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); + expect(response.body.errorCountChartPreview.series).to.eql([]); }); + }); - it('with error grouping key', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - errorGroupingKey: `${getErrorGroupingKey('Error 1')}`, - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', - ...options, + describe(`with data loaded`, () => { + describe('error_count: with data loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await Promise.all([ + generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); }); - expect(response.status).to.be(200); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production', y: 250 }]); - }); + after(() => apmSynthtraceEsClient.clean()); - it('with no group by parameter', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', - }); + it('with data', async () => { + const options = getOptions(); - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series.length).to.equal(1); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production', y: 375 }]); - }); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorCountChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT], + it('with error grouping key', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + errorGroupingKey: `${generateLongIdWithSeed('Error 1')}`, + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, }, - }, - }; + }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production', y: 250 }]); }); - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series.length).to.equal(1); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production', y: 375 }]); - }); + it('with no group by parameter', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); - it('with group by on error grouping key', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + expect(response.status).to.be(200); + expect(response.body.errorCountChartPreview.series.length).to.equal(1); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production', y: 375 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT], + }, }, - }, - }; + }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', - }); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series.length).to.equal(2); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: `synth-go_production_${getErrorGroupingKey('Error 1')}`, - y: 250, - }, - { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, - y: 125, - }, - ]); - }); + expect(response.status).to.be(200); + expect(response.body.errorCountChartPreview.series.length).to.equal(1); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production', y: 375 }]); + }); - it('with group by on error grouping key and filter on error grouping key', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - errorGroupingKey: `${getErrorGroupingKey('Error 0')}`, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + it('with group by on error grouping key', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + }, }, - }, - }; + }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorCountChartPreview.series.length).to.equal(2); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: `synth-go_production_${generateLongIdWithSeed('Error 1')}`, + y: 250, + }, + { + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, + y: 125, + }, + ]); }); - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series.length).to.equal(1); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, - y: 125, - }, - ]); - }); + it('with group by on error grouping key and filter on error grouping key', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + errorGroupingKey: `${generateLongIdWithSeed('Error 0')}`, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); - it('with empty service name', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: '', - environment: 'ENVIRONMENT_ALL', - interval: '5m', + expect(response.status).to.be(200); + expect(response.body.errorCountChartPreview.series.length).to.equal(1); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, + y: 125, }, - }, - }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + ]); }); - expect(response.status).to.be(200); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production', y: 375 }, - { name: 'synth-java_production', y: 375 }, - ]); - }); - - it('with empty service name and group by on error grouping key', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: '', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + it('with empty service name', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: '', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, }, - }, - }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }; + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production', y: 375 }, + { name: 'synth-java_production', y: 375 }, + ]); }); - expect(response.status).to.be(200); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: `synth-go_production_${getErrorGroupingKey('Error 1')}`, - y: 250, - }, - { - name: `synth-java_production_${getErrorGroupingKey('Error 1')}`, - y: 250, - }, - { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, - y: 125, - }, - { - name: `synth-java_production_${getErrorGroupingKey('Error 0')}`, - y: 125, - }, - ]); + it('with empty service name and group by on error grouping key', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: '', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + }, + }, + }; + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: `synth-go_production_${generateLongIdWithSeed('Error 1')}`, + y: 250, + }, + { + name: `synth-java_production_${generateLongIdWithSeed('Error 1')}`, + y: 250, + }, + { + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, + y: 125, + }, + { + name: `synth-java_production_${generateLongIdWithSeed('Error 0')}`, + y: 125, + }, + ]); + }); }); }); - }); - registry.when.skip( - `with data loaded and using KQL filter`, - { config: 'basic', archives: [] }, - () => { - // FLAKY: https://github.com/elastic/kibana/issues/176975 + describe(`with data loaded and using KQL filter`, () => { describe('error_count: with data loaded and using KQL filter', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); }); @@ -340,7 +342,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ...getOptionsWithFilterQuery().params.query, searchConfiguration: JSON.stringify({ query: { - query: `service.name: synth-go and error.grouping_key: ${getErrorGroupingKey( + query: `service.name: synth-go and error.grouping_key: ${generateLongIdWithSeed( 'Error 1' )}`, language: 'kuery', @@ -430,11 +432,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { })) ).to.eql([ { - name: `synth-go_production_${getErrorGroupingKey('Error 1')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 1')}`, y: 250, }, { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, y: 125, }, ]); @@ -447,7 +449,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ...getOptionsWithFilterQuery().params.query, searchConfiguration: JSON.stringify({ query: { - query: `service.name: synth-go and error.grouping_key: ${getErrorGroupingKey( + query: `service.name: synth-go and error.grouping_key: ${generateLongIdWithSeed( 'Error 0' )}`, language: 'kuery', @@ -472,7 +474,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { })) ).to.eql([ { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, y: 125, }, ]); @@ -539,24 +541,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { })) ).to.eql([ { - name: `synth-go_production_${getErrorGroupingKey('Error 1')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 1')}`, y: 250, }, { - name: `synth-java_production_${getErrorGroupingKey('Error 1')}`, + name: `synth-java_production_${generateLongIdWithSeed('Error 1')}`, y: 250, }, { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, y: 125, }, { - name: `synth-java_production_${getErrorGroupingKey('Error 0')}`, + name: `synth-java_production_${generateLongIdWithSeed('Error 0')}`, y: 125, }, ]); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts new file mode 100644 index 0000000000000..3e5c0753fbc1d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts @@ -0,0 +1,607 @@ +/* + * 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 { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '@kbn/observability-shared-plugin/common'; +import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; +import expect from '@kbn/expect'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { generateErrorData } from './generate_data'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + const getOptions = () => ({ + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }); + + const getOptionsWithFilterQuery = () => ({ + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + interval: '5m', + searchConfiguration: JSON.stringify({ + query: { + query: 'service.name: synth-go and transaction.type: request', + language: 'kuery', + }, + }), + serviceName: undefined, + transactionType: undefined, + transactionName: undefined, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + describe('preview chart error rate', () => { + describe(`without data loaded`, () => { + it('transaction_error_rate without data', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series).to.eql([]); + }); + }); + + describe(`with data loaded`, () => { + describe('transaction_error_rate: with data loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await Promise.all([ + generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('with data', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); + + it('with transaction name', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + transactionName: 'GET /banana', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 50 }]); + }); + + it('with nonexistent transaction name', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + transactionName: 'foo', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series).to.eql([]); + }); + + it('with no group by parameter', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); + }); + + it('with group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(2); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: 'synth-go_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-go_production_request_GET /apple', + y: 25, + }, + ]); + }); + + it('with group by on transaction name and filter on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + transactionName: 'GET /apple', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 25 }]); + }); + + it('with empty service name, transaction name and transaction type', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: '', + transactionName: '', + transactionType: '', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request', y: 37.5 }, + { name: 'synth-java_production_request', y: 37.5 }, + ]); + }); + + it('with empty service name, transaction name, transaction type and group by on transaction name', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: '', + transactionName: '', + transactionType: '', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: 'synth-go_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-java_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-go_production_request_GET /apple', + y: 25, + }, + { + name: 'synth-java_production_request_GET /apple', + y: 25, + }, + ]); + }); + }); + }); + + describe(`with data loaded and using KQL filter`, () => { + describe('transaction_error_rate: with data loaded and using KQL filter', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await Promise.all([ + generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('with data', async () => { + const options = getOptionsWithFilterQuery(); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); + + it('with transaction name in filter query', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: GET /banana', + language: 'kuery', + }, + }), + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 50 }]); + }); + + it('with nonexistent transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: foo', + language: 'kuery', + }, + }), + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series).to.eql([]); + }); + + it('with no group by parameter', async () => { + const options = getOptionsWithFilterQuery(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); + }); + + it('with group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(2); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: 'synth-go_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-go_production_request_GET /apple', + y: 25, + }, + ]); + }); + + it('with group by on transaction name and filter on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: GET /apple', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 25 }]); + }); + + it('with empty filter query', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: '', + language: 'kuery', + }, + }), + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request', y: 37.5 }, + { name: 'synth-java_production_request', y: 37.5 }, + ]); + }); + + it('with empty filter query and group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: '', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: 'synth-go_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-java_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-go_production_request_GET /apple', + y: 25, + }, + { + name: 'synth-java_production_request_GET /apple', + y: 25, + }, + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_transaction_duration.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_transaction_duration.spec.ts new file mode 100644 index 0000000000000..af7f83c393a68 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_transaction_duration.spec.ts @@ -0,0 +1,564 @@ +/* + * 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 { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '@kbn/observability-shared-plugin/common'; +import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; +import expect from '@kbn/expect'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { generateLatencyData } from './generate_data'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + const getOptions = () => ({ + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }); + + const getOptionsWithFilterQuery = () => ({ + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + interval: '5m', + searchConfiguration: JSON.stringify({ + query: { + query: 'service.name: synth-go and transaction.type: request', + language: 'kuery', + }, + }), + serviceName: undefined, + transactionType: undefined, + transactionName: undefined, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + describe('preview chart transaction duration', () => { + describe(`without data loaded`, () => { + it('transaction_duration (without data)', async () => { + const options = getOptions(); + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series).to.eql([]); + }); + }); + + describe(`with data loaded`, () => { + describe('transaction_duration: with data loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + await Promise.all([ + generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('with data', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); + + it('with transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + transactionName: 'GET /banana', + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 5000 }]); + }); + + it('with nonexistent transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + transactionName: 'foo', + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series).to.eql([]); + }); + + it('with no group by parameter', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); + }); + + it('with group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(2); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request_GET /apple', y: 10000 }, + { name: 'synth-go_production_request_GET /banana', y: 5000 }, + ]); + }); + + it('with group by on transaction name and filter on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + transactionName: 'GET /apple', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 10000 }]); + }); + + it('with empty service name, transaction name and transaction type', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + serviceName: '', + transactionName: '', + transactionType: '', + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request', y: 7500 }, + { name: 'synth-java_production_request', y: 7500 }, + ]); + }); + + it('with empty service name, transaction name, transaction type and group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + serviceName: '', + transactionName: '', + transactionType: '', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(4); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request_GET /apple', y: 10000 }, + { name: 'synth-java_production_request_GET /apple', y: 10000 }, + { name: 'synth-go_production_request_GET /banana', y: 5000 }, + { name: 'synth-java_production_request_GET /banana', y: 5000 }, + ]); + }); + }); + }); + + describe(`with data loaded and using KQL filter`, () => { + describe('transaction_duration: with data loaded and using KQL filter', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await Promise.all([ + generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('with data', async () => { + const options = getOptionsWithFilterQuery(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); + + it('with transaction name in filter query', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: GET /banana', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 5000 }]); + }); + + it('with nonexistent transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: foo', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series).to.eql([]); + }); + + it('with no group by parameter', async () => { + const options = getOptionsWithFilterQuery(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); + }); + + it('with group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(2); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request_GET /apple', y: 10000 }, + { name: 'synth-go_production_request_GET /banana', y: 5000 }, + ]); + }); + + it('with group by on transaction name and filter on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: GET /apple', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 10000 }]); + }); + + it('with empty filter query', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: '', + language: 'kuery', + }, + }), + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request', y: 7500 }, + { name: 'synth-java_production_request', y: 7500 }, + ]); + }); + + it('with empty filter query and group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: '', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(4); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request_GET /apple', y: 10000 }, + { name: 'synth-java_production_request_GET /apple', y: 10000 }, + { name: 'synth-go_production_request_GET /banana', y: 5000 }, + { name: 'synth-java_production_request_GET /banana', y: 5000 }, + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts similarity index 87% rename from x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts index 0d7460ff5be50..c23baa32c17dc 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts @@ -11,7 +11,9 @@ import { transactionDurationActionVariables } from '@kbn/apm-plugin/server/route import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { SupertestWithRoleScopeType } from '../../../../services'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { createApmRule, fetchServiceInventoryAlertCounts, @@ -25,13 +27,12 @@ import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; import { waitForActiveRule } from './helpers/wait_for_active_rule'; import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('supertest'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); const es = getService('es'); const logger = getService('log'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const ruleParams = { threshold: 3000, @@ -44,8 +45,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { groupBy: ['service.name', 'service.environment', 'transaction.type', 'transaction.name'], }; - registry.when('transaction duration alert', { config: 'basic', archives: [] }, () => { - before(() => { + describe('transaction duration alert', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let supertest: SupertestWithRoleScopeType; + + before(async () => { + supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + withInternalHeaders: true, + }); + const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) .instance('instance'); @@ -68,11 +76,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { .success(), ]; }); + + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); return apmSynthtraceEsClient.index(events); }); after(async () => { await apmSynthtraceEsClient.clean(); + await supertest.destroy(); }); describe('create rule for opbeans-java without kql filter', () => { @@ -133,7 +144,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('populates the document with the correct values', async () => { - expect(omit(results[0], 'alertDetailsUrl')).to.eql({ + expect(omit(results[0], 'alertDetailsUrl', 'viewInAppUrl')).to.eql({ environment: 'production', interval: '5 mins', reason: @@ -143,9 +154,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { transactionName: 'tx-java', threshold: '3000', triggerValue: '5,000 ms', - viewInAppUrl: - 'http://mockedPublicBaseUrl/app/apm/services/opbeans-java?transactionType=request&environment=production', }); + + const url = new URL(results[0].viewInAppUrl); + + expect(url.pathname).to.equal('/app/apm/services/opbeans-java'); + expect(url.searchParams.get('transactionType')).to.equal('request'); + expect(url.searchParams.get('environment')).to.equal('production'); }); }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts similarity index 88% rename from x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts index 509d839ecef6d..e3f3bde6e6bf8 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts @@ -10,7 +10,9 @@ import { transactionErrorRateActionVariables } from '@kbn/apm-plugin/server/rout import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { SupertestWithRoleScopeType } from '../../../../services'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { createApmRule, fetchServiceInventoryAlertCounts, @@ -24,16 +26,22 @@ import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; import { waitForActiveRule } from './helpers/wait_for_active_rule'; import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('supertest'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); const es = getService('es'); const logger = getService('log'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + describe('transaction error rate alert', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let supertest: SupertestWithRoleScopeType; + + before(async () => { + supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + withInternalHeaders: true, + }); - registry.when('transaction error rate alert', { config: 'basic', archives: [] }, () => { - before(() => { const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) .instance('instance'); @@ -66,11 +74,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { .success(), ]; }); + + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); return apmSynthtraceEsClient.index(events); }); after(async () => { await apmSynthtraceEsClient.clean(); + await supertest.destroy(); }); describe('create rule without kql query', () => { @@ -142,7 +153,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('has the right values', () => { - expect(omit(results[0], 'alertDetailsUrl')).to.eql({ + expect(omit(results[0], 'alertDetailsUrl', 'viewInAppUrl')).to.eql({ environment: 'production', interval: '5 mins', reason: @@ -152,9 +163,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { threshold: '40', transactionType: 'request', triggerValue: '50', - viewInAppUrl: - 'http://mockedPublicBaseUrl/app/apm/services/opbeans-java?transactionType=request&environment=production', }); + + const url = new URL(results[0].viewInAppUrl); + + expect(url.pathname).to.equal('/app/apm/services/opbeans-java'); + expect(url.searchParams.get('transactionType')).to.equal('request'); + expect(url.searchParams.get('environment')).to.equal('production'); }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index a62c11d40b1af..0d1055c58299b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -12,5 +12,6 @@ export default function apmApiIntegrationTests({ }: DeploymentAgnosticFtrProviderContext) { describe('APM', function () { loadTestFile(require.resolve('./agent_explorer')); + loadTestFile(require.resolve('./alerts')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/services/apm_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/apm_api.ts index 26d92997a6021..c3f43b57902e8 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/apm_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/apm_api.ts @@ -135,3 +135,5 @@ export function ApmApiProvider(context: DeploymentAgnosticFtrProviderContext) { writeUser: createApmApiClient(context, 'editor'), }; } + +export type ApmApiClient = ReturnType; diff --git a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts index 033d64e8f12e8..61bdfd97d2254 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts @@ -12,12 +12,12 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { range } from 'lodash'; import { ML_ANOMALY_SEVERITY } from '@kbn/ml-anomaly-utils/anomaly_severity'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createAndRunApmMlJobs } from '../../common/utils/create_and_run_apm_ml_jobs'; -import { createApmRule } from './helpers/alerting_api_helper'; +import { waitForAlertsForRule } from '../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule'; import { waitForActiveRule } from './helpers/wait_for_active_rule'; -import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; +import { createApmRule } from './helpers/alerting_api_helper'; import { cleanupRuleAndAlertState } from './helpers/cleanup_rule_and_alert_state'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createAndRunApmMlJobs } from '../../common/utils/create_and_run_apm_ml_jobs'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts index 5da6ee4f860d0..4ae1085eea053 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts @@ -5,16 +5,13 @@ * 2.0. */ -import { Client, errors } from '@elastic/elasticsearch'; +import { Client } from '@elastic/elasticsearch'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import pRetry from 'p-retry'; import type { Agent as SuperTestAgent } from 'supertest'; import { ApmRuleType } from '@kbn/rule-data-utils'; import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema'; -import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; -import { ApmApiClient } from '../../../common/config'; export const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-*'; export const APM_ACTION_VARIABLE_INDEX = 'apm-index-connector-test'; @@ -53,59 +50,6 @@ export async function createApmRule({ } } -function getTimerange() { - return { - start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - end: new Date(Date.now() + 5 * 60 * 1000).toISOString(), - }; -} - -export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) { - const timerange = getTimerange(); - const serviceInventoryResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - ...timerange, - environment: 'ENVIRONMENT_ALL', - kuery: '', - probability: 1, - documentType: ApmDocumentType.ServiceTransactionMetric, - rollupInterval: RollupInterval.SixtyMinutes, - useDurationSummary: true, - }, - }, - }); - - return serviceInventoryResponse.body.items.reduce>((acc, item) => { - return { ...acc, [item.serviceName]: item.alertsCount ?? 0 }; - }, {}); -} - -export async function fetchServiceTabAlertCount({ - apmApiClient, - serviceName, -}: { - apmApiClient: ApmApiClient; - serviceName: string; -}) { - const timerange = getTimerange(); - const alertsCountReponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count', - params: { - path: { - serviceName, - }, - query: { - ...timerange, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - - return alertsCountReponse.body.alertsCount; -} - export async function runRuleSoon({ ruleId, supertest, @@ -132,13 +76,6 @@ export async function runRuleSoon({ ); } -export async function deleteAlertsByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { - await es.deleteByQuery({ - index: APM_ALERTS_INDEX, - query: { term: { 'kibana.alert.rule.uuid': ruleId } }, - }); -} - export async function deleteRuleById({ supertest, ruleId, @@ -159,71 +96,6 @@ export async function deleteApmRules(supertest: SuperTestAgent) { ); } -export function deleteApmAlerts(es: Client) { - return es.deleteByQuery({ - index: APM_ALERTS_INDEX, - conflicts: 'proceed', - query: { match_all: {} }, - }); -} - -export async function clearKibanaApmEventLog(es: Client) { - return es.deleteByQuery({ - index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'apm' } }, - }); -} - -export type ApmAlertFields = ParsedTechnicalFields & ObservabilityApmAlert; - -export async function createIndexConnector({ - supertest, - name, -}: { - supertest: SuperTestAgent; - name: string; -}) { - const { body } = await supertest - .post(`/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name, - config: { - index: APM_ACTION_VARIABLE_INDEX, - refresh: true, - }, - connector_type_id: '.index', - }); - - return body.id as string; -} - -export function getIndexAction({ - actionId, - actionVariables, -}: { - actionId: string; - actionVariables: Array<{ name: string }>; -}) { - return { - group: 'threshold_met', - id: actionId, - params: { - documents: [ - actionVariables.reduce>((acc, actionVariable) => { - acc[actionVariable.name] = `{{context.${actionVariable.name}}}`; - return acc; - }, {}), - ], - }, - frequency: { - notify_when: 'onActionGroupChange', - throttle: null, - summary: false, - }, - }; -} - export async function deleteAllActionConnectors({ supertest, es, @@ -241,6 +113,8 @@ export async function deleteAllActionConnectors({ ); } +export type ApmAlertFields = ParsedTechnicalFields & ObservabilityApmAlert; + async function deleteActionConnector({ supertest, actionId, @@ -250,15 +124,3 @@ async function deleteActionConnector({ }) { return supertest.delete(`/api/actions/connector/${actionId}`).set('kbn-xsrf', 'foo'); } - -export async function deleteActionConnectorIndex(es: Client) { - try { - await es.indices.delete({ index: APM_ACTION_VARIABLE_INDEX }); - } catch (e) { - if (e instanceof errors.ResponseError && e.statusCode === 404) { - return; - } - - throw e; - } -} diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts index 2fae6c9643ff7..c3cce2d498c87 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts @@ -9,12 +9,11 @@ import { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; import type { Agent as SuperTestAgent } from 'supertest'; import { + deleteActionConnectorIndex, clearKibanaApmEventLog, - deleteApmRules, deleteApmAlerts, - deleteActionConnectorIndex, - deleteAllActionConnectors, -} from './alerting_api_helper'; +} from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper'; +import { deleteApmRules, deleteAllActionConnectors } from './alerting_api_helper'; export async function cleanupRuleAndAlertState({ es, diff --git a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts deleted file mode 100644 index 3af62d826fd31..0000000000000 --- a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts +++ /dev/null @@ -1,599 +0,0 @@ -/* - * 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 { - SERVICE_ENVIRONMENT, - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '@kbn/apm-plugin/common/es_fields/apm'; -import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { generateErrorData } from './generate_data'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - const getOptions = () => ({ - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }); - - const getOptionsWithFilterQuery = () => ({ - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - interval: '5m', - searchConfiguration: JSON.stringify({ - query: { - query: 'service.name: synth-go and transaction.type: request', - language: 'kuery', - }, - }), - serviceName: undefined, - transactionType: undefined, - transactionName: undefined, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - - registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => { - it('transaction_error_rate without data', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series).to.eql([]); - }); - }); - - registry.when(`with data loaded`, { config: 'basic', archives: [] }, () => { - // FLAKY: https://github.com/elastic/kibana/issues/176977 - describe('transaction_error_rate: with data loaded', () => { - before(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); - }); - - it('with transaction name', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - transactionName: 'GET /banana', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 50 }]); - }); - - it('with nonexistent transaction name', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - transactionName: 'foo', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series).to.eql([]); - }); - - it('with no group by parameter', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); - }); - - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); - }); - - it('with group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(2); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: 'synth-go_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-go_production_request_GET /apple', - y: 25, - }, - ]); - }); - - it('with group by on transaction name and filter on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - transactionName: 'GET /apple', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 25 }]); - }); - - it.skip('with empty service name, transaction name and transaction type', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: '', - transactionName: '', - transactionType: '', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request', y: 37.5 }, - { name: 'synth-java_production_request', y: 37.5 }, - ]); - }); - - it('with empty service name, transaction name, transaction type and group by on transaction name', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: '', - transactionName: '', - transactionType: '', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: 'synth-go_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-java_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-go_production_request_GET /apple', - y: 25, - }, - { - name: 'synth-java_production_request_GET /apple', - y: 25, - }, - ]); - }); - }); - }); - - registry.when(`with data loaded and using KQL filter`, { config: 'basic', archives: [] }, () => { - // FLAKY: https://github.com/elastic/kibana/issues/176983 - describe('transaction_error_rate: with data loaded and using KQL filter', () => { - before(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { - const options = getOptionsWithFilterQuery(); - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); - }); - - it('with transaction name in filter query', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: GET /banana', - language: 'kuery', - }, - }), - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 50 }]); - }); - - it('with nonexistent transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: foo', - language: 'kuery', - }, - }), - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series).to.eql([]); - }); - - it('with no group by parameter', async () => { - const options = getOptionsWithFilterQuery(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); - }); - - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); - }); - - it('with group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(2); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: 'synth-go_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-go_production_request_GET /apple', - y: 25, - }, - ]); - }); - - it('with group by on transaction name and filter on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: GET /apple', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 25 }]); - }); - - it('with empty filter query', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: '', - language: 'kuery', - }, - }), - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request', y: 37.5 }, - { name: 'synth-java_production_request', y: 37.5 }, - ]); - }); - - it.skip('with empty filter query and group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: '', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: 'synth-go_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-java_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-go_production_request_GET /apple', - y: 25, - }, - { - name: 'synth-java_production_request_GET /apple', - y: 25, - }, - ]); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts deleted file mode 100644 index a677ce11cdb0c..0000000000000 --- a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts +++ /dev/null @@ -1,556 +0,0 @@ -/* - * 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 { - SERVICE_ENVIRONMENT, - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '@kbn/apm-plugin/common/es_fields/apm'; -import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { generateLatencyData } from './generate_data'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - const getOptions = () => ({ - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }); - - const getOptionsWithFilterQuery = () => ({ - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - interval: '5m', - searchConfiguration: JSON.stringify({ - query: { - query: 'service.name: synth-go and transaction.type: request', - language: 'kuery', - }, - }), - serviceName: undefined, - transactionType: undefined, - transactionName: undefined, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - - registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => { - it('transaction_duration (without data)', async () => { - const options = getOptions(); - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series).to.eql([]); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/176989 - registry.when(`with data loaded`, { config: 'basic', archives: [] }, () => { - // FLAKY: https://github.com/elastic/kibana/issues/176986 - // Failing: See https://github.com/elastic/kibana/issues/176989 - describe('transaction_duration: with data loaded', () => { - before(async () => { - await generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); - }); - - it('with transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - transactionName: 'GET /banana', - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 5000 }]); - }); - - it('with nonexistent transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - transactionName: 'foo', - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series).to.eql([]); - }); - - it('with no group by parameter', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); - }); - - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); - }); - - it('with group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(2); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request_GET /apple', y: 10000 }, - { name: 'synth-go_production_request_GET /banana', y: 5000 }, - ]); - }); - - it('with group by on transaction name and filter on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - transactionName: 'GET /apple', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 10000 }]); - }); - - it('with empty service name, transaction name and transaction type', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - serviceName: '', - transactionName: '', - transactionType: '', - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request', y: 7500 }, - { name: 'synth-java_production_request', y: 7500 }, - ]); - }); - - it('with empty service name, transaction name, transaction type and group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - serviceName: '', - transactionName: '', - transactionType: '', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(4); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request_GET /apple', y: 10000 }, - { name: 'synth-java_production_request_GET /apple', y: 10000 }, - { name: 'synth-go_production_request_GET /banana', y: 5000 }, - { name: 'synth-java_production_request_GET /banana', y: 5000 }, - ]); - }); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/176989 - registry.when(`with data loaded and using KQL filter`, { config: 'basic', archives: [] }, () => { - describe('transaction_duration: with data loaded and using KQL filter', () => { - before(async () => { - await generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { - const options = getOptionsWithFilterQuery(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); - }); - - it('with transaction name in filter query', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: GET /banana', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 5000 }]); - }); - - it('with nonexistent transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: foo', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series).to.eql([]); - }); - - it('with no group by parameter', async () => { - const options = getOptionsWithFilterQuery(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); - }); - - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); - }); - - it('with group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(2); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request_GET /apple', y: 10000 }, - { name: 'synth-go_production_request_GET /banana', y: 5000 }, - ]); - }); - - it('with group by on transaction name and filter on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: GET /apple', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 10000 }]); - }); - - it('with empty filter query', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: '', - language: 'kuery', - }, - }), - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request', y: 7500 }, - { name: 'synth-java_production_request', y: 7500 }, - ]); - }); - - it('with empty filter query and group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: '', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(4); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request_GET /apple', y: 10000 }, - { name: 'synth-java_production_request_GET /apple', y: 10000 }, - { name: 'synth-go_production_request_GET /banana', y: 5000 }, - { name: 'synth-java_production_request_GET /banana', y: 5000 }, - ]); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts index 044006e273348..93c63875e28b8 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts @@ -7,10 +7,10 @@ import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { ApmRuleType } from '@kbn/rule-data-utils'; import expect from '@kbn/expect'; +import { waitForActiveApmAlert } from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { createApmRule } from '../../alerts/helpers/alerting_api_helper'; import { cleanupRuleAndAlertState } from '../../alerts/helpers/cleanup_rule_and_alert_state'; -import { waitForActiveApmAlert } from '../../alerts/helpers/wait_for_active_apm_alerts'; import { createServiceGroupApi, deleteAllServiceGroups, From 14dc86c63272013f6a539b5449d3ebd3b224ffbf Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 20:34:32 +0100 Subject: [PATCH 6/9] Use AlertingApiProvider --- .../apm/alerts/error_count_threshold.spec.ts | 122 ++++++--- .../apm/alerts/helpers/alerting_api_helper.ts | 256 ------------------ .../apm/alerts/helpers/alerting_helper.ts | 96 +++++++ .../helpers/cleanup_rule_and_alert_state.ts | 39 --- .../alerts/helpers/wait_for_active_rule.ts | 44 --- .../helpers/wait_for_alerts_for_rule.ts | 52 ---- .../apm/alerts/transaction_duration.spec.ts | 103 +++++-- .../apm/alerts/transaction_error_rate.spec.ts | 98 +++++-- .../services/alerting_api.ts | 136 +++++++++- .../tests/alerts/anomaly_alert.spec.ts | 2 +- .../alerts/helpers/alerting_api_helper.ts | 43 ++- .../helpers/cleanup_rule_and_alert_state.ts | 2 +- .../helpers/wait_for_active_apm_alerts.ts | 4 +- .../helpers/wait_for_alerts_for_rule.ts | 5 +- .../wait_for_index_connector_results.ts | 2 +- .../service_group_count.spec.ts | 2 +- .../tests/services/service_alerts.spec.ts | 2 +- .../transactions_groups_alerts.spec.ts | 2 +- 18 files changed, 508 insertions(+), 502 deletions(-) delete mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_helper.ts delete mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts delete mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts delete mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts rename x-pack/test/{api_integration/deployment_agnostic/apis/observability/apm => apm_api_integration/tests}/alerts/helpers/wait_for_active_apm_alerts.ts (93%) rename x-pack/test/{api_integration/deployment_agnostic/apis/observability/apm => apm_api_integration/tests}/alerts/helpers/wait_for_index_connector_results.ts (85%) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts index d71daed401c2d..4e3164ebe5970 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts @@ -11,31 +11,28 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import type { SupertestWithRoleScopeType } from '../../../../services'; +import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { - createApmRule, fetchServiceInventoryAlertCounts, fetchServiceTabAlertCount, ApmAlertFields, - createIndexConnector, getIndexAction, -} from './helpers/alerting_api_helper'; -import { cleanupRuleAndAlertState } from './helpers/cleanup_rule_and_alert_state'; -import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; -import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results'; -import { waitForActiveRule } from './helpers/wait_for_active_rule'; + APM_ACTION_VARIABLE_INDEX, + APM_ALERTS_INDEX, +} from './helpers/alerting_helper'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const roleScopedSupertest = getService('roleScopedSupertest'); - const es = getService('es'); - const logger = getService('log'); const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); + const alertingApi = getService('alertingApi'); + const samlAuth = getService('samlAuth'); describe('error count threshold alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertest: SupertestWithRoleScopeType; + let supertestWithRoleScope: SupertestWithRoleScopeType; + let roleAuthc: RoleCredentials; const javaErrorMessage = 'a java error'; const phpErrorMessage = 'a php error'; @@ -55,10 +52,13 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }; before(async () => { - supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + supertestWithRoleScope = await roleScopedSupertest.getSupertestWithRoleScope('viewer', { withInternalHeaders: true, + useCookieHeader: true, }); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) .instance('instance'); @@ -113,7 +113,8 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertest.destroy(); + await supertestWithRoleScope.destroy(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); describe('create rule without kql filter', () => { @@ -122,31 +123,58 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let actionId: string; before(async () => { - actionId = await createIndexConnector({ supertest, name: 'Transation error count' }); + actionId = await alertingApi.createIndexConnector({ + name: 'Transation error count', + indexName: APM_ACTION_VARIABLE_INDEX, + roleAuthc, + }); + const indexAction = getIndexAction({ actionId, actionVariables: errorCountActionVariables, }); - const createdRule = await createApmRule({ - supertest, + + const createdRule = await alertingApi.createRule({ ruleTypeId: ApmRuleType.ErrorCount, name: 'Apm error count without kql query', + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], params: { ...ruleParams, }, actions: [indexAction], + roleAuthc, }); ruleId = createdRule.id; - alerts = await waitForAlertsForRule({ es, ruleId, minimumAlertCount: 2 }); + alerts = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ALERTS_INDEX, + ruleId, + docCountTarget: 2, + }) + ).hits.hits.map((hit) => hit._source) as ApmAlertFields[]; }); - after(async () => { - await cleanupRuleAndAlertState({ es, supertest, logger }); - }); + after(() => + alertingApi.cleanUpAlerts({ + roleAuthc, + ruleId, + alertIndexName: APM_ALERTS_INDEX, + connectorIndexName: APM_ACTION_VARIABLE_INDEX, + consumer: 'apm', + }) + ); it('checks if rule is active', async () => { - const ruleStatus = await waitForActiveRule({ ruleId, supertest }); + const ruleStatus = await alertingApi.waitForRuleStatus({ + ruleId, + roleAuthc, + expectedStatus: 'active', + }); expect(ruleStatus).to.be('active'); }); @@ -154,8 +182,18 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let results: Array>; before(async () => { - await waitForActiveRule({ ruleId, supertest }); - results = await waitForIndexConnectorResults({ es, minCount: 2 }); + await alertingApi.waitForRuleStatus({ + ruleId, + roleAuthc, + expectedStatus: 'active', + }); + + results = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ACTION_VARIABLE_INDEX, + docCountTarget: 2, + }) + ).hits.hits.map((hit) => hit._source) as Array>; }); it('produces a index action document for each service', async () => { @@ -166,7 +204,11 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }); it('checks if rule is active', async () => { - const ruleStatus = await waitForActiveRule({ ruleId, supertest }); + const ruleStatus = await alertingApi.waitForRuleStatus({ + ruleId, + roleAuthc, + expectedStatus: 'active', + }); expect(ruleStatus).to.be('active'); }); @@ -277,30 +319,48 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let ruleId: string; before(async () => { - const createdRule = await createApmRule({ - supertest, + const createdRule = await alertingApi.createRule({ ruleTypeId: ApmRuleType.ErrorCount, name: 'Apm error count with kql query', + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], params: { + ...ruleParams, searchConfiguration: { query: { query: 'service.name: opbeans-php', language: 'kuery', }, }, - ...ruleParams, }, actions: [], + roleAuthc, }); + ruleId = createdRule.id; }); - after(async () => { - await cleanupRuleAndAlertState({ es, supertest, logger }); - }); + after(() => + alertingApi.cleanUpAlerts({ + roleAuthc, + ruleId, + alertIndexName: APM_ALERTS_INDEX, + connectorIndexName: APM_ACTION_VARIABLE_INDEX, + consumer: 'apm', + }) + ); it('produces one alert for the opbeans-php service', async () => { - const alerts = await waitForAlertsForRule({ es, ruleId }); + const alerts = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ALERTS_INDEX, + ruleId, + }) + ).hits.hits.map((hit) => hit._source) as ApmAlertFields[]; + expect(alerts[0]['kibana.alert.reason']).to.be( 'Error count is 30 in the last 1 hr for service: opbeans-php, env: production, name: tx-php, error key: 000000000000000000000a php error, error name: a php error. Alert when > 1.' ); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts deleted file mode 100644 index 27cc2b840d12b..0000000000000 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - * 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 { Client, errors } from '@elastic/elasticsearch'; -import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; -import pRetry from 'p-retry'; -import { ApmRuleType } from '@kbn/rule-data-utils'; -import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema'; -import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; -import type { ApmApiClient } from '../../../../../services/apm_api'; -import type { SupertestWithRoleScopeType } from '../../../../../services'; - -export const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-*'; -export const APM_ACTION_VARIABLE_INDEX = 'apm-index-connector-test'; - -export async function createApmRule({ - supertest, - name, - ruleTypeId, - params, - actions = [], -}: { - supertest: SupertestWithRoleScopeType; - ruleTypeId: T; - name: string; - params: ApmRuleParamsType[T]; - actions?: any[]; -}) { - try { - const { body } = await supertest.post(`/api/alerting/rule`).send({ - params, - consumer: 'apm', - schedule: { - interval: '1m', - }, - tags: ['apm'], - name, - rule_type_id: ruleTypeId, - actions, - }); - return body; - } catch (error: any) { - throw new Error(`[Rule] Creating a rule failed: ${error}`); - } -} - -function getTimerange() { - return { - start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - end: new Date(Date.now() + 5 * 60 * 1000).toISOString(), - }; -} - -export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) { - const timerange = getTimerange(); - const serviceInventoryResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - ...timerange, - environment: 'ENVIRONMENT_ALL', - kuery: '', - probability: 1, - documentType: ApmDocumentType.ServiceTransactionMetric, - rollupInterval: RollupInterval.SixtyMinutes, - useDurationSummary: true, - }, - }, - }); - - return serviceInventoryResponse.body.items.reduce>((acc, item) => { - return { ...acc, [item.serviceName]: item.alertsCount ?? 0 }; - }, {}); -} - -export async function fetchServiceTabAlertCount({ - apmApiClient, - serviceName, -}: { - apmApiClient: ApmApiClient; - serviceName: string; -}) { - const timerange = getTimerange(); - const alertsCountReponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count', - params: { - path: { - serviceName, - }, - query: { - ...timerange, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - - return alertsCountReponse.body.alertsCount; -} - -export async function runRuleSoon({ - ruleId, - supertest, -}: { - ruleId: string; - supertest: SupertestWithRoleScopeType; -}): Promise> { - return pRetry( - async () => { - try { - const response = await supertest.post(`/internal/alerting/rule/${ruleId}/_run_soon`); - // Sometimes the rule may already be running, which returns a 200. Try until it isn't - if (response.status !== 204) { - throw new Error(`runRuleSoon got ${response.status} status`); - } - return response; - } catch (error) { - throw new Error(`[Rule] Running a rule ${ruleId} failed: ${error}`); - } - }, - { retries: 10 } - ); -} - -export async function deleteAlertsByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { - await es.deleteByQuery({ - index: APM_ALERTS_INDEX, - query: { term: { 'kibana.alert.rule.uuid': ruleId } }, - }); -} - -export async function deleteRuleById({ - supertest, - ruleId, -}: { - supertest: SupertestWithRoleScopeType; - ruleId: string; -}) { - await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo'); -} - -export async function deleteApmRules(supertest: SupertestWithRoleScopeType) { - const res = await supertest.get( - `/api/alerting/rules/_find?filter=alert.attributes.consumer:apm&per_page=10000` - ); - - return Promise.all( - res.body.data.map((rule: any) => deleteRuleById({ supertest, ruleId: rule.id })) - ); -} - -export function deleteApmAlerts(es: Client) { - return es.deleteByQuery({ - index: APM_ALERTS_INDEX, - conflicts: 'proceed', - query: { match_all: {} }, - }); -} - -export async function clearKibanaApmEventLog(es: Client) { - return es.deleteByQuery({ - index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'apm' } }, - }); -} - -export type ApmAlertFields = ParsedTechnicalFields & ObservabilityApmAlert; - -export async function createIndexConnector({ - supertest, - name, -}: { - supertest: SupertestWithRoleScopeType; - name: string; -}) { - const { body } = await supertest.post(`/api/actions/connector`).send({ - name, - config: { - index: APM_ACTION_VARIABLE_INDEX, - refresh: true, - }, - connector_type_id: '.index', - }); - - return body.id as string; -} - -export function getIndexAction({ - actionId, - actionVariables, -}: { - actionId: string; - actionVariables: Array<{ name: string }>; -}) { - return { - group: 'threshold_met', - id: actionId, - params: { - documents: [ - actionVariables.reduce>((acc, actionVariable) => { - acc[actionVariable.name] = `{{context.${actionVariable.name}}}`; - return acc; - }, {}), - ], - }, - frequency: { - notify_when: 'onActionGroupChange', - throttle: null, - summary: false, - }, - }; -} - -export async function deleteAllActionConnectors({ - supertest, - es, -}: { - supertest: SupertestWithRoleScopeType; - es: Client; -}): Promise { - const res = await supertest.get(`/api/actions/connectors`); - - const body = res.body as Array<{ id: string; connector_type_id: string; name: string }>; - return Promise.all( - body.map(({ id }) => { - return deleteActionConnector({ supertest, actionId: id }); - }) - ); -} - -async function deleteActionConnector({ - supertest, - actionId, -}: { - supertest: SupertestWithRoleScopeType; - actionId: string; -}) { - return supertest.delete(`/api/actions/connector/${actionId}`); -} - -export async function deleteActionConnectorIndex(es: Client) { - try { - await es.indices.delete({ index: APM_ACTION_VARIABLE_INDEX }); - } catch (e) { - if (e instanceof errors.ResponseError && e.statusCode === 404) { - return; - } - - throw e; - } -} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_helper.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_helper.ts new file mode 100644 index 0000000000000..e5dcd4a45ac3b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_helper.ts @@ -0,0 +1,96 @@ +/* + * 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 { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; +import type { ApmApiClient } from '../../../../../services/apm_api'; + +export const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-*'; +export const APM_ACTION_VARIABLE_INDEX = 'apm-index-connector-test'; + +function getTimerange() { + return { + start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + end: new Date(Date.now() + 5 * 60 * 1000).toISOString(), + }; +} + +export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) { + const timerange = getTimerange(); + const serviceInventoryResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + kuery: '', + probability: 1, + documentType: ApmDocumentType.ServiceTransactionMetric, + rollupInterval: RollupInterval.SixtyMinutes, + useDurationSummary: true, + }, + }, + }); + + return serviceInventoryResponse.body.items.reduce>((acc, item) => { + return { ...acc, [item.serviceName]: item.alertsCount ?? 0 }; + }, {}); +} + +export async function fetchServiceTabAlertCount({ + apmApiClient, + serviceName, +}: { + apmApiClient: ApmApiClient; + serviceName: string; +}) { + const timerange = getTimerange(); + const alertsCountReponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count', + params: { + path: { + serviceName, + }, + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + return alertsCountReponse.body.alertsCount; +} + +export function getIndexAction({ + actionId, + actionVariables, +}: { + actionId: string; + actionVariables: Array<{ name: string }>; +}) { + return { + group: 'threshold_met', + id: actionId, + params: { + documents: [ + actionVariables.reduce>((acc, actionVariable) => { + acc[actionVariable.name] = `{{context.${actionVariable.name}}}`; + return acc; + }, {}), + ], + }, + frequency: { + notify_when: 'onActionGroupChange', + throttle: null, + summary: false, + }, + }; +} + +export type ApmAlertFields = ParsedTechnicalFields & ObservabilityApmAlert; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts deleted file mode 100644 index b94c2bcc52ee3..0000000000000 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { Client } from '@elastic/elasticsearch'; -import { ToolingLog } from '@kbn/tooling-log'; -import type { SupertestWithRoleScopeType } from '../../../../../services'; -import { - clearKibanaApmEventLog, - deleteApmRules, - deleteApmAlerts, - deleteActionConnectorIndex, - deleteAllActionConnectors, -} from './alerting_api_helper'; - -export async function cleanupRuleAndAlertState({ - es, - supertest, - logger, -}: { - es: Client; - supertest: SupertestWithRoleScopeType; - logger: ToolingLog; -}) { - try { - await Promise.all([ - deleteApmRules(supertest), - deleteApmAlerts(es), - clearKibanaApmEventLog(es), - deleteActionConnectorIndex(es), - deleteAllActionConnectors({ supertest, es }), - ]); - } catch (e) { - logger.error(`An error occured while cleaning up the state: ${e}`); - } -} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts deleted file mode 100644 index 989875522c720..0000000000000 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { ToolingLog } from '@kbn/tooling-log'; -import pRetry from 'p-retry'; -import type { SupertestWithRoleScopeType } from '../../../../../services'; - -const RETRIES_COUNT = 10; - -export async function waitForActiveRule({ - ruleId, - supertest, - logger, -}: { - ruleId: string; - supertest: SupertestWithRoleScopeType; - logger?: ToolingLog; -}): Promise> { - return pRetry( - async () => { - const response = await supertest.get(`/api/alerting/rule/${ruleId}`); - const status = response.body?.execution_status?.status; - const expectedStatus = 'active'; - - if (status !== expectedStatus) { - throw new Error(`Expected: ${expectedStatus}: got ${status}`); - } - - return status; - }, - { - retries: RETRIES_COUNT, - onFailedAttempt: (error) => { - if (logger) { - logger.info(`Attempt ${error.attemptNumber}/${RETRIES_COUNT}: Waiting for active rule`); - } - }, - } - ); -} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts deleted file mode 100644 index 334631b354cd1..0000000000000 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 { - AggregationsAggregate, - SearchResponse, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import pRetry from 'p-retry'; -import { ApmAlertFields, APM_ALERTS_INDEX } from './alerting_api_helper'; - -async function getAlertByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { - const response = (await es.search({ - index: APM_ALERTS_INDEX, - body: { - query: { - term: { - 'kibana.alert.rule.uuid': ruleId, - }, - }, - }, - })) as SearchResponse>; - - return response.hits.hits.map((hit) => hit._source) as ApmAlertFields[]; -} - -export async function waitForAlertsForRule({ - es, - ruleId, - minimumAlertCount = 1, -}: { - es: Client; - ruleId: string; - minimumAlertCount?: number; -}) { - return pRetry( - async () => { - const alerts = await getAlertByRuleId({ es, ruleId }); - const actualAlertCount = alerts.length; - if (actualAlertCount < minimumAlertCount) { - throw new Error(`Expected ${minimumAlertCount} but got ${actualAlertCount} alerts`); - } - - return alerts; - }, - { retries: 5 } - ); -} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts index c23baa32c17dc..e82168820a90d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts @@ -12,27 +12,23 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { SupertestWithRoleScopeType } from '../../../../services'; +import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { - createApmRule, fetchServiceInventoryAlertCounts, fetchServiceTabAlertCount, ApmAlertFields, - createIndexConnector, getIndexAction, -} from './helpers/alerting_api_helper'; -import { cleanupRuleAndAlertState } from './helpers/cleanup_rule_and_alert_state'; -import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; -import { waitForActiveRule } from './helpers/wait_for_active_rule'; -import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results'; + APM_ACTION_VARIABLE_INDEX, + APM_ALERTS_INDEX, +} from './helpers/alerting_helper'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const roleScopedSupertest = getService('roleScopedSupertest'); - const es = getService('es'); - const logger = getService('log'); const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); + const alertingApi = getService('alertingApi'); + const samlAuth = getService('samlAuth'); const ruleParams = { threshold: 3000, @@ -48,12 +44,16 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('transaction duration alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; let supertest: SupertestWithRoleScopeType; + let roleAuthc: RoleCredentials; before(async () => { - supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + supertest = await roleScopedSupertest.getSupertestWithRoleScope('viewer', { withInternalHeaders: true, + useCookieHeader: true, }); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) .instance('instance'); @@ -84,6 +84,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); await supertest.destroy(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); describe('create rule for opbeans-java without kql filter', () => { @@ -92,31 +93,55 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let alerts: ApmAlertFields[]; before(async () => { - actionId = await createIndexConnector({ supertest, name: 'Transation duration' }); + actionId = await alertingApi.createIndexConnector({ + name: 'Transation duration', + indexName: APM_ACTION_VARIABLE_INDEX, + roleAuthc, + }); const indexAction = getIndexAction({ actionId, actionVariables: transactionDurationActionVariables, }); - const createdRule = await createApmRule({ - supertest, + const createdRule = await alertingApi.createRule({ ruleTypeId: ApmRuleType.TransactionDuration, name: 'Apm transaction duration without kql filter', + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], params: { ...ruleParams, }, actions: [indexAction], + roleAuthc, }); ruleId = createdRule.id; - alerts = await waitForAlertsForRule({ es, ruleId }); + alerts = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ALERTS_INDEX, + ruleId, + }) + ).hits.hits.map((hit) => hit._source) as ApmAlertFields[]; }); - after(async () => { - await cleanupRuleAndAlertState({ es, supertest, logger }); - }); + after(() => + alertingApi.cleanUpAlerts({ + roleAuthc, + ruleId, + alertIndexName: APM_ALERTS_INDEX, + connectorIndexName: APM_ACTION_VARIABLE_INDEX, + consumer: 'apm', + }) + ); it('checks if rule is active', async () => { - const ruleStatus = await waitForActiveRule({ ruleId, supertest }); + const ruleStatus = await alertingApi.waitForRuleStatus({ + ruleId, + roleAuthc, + expectedStatus: 'active', + }); expect(ruleStatus).to.be('active'); }); @@ -124,7 +149,11 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let results: Array>; before(async () => { - results = await waitForIndexConnectorResults({ es }); + results = results = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ACTION_VARIABLE_INDEX, + }) + ).hits.hits.map((hit) => hit._source) as Array>; }); it('populates the action connector index with every action variable', async () => { @@ -207,10 +236,14 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let alerts: ApmAlertFields[]; beforeEach(async () => { - const createdRule = await createApmRule({ - supertest, + const createdRule = await alertingApi.createRule({ ruleTypeId: ApmRuleType.TransactionDuration, name: 'Apm transaction duration with kql filter', + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], params: { searchConfiguration: { query: { @@ -222,17 +255,33 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon ...ruleParams, }, actions: [], + roleAuthc, }); ruleId = createdRule.id; - alerts = await waitForAlertsForRule({ es, ruleId }); + alerts = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ALERTS_INDEX, + ruleId, + }) + ).hits.hits.map((hit) => hit._source) as ApmAlertFields[]; }); - afterEach(async () => { - await cleanupRuleAndAlertState({ es, supertest, logger }); - }); + afterEach(() => + alertingApi.cleanUpAlerts({ + roleAuthc, + ruleId, + alertIndexName: APM_ALERTS_INDEX, + connectorIndexName: APM_ACTION_VARIABLE_INDEX, + consumer: 'apm', + }) + ); it('checks if rule is active', async () => { - const ruleStatus = await waitForActiveRule({ ruleId, supertest }); + const ruleStatus = await alertingApi.waitForRuleStatus({ + ruleId, + roleAuthc, + expectedStatus: 'active', + }); expect(ruleStatus).to.be('active'); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts index e3f3bde6e6bf8..538232884f729 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts @@ -11,37 +11,37 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { SupertestWithRoleScopeType } from '../../../../services'; +import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { - createApmRule, fetchServiceInventoryAlertCounts, fetchServiceTabAlertCount, ApmAlertFields, getIndexAction, - createIndexConnector, -} from './helpers/alerting_api_helper'; -import { cleanupRuleAndAlertState } from './helpers/cleanup_rule_and_alert_state'; -import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; -import { waitForActiveRule } from './helpers/wait_for_active_rule'; -import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results'; + APM_ACTION_VARIABLE_INDEX, + APM_ALERTS_INDEX, +} from './helpers/alerting_helper'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const roleScopedSupertest = getService('roleScopedSupertest'); - const es = getService('es'); - const logger = getService('log'); const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); + const alertingApi = getService('alertingApi'); + const samlAuth = getService('samlAuth'); describe('transaction error rate alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; let supertest: SupertestWithRoleScopeType; + let roleAuthc: RoleCredentials; before(async () => { - supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + supertest = await roleScopedSupertest.getSupertestWithRoleScope('viewer', { withInternalHeaders: true, + useCookieHeader: true, }); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) .instance('instance'); @@ -82,6 +82,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); await supertest.destroy(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); describe('create rule without kql query', () => { @@ -90,16 +91,25 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let alerts: ApmAlertFields[]; before(async () => { - actionId = await createIndexConnector({ supertest, name: 'Transation error rate' }); + actionId = await alertingApi.createIndexConnector({ + name: 'Transation error rate', + indexName: APM_ACTION_VARIABLE_INDEX, + roleAuthc, + }); + const indexAction = getIndexAction({ actionId, actionVariables: transactionErrorRateActionVariables, }); - const createdRule = await createApmRule({ - supertest, + const createdRule = await alertingApi.createRule({ ruleTypeId: ApmRuleType.TransactionErrorRate, name: 'Apm transaction error rate without kql query', + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], params: { threshold: 40, windowSize: 5, @@ -115,17 +125,33 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon ], }, actions: [indexAction], + roleAuthc, }); ruleId = createdRule.id; - alerts = await waitForAlertsForRule({ es, ruleId }); + alerts = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ALERTS_INDEX, + ruleId, + }) + ).hits.hits.map((hit) => hit._source) as ApmAlertFields[]; }); - after(async () => { - await cleanupRuleAndAlertState({ es, supertest, logger }); - }); + after(() => + alertingApi.cleanUpAlerts({ + roleAuthc, + ruleId, + alertIndexName: APM_ALERTS_INDEX, + connectorIndexName: APM_ACTION_VARIABLE_INDEX, + consumer: 'apm', + }) + ); it('checks if rule is active', async () => { - const ruleStatus = await waitForActiveRule({ ruleId, supertest }); + const ruleStatus = await alertingApi.waitForRuleStatus({ + ruleId, + roleAuthc, + expectedStatus: 'active', + }); expect(ruleStatus).to.be('active'); }); @@ -133,7 +159,11 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let results: Array>; before(async () => { - results = await waitForIndexConnectorResults({ es }); + results = results = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ACTION_VARIABLE_INDEX, + }) + ).hits.hits.map((hit) => hit._source) as Array>; }); it('has the right keys', async () => { @@ -216,10 +246,14 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon let alerts: ApmAlertFields[]; beforeEach(async () => { - const createdRule = await createApmRule({ - supertest, + const createdRule = await alertingApi.createRule({ ruleTypeId: ApmRuleType.TransactionErrorRate, name: 'Apm transaction error rate without kql query', + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], params: { threshold: 40, windowSize: 5, @@ -242,14 +276,26 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon ], }, actions: [], + roleAuthc, }); ruleId = createdRule.id; - alerts = await waitForAlertsForRule({ es, ruleId }); + alerts = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ALERTS_INDEX, + ruleId, + }) + ).hits.hits.map((hit) => hit._source) as ApmAlertFields[]; }); - afterEach(async () => { - await cleanupRuleAndAlertState({ es, supertest, logger }); - }); + afterEach(() => + alertingApi.cleanUpAlerts({ + roleAuthc, + ruleId, + alertIndexName: APM_ALERTS_INDEX, + connectorIndexName: APM_ACTION_VARIABLE_INDEX, + consumer: 'apm', + }) + ); it('indexes alert document with all group-by fields', async () => { expect(alerts[0]).property('service.name', 'opbeans-node'); diff --git a/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts index 2956ee412a478..dd09804b5da83 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts @@ -11,8 +11,9 @@ import type { } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { MetricThresholdParams } from '@kbn/infra-plugin/common/alerting/metrics'; import { ThresholdParams } from '@kbn/observability-plugin/common/custom_threshold_rule/types'; +import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema'; import { RoleCredentials } from '@kbn/ftr-common-functional-services'; -import type { Client } from '@elastic/elasticsearch'; +import { errors, type Client } from '@elastic/elasticsearch'; import type { TryWithRetriesOptions } from '@kbn/ftr-common-functional-services'; import { v4 as uuidv4 } from 'uuid'; import moment from 'moment'; @@ -613,17 +614,6 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide return body; }, - async findRuleById(roleAuthc: RoleCredentials, ruleId: string) { - if (!ruleId) { - throw new Error(`'ruleId' is undefined`); - } - const response = await supertestWithoutAuth - .get(`/api/alerting/rule/${ruleId}`) - .set(samlAuth.getInternalRequestHeader()) - .set(roleAuthc.apiKeyHeader); - return response.body || {}; - }, - waiting: { async waitForDocumentInIndex({ esClient, @@ -948,7 +938,6 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide return { helpers, - async waitForRuleStatus({ ruleId, expectedStatus, @@ -976,14 +965,27 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide async waitForDocumentInIndex({ indexName, docCountTarget = 1, + ruleId, }: { indexName: string; docCountTarget?: number; + ruleId?: string; }): Promise>> { return await retry.tryForTime(retryTimeout, async () => { const response = await es.search({ index: indexName, rest_total_hits_as_int: true, + ...(ruleId + ? { + body: { + query: { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + }, + } + : {}), }); logger.debug(`Found ${response.hits.total} docs, looking for at least ${docCountTarget}.`); @@ -1064,7 +1066,15 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide }: { ruleTypeId: string; name: string; - params: MetricThresholdParams | ThresholdParams | SloBurnRateRuleParams; + params: + | CreateEsQueryRuleParams + | MetricThresholdParams + | ThresholdParams + | SloBurnRateRuleParams + | ApmRuleParamsType['apm.anomaly'] + | ApmRuleParamsType['apm.error_rate'] + | ApmRuleParamsType['apm.transaction_duration'] + | ApmRuleParamsType['apm.transaction_error_rate']; actions?: any[]; tags?: any[]; schedule?: { interval: string }; @@ -1096,5 +1106,103 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide .set(samlAuth.getInternalRequestHeader()); return response.body.data.find((obj: any) => obj.id === ruleId); }, + + async searchRules(roleAuthc: RoleCredentials, filter: string) { + return supertestWithoutAuth + .get('/api/alerting/rules/_find') + .query({ filter }) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + }, + + async deleteRuleById({ roleAuthc, ruleId }: { roleAuthc: RoleCredentials; ruleId: string }) { + return supertestWithoutAuth + .delete(`/api/alerting/rule/${ruleId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + }, + + async deleteRules({ roleAuthc, filter }: { roleAuthc: RoleCredentials; filter: string }) { + const response = await this.searchRules(roleAuthc, filter); + return Promise.all( + response.body.data.map((rule: any) => this.deleteRuleById({ roleAuthc, ruleId: rule.id })) + ); + }, + + async deleteAllActionConnectors({ roleAuthc }: { roleAuthc: RoleCredentials }): Promise { + const res = await supertestWithoutAuth + .get(`/api/actions/connectors`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + + const body = res.body as Array<{ id: string; connector_type_id: string; name: string }>; + return Promise.all( + body.map(({ id }) => { + return this.deleteActionConnector({ + roleAuthc, + actionId: id, + }); + }) + ); + }, + + async deleteActionConnector({ + roleAuthc, + actionId, + }: { + roleAuthc: RoleCredentials; + actionId: string; + }) { + return supertestWithoutAuth + .delete(`/api/actions/connector/${actionId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + }, + + async cleanUpAlerts({ + roleAuthc, + ruleId, + consumer, + alertIndexName, + connectorIndexName, + }: { + roleAuthc: RoleCredentials; + ruleId: string; + consumer?: string; + alertIndexName?: string; + connectorIndexName?: string; + }) { + return Promise.allSettled([ + // Delete the rule by ID + this.deleteRuleById({ roleAuthc, ruleId }), + // Delete all documents in the alert index if specified + alertIndexName + ? es.deleteByQuery({ + index: alertIndexName, + conflicts: 'proceed', + query: { match_all: {} }, + }) + : Promise.resolve(), + // Delete event logs for the specified consumer if provided + consumer + ? es.deleteByQuery({ + index: '.kibana-event-log-*', + query: { term: { 'kibana.alert.rule.consumer': consumer } }, + }) + : Promise.resolve(), + // Delete connector index if provided + connectorIndexName + ? es.indices.delete({ index: connectorIndexName }).catch((e) => { + if (e instanceof errors.ResponseError && e.statusCode === 404) { + return; + } + + throw e; + }) + : Promise.resolve(), + // Delete all action connectors + this.deleteAllActionConnectors({ roleAuthc }), + ]); + }, }; } diff --git a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts index 61bdfd97d2254..e88115389f585 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts @@ -12,7 +12,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { range } from 'lodash'; import { ML_ANOMALY_SEVERITY } from '@kbn/ml-anomaly-utils/anomaly_severity'; -import { waitForAlertsForRule } from '../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule'; +import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; import { waitForActiveRule } from './helpers/wait_for_active_rule'; import { createApmRule } from './helpers/alerting_api_helper'; import { cleanupRuleAndAlertState } from './helpers/cleanup_rule_and_alert_state'; diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts index 4ae1085eea053..86544981bbdb4 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts @@ -5,16 +5,17 @@ * 2.0. */ -import { Client } from '@elastic/elasticsearch'; +import { Client, errors } from '@elastic/elasticsearch'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import pRetry from 'p-retry'; import type { Agent as SuperTestAgent } from 'supertest'; import { ApmRuleType } from '@kbn/rule-data-utils'; import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema'; import { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; - -export const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-*'; -export const APM_ACTION_VARIABLE_INDEX = 'apm-index-connector-test'; +import { + APM_ACTION_VARIABLE_INDEX, + APM_ALERTS_INDEX, +} from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_helper'; export async function createApmRule({ supertest, @@ -76,6 +77,13 @@ export async function runRuleSoon({ ); } +export async function deleteAlertsByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { + await es.deleteByQuery({ + index: APM_ALERTS_INDEX, + query: { term: { 'kibana.alert.rule.uuid': ruleId } }, + }); +} + export async function deleteRuleById({ supertest, ruleId, @@ -113,6 +121,21 @@ export async function deleteAllActionConnectors({ ); } +export function deleteApmAlerts(es: Client) { + return es.deleteByQuery({ + index: APM_ALERTS_INDEX, + conflicts: 'proceed', + query: { match_all: {} }, + }); +} + +export async function clearKibanaApmEventLog(es: Client) { + return es.deleteByQuery({ + index: '.kibana-event-log-*', + query: { term: { 'kibana.alert.rule.consumer': 'apm' } }, + }); +} + export type ApmAlertFields = ParsedTechnicalFields & ObservabilityApmAlert; async function deleteActionConnector({ @@ -124,3 +147,15 @@ async function deleteActionConnector({ }) { return supertest.delete(`/api/actions/connector/${actionId}`).set('kbn-xsrf', 'foo'); } + +export async function deleteActionConnectorIndex(es: Client) { + try { + await es.indices.delete({ index: APM_ACTION_VARIABLE_INDEX }); + } catch (e) { + if (e instanceof errors.ResponseError && e.statusCode === 404) { + return; + } + + throw e; + } +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts index c3cce2d498c87..b41e59cd4b774 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts @@ -12,7 +12,7 @@ import { deleteActionConnectorIndex, clearKibanaApmEventLog, deleteApmAlerts, -} from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper'; +} from './alerting_api_helper'; import { deleteApmRules, deleteAllActionConnectors } from './alerting_api_helper'; export async function cleanupRuleAndAlertState({ diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_active_apm_alerts.ts similarity index 93% rename from x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts.ts rename to x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_active_apm_alerts.ts index 9f83b4850dd40..e342e31ee0fa3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_active_apm_alerts.ts @@ -5,9 +5,9 @@ * 2.0. */ import type { Client } from '@elastic/elasticsearch'; -import pRetry from 'p-retry'; import { ToolingLog } from '@kbn/tooling-log'; -import { APM_ALERTS_INDEX } from './alerting_api_helper'; +import pRetry from 'p-retry'; +import { APM_ALERTS_INDEX } from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_helper'; export async function getActiveApmAlerts({ ruleId, diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_alerts_for_rule.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_alerts_for_rule.ts index 334631b354cd1..28437e4edbc62 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_alerts_for_rule.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_alerts_for_rule.ts @@ -11,7 +11,10 @@ import type { SearchResponse, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import pRetry from 'p-retry'; -import { ApmAlertFields, APM_ALERTS_INDEX } from './alerting_api_helper'; +import { + APM_ALERTS_INDEX, + ApmAlertFields, +} from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_helper'; async function getAlertByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { const response = (await es.search({ diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_index_connector_results.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_index_connector_results.ts similarity index 85% rename from x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_index_connector_results.ts rename to x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_index_connector_results.ts index 9646289420804..b6634e3a33f2c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_index_connector_results.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_index_connector_results.ts @@ -7,7 +7,7 @@ import { Client } from '@elastic/elasticsearch'; import pRetry from 'p-retry'; -import { APM_ACTION_VARIABLE_INDEX } from './alerting_api_helper'; +import { APM_ACTION_VARIABLE_INDEX } from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_helper'; async function getIndexConnectorResults(es: Client) { const res = await es.search({ index: APM_ACTION_VARIABLE_INDEX }); diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts index 93c63875e28b8..d39724b0570b2 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts @@ -7,7 +7,7 @@ import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { ApmRuleType } from '@kbn/rule-data-utils'; import expect from '@kbn/expect'; -import { waitForActiveApmAlert } from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts'; +import { waitForActiveApmAlert } from '../../alerts/helpers/wait_for_active_apm_alerts'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { createApmRule } from '../../alerts/helpers/alerting_api_helper'; import { cleanupRuleAndAlertState } from '../../alerts/helpers/cleanup_rule_and_alert_state'; diff --git a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts index 958c2ed88f460..e3324546c84d5 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts @@ -8,10 +8,10 @@ import expect from '@kbn/expect'; import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { ApmRuleType } from '@kbn/rule-data-utils'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import { waitForAlertsForRule } from '../alerts/helpers/wait_for_alerts_for_rule'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createApmRule, runRuleSoon, ApmAlertFields } from '../alerts/helpers/alerting_api_helper'; import { waitForActiveRule } from '../alerts/helpers/wait_for_active_rule'; -import { waitForAlertsForRule } from '../alerts/helpers/wait_for_alerts_for_rule'; import { cleanupRuleAndAlertState } from '../alerts/helpers/cleanup_rule_and_alert_state'; export default function ServiceAlerts({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_alerts.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_alerts.spec.ts index 6c009682e1421..f6b2c7c3a74a7 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_alerts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_alerts.spec.ts @@ -13,10 +13,10 @@ import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { ApmRuleType } from '@kbn/rule-data-utils'; +import { waitForAlertsForRule } from '../alerts/helpers/wait_for_alerts_for_rule'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createApmRule, runRuleSoon, ApmAlertFields } from '../alerts/helpers/alerting_api_helper'; import { waitForActiveRule } from '../alerts/helpers/wait_for_active_rule'; -import { waitForAlertsForRule } from '../alerts/helpers/wait_for_alerts_for_rule'; import { cleanupRuleAndAlertState } from '../alerts/helpers/cleanup_rule_and_alert_state'; type TransactionsGroupsMainStatistics = From 18e8defdce8f91fc10c0e8a1a8f50a520e6634c1 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 11 Nov 2024 13:28:09 +0100 Subject: [PATCH 7/9] CODEOWNERs update --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 73a670d145347..e3148bff7a2ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1190,6 +1190,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/plugins/observability_solution/infra/server/usage @elastic/obs-ux-infra_services-team /x-pack/plugins/observability_solution/infra/server/utils @elastic/obs-ux-infra_services-team /x-pack/test/api_integration/deployment_agnostic/apis/observability/infra @elastic/obs-ux-logs-team +/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm @elastic/obs-ux-logs-team ## Logs UI code exceptions -> @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_stream_log_file.ts @elastic/obs-ux-logs-team From 5d475f151151463aabae24567818e416dba44098 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 11 Nov 2024 14:42:23 +0100 Subject: [PATCH 8/9] Rename supertest --- .../apm/alerts/error_count_threshold.spec.ts | 17 ++++++++++------- .../apm/alerts/transaction_duration.spec.ts | 15 +++++++++------ .../apm/alerts/transaction_error_rate.spec.ts | 15 +++++++++------ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts index 4e3164ebe5970..65a22337349c6 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts @@ -31,7 +31,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('error count threshold alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertestWithRoleScope: SupertestWithRoleScopeType; + let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; let roleAuthc: RoleCredentials; const javaErrorMessage = 'a java error'; @@ -52,12 +52,15 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }; before(async () => { - supertestWithRoleScope = await roleScopedSupertest.getSupertestWithRoleScope('viewer', { - withInternalHeaders: true, - useCookieHeader: true, - }); + supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'viewer', + { + withInternalHeaders: true, + useCookieHeader: true, + } + ); - roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('editor'); const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) @@ -113,7 +116,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertestWithRoleScope.destroy(); + await supertestViewerWithCookieCredentials.destroy(); await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts index e82168820a90d..0cd3446359557 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts @@ -43,14 +43,17 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('transaction duration alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertest: SupertestWithRoleScopeType; + let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; let roleAuthc: RoleCredentials; before(async () => { - supertest = await roleScopedSupertest.getSupertestWithRoleScope('viewer', { - withInternalHeaders: true, - useCookieHeader: true, - }); + supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'viewer', + { + withInternalHeaders: true, + useCookieHeader: true, + } + ); roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); @@ -83,7 +86,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertest.destroy(); + await supertestViewerWithCookieCredentials.destroy(); await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts index 538232884f729..e538ff0e6a3ba 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts @@ -31,14 +31,17 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('transaction error rate alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertest: SupertestWithRoleScopeType; + let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; let roleAuthc: RoleCredentials; before(async () => { - supertest = await roleScopedSupertest.getSupertestWithRoleScope('viewer', { - withInternalHeaders: true, - useCookieHeader: true, - }); + supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'viewer', + { + withInternalHeaders: true, + useCookieHeader: true, + } + ); roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); @@ -81,7 +84,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertest.destroy(); + await supertestViewerWithCookieCredentials.destroy(); await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); From 3a56a72fc4d7ebff7f10fb9cd4a886b22e25ae21 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 11 Nov 2024 17:30:43 +0100 Subject: [PATCH 9/9] revert role change --- .../apis/observability/apm/alerts/error_count_threshold.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts index 65a22337349c6..16493e8220f68 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts @@ -60,7 +60,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon } ); - roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('editor'); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' })