From 1169b19a690f437a3dc5638ab5b159a70cb59ead Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 12 Nov 2024 12:01:23 +0100 Subject: [PATCH] [APM] Migrate apm alerts tests to deployment agnostic (#199097) ## Summary Closes [#198959](https://github.com/elastic/kibana/issues/198959) Part of https://github.com/elastic/kibana/issues/193245 This PR contains the changes to migrate `alerts` test folder to Deployment-agnostic testing strategy. It also fixes a bug when filtering alerts by `kibana.alert.rule.producer`. On serverless ,the producer is `observability`, not `apm` ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` It's recommended to be run against [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) - This PR won't pass on MKI while the instance is not created using the `kibana.alert.rule.producer` fix from this PR - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 1 + .../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/agent_explorer/agent_explorer.spec.ts | 2 +- .../latest_agent_versions.spec.ts | 2 +- .../apm}/alerts/error_count_threshold.spec.ts | 163 +++-- .../apm}/alerts/generate_data.ts | 0 .../apm/alerts/helpers/alerting_helper.ts | 96 +++ .../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 | 141 ++-- .../alerts/transaction_error_rate.spec.ts | 138 ++-- .../apis/observability/apm/index.ts | 1 + .../services/alerting_api.ts | 136 +++- .../deployment_agnostic/services/apm_api.ts | 2 + .../tests/alerts/anomaly_alert.spec.ts | 8 +- .../alerts/helpers/alerting_api_helper.ts | 145 +---- .../helpers/cleanup_rule_and_alert_state.ts | 5 +- .../helpers/wait_for_active_apm_alerts.ts | 4 +- .../helpers/wait_for_alerts_for_rule.ts | 5 +- .../wait_for_index_connector_results.ts | 2 +- .../alerts/preview_chart_error_rate.spec.ts | 599 ----------------- ...preview_chart_transaction_duration.spec.ts | 556 ---------------- .../service_group_count.spec.ts | 2 +- .../tests/services/service_alerts.spec.ts | 2 +- .../transactions_groups_alerts.spec.ts | 2 +- 30 files changed, 2046 insertions(+), 1670 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/error_count_threshold.spec.ts (67%) 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_helper.ts 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 (67%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/transaction_error_rate.spec.ts (69%) 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/.github/CODEOWNERS b/.github/CODEOWNERS index abc6749d52ea9..ef265cf7c569a 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 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/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/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 acc31a3743da8..aa099ca66d23b 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', }); } 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 67% 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..16493e8220f68 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,29 +10,30 @@ 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 { 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'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('supertest'); - const es = getService('es'); - const logger = getService('log'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - registry.when('error count threshold alert', { config: 'basic', archives: [] }, () => { + APM_ACTION_VARIABLE_INDEX, + APM_ALERTS_INDEX, +} from './helpers/alerting_helper'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); + 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 supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; + let roleAuthc: RoleCredentials; + const javaErrorMessage = 'a java error'; const phpErrorMessage = 'a php error'; @@ -50,7 +51,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { ], }; - before(() => { + before(async () => { + supertestViewerWithCookieCredentials = 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'); @@ -95,13 +106,19 @@ 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 supertestViewerWithCookieCredentials.destroy(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); describe('create rule without kql filter', () => { let ruleId: string; @@ -109,31 +126,58 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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'); }); @@ -141,7 +185,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { let results: Array>; before(async () => { - 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 () => { @@ -151,6 +206,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); + it('checks if rule is active', async () => { + const ruleStatus = await alertingApi.waitForRuleStatus({ + ruleId, + roleAuthc, + expectedStatus: 'active', + }); + 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 +234,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 +245,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'); }); }); @@ -255,30 +322,48 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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/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_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/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 67% 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..0cd3446359557 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,27 +11,24 @@ 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 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'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('supertest'); - const es = getService('es'); - const logger = getService('log'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); + APM_ACTION_VARIABLE_INDEX, + APM_ALERTS_INDEX, +} from './helpers/alerting_helper'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + const alertingApi = getService('alertingApi'); + const samlAuth = getService('samlAuth'); const ruleParams = { threshold: 3000, @@ -44,8 +41,22 @@ 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 supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; + let roleAuthc: RoleCredentials; + + before(async () => { + supertestViewerWithCookieCredentials = 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'); @@ -68,11 +79,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { .success(), ]; }); + + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); return apmSynthtraceEsClient.index(events); }); after(async () => { await apmSynthtraceEsClient.clean(); + await supertestViewerWithCookieCredentials.destroy(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); describe('create rule for opbeans-java without kql filter', () => { @@ -81,31 +96,55 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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'); }); @@ -113,7 +152,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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 () => { @@ -133,7 +176,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 +186,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'); }); }); @@ -192,10 +239,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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: { @@ -207,17 +258,33 @@ export default function ApiTest({ getService }: FtrProviderContext) { ...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/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 69% 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..e538ff0e6a3ba 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,30 +10,41 @@ 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 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'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('supertest'); - const es = getService('es'); - const logger = getService('log'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - registry.when('transaction error rate alert', { config: 'basic', archives: [] }, () => { - before(() => { + APM_ACTION_VARIABLE_INDEX, + APM_ALERTS_INDEX, +} from './helpers/alerting_helper'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); + 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 supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; + let roleAuthc: RoleCredentials; + + before(async () => { + supertestViewerWithCookieCredentials = 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'); @@ -66,11 +77,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { .success(), ]; }); + + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); return apmSynthtraceEsClient.index(events); }); after(async () => { await apmSynthtraceEsClient.clean(); + await supertestViewerWithCookieCredentials.destroy(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); describe('create rule without kql query', () => { @@ -79,16 +94,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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, @@ -104,17 +128,33 @@ export default function ApiTest({ getService }: FtrProviderContext) { ], }, 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'); }); @@ -122,7 +162,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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 () => { @@ -142,7 +186,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 +196,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'); }); }); @@ -201,10 +249,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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, @@ -227,14 +279,26 @@ export default function ApiTest({ getService }: FtrProviderContext) { ], }, 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/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index 22bf6d8aa7f52..cc56d0f2e6684 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,6 +12,7 @@ export default function apmApiIntegrationTests({ }: DeploymentAgnosticFtrProviderContext) { describe('APM', function () { loadTestFile(require.resolve('./agent_explorer')); + loadTestFile(require.resolve('./alerts')); loadTestFile(require.resolve('./mobile')); loadTestFile(require.resolve('./custom_dashboards')); loadTestFile(require.resolve('./dependencies')); 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/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..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,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 { waitForActiveRule } from './helpers/wait_for_active_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'; +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..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 @@ -11,13 +11,11 @@ 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'; +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, @@ -53,59 +51,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, @@ -159,71 +104,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 +121,23 @@ 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({ supertest, actionId, 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..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 @@ -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'; +import { deleteApmRules, deleteAllActionConnectors } from './alerting_api_helper'; export async function cleanupRuleAndAlertState({ es, diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_active_apm_alerts.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_active_apm_alerts.ts index 9f83b4850dd40..e342e31ee0fa3 100644 --- a/x-pack/test/apm_api_integration/tests/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/apm_api_integration/tests/alerts/helpers/wait_for_index_connector_results.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_index_connector_results.ts index 9646289420804..b6634e3a33f2c 100644 --- a/x-pack/test/apm_api_integration/tests/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/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..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,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 '../../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, 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 =