From 670ee8299e99f394a4d79efb8fbe325cb6d36cb0 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 6 Nov 2024 10:52:02 +0100 Subject: [PATCH] Migrate APM alerts test --- .../get_apm_service_summary/index.ts | 5 +- .../get_service_group_alerts.ts | 9 +- .../get_service_transaction_groups_alerts.ts | 11 +- .../get_services/get_service_alerts.ts | 11 +- .../apm}/alerts/error_count_threshold.spec.ts | 46 +- .../apm}/alerts/generate_data.ts | 0 .../apm/alerts/helpers/alerting_api_helper.ts | 256 ++++++++ .../helpers/cleanup_rule_and_alert_state.ts | 39 ++ .../helpers/wait_for_active_apm_alerts.ts | 0 .../alerts/helpers/wait_for_active_rule.ts | 44 ++ .../helpers/wait_for_alerts_for_rule.ts | 52 ++ .../wait_for_index_connector_results.ts | 0 .../apis/observability/apm/alerts/index.ts | 19 + .../alerts/preview_chart_error_count.spec.ts | 478 +++++++------- .../alerts/preview_chart_error_rate.spec.ts | 607 ++++++++++++++++++ ...preview_chart_transaction_duration.spec.ts | 564 ++++++++++++++++ .../apm}/alerts/transaction_duration.spec.ts | 37 +- .../alerts/transaction_error_rate.spec.ts | 37 +- .../apis/observability/apm/index.ts | 1 + .../deployment_agnostic/services/apm_api.ts | 2 + .../tests/alerts/anomaly_alert.spec.ts | 8 +- .../alerts/helpers/alerting_api_helper.ts | 144 +---- .../helpers/cleanup_rule_and_alert_state.ts | 7 +- .../alerts/preview_chart_error_rate.spec.ts | 599 ----------------- ...preview_chart_transaction_duration.spec.ts | 556 ---------------- .../service_group_count.spec.ts | 2 +- 26 files changed, 1947 insertions(+), 1587 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/error_count_threshold.spec.ts (86%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/generate_data.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/helpers/wait_for_active_apm_alerts.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/helpers/wait_for_index_connector_results.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/preview_chart_error_count.spec.ts (50%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_transaction_duration.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/transaction_duration.spec.ts (87%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/alerts/transaction_error_rate.spec.ts (88%) delete mode 100644 x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts index d28152127648b..5c9d40cc22772 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts @@ -6,13 +6,14 @@ */ import datemath from '@elastic/datemath'; import { ElasticsearchClient, Logger } from '@kbn/core/server'; -import { rangeQuery, ScopedAnnotationsClient } from '@kbn/observability-plugin/server'; +import { rangeQuery, ScopedAnnotationsClient, termsQuery } from '@kbn/observability-plugin/server'; import { ALERT_RULE_PRODUCER, ALERT_STATUS, ALERT_STATUS_ACTIVE, } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; import * as t from 'io-ts'; +import { observabilityFeatureId } from '@kbn/observability-shared-plugin/common'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { Environment } from '../../../../common/environment_rt'; import { SERVICE_NAME } from '../../../../common/es_fields/apm'; @@ -139,7 +140,7 @@ export async function getApmServiceSummary({ query: { bool: { filter: [ - ...termQuery(ALERT_RULE_PRODUCER, 'apm'), + ...termsQuery(ALERT_RULE_PRODUCER, 'apm', observabilityFeatureId), ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...rangeQuery(start, end), ...termQuery(SERVICE_NAME, serviceName), diff --git a/x-pack/plugins/observability_solution/apm/server/routes/service_groups/get_service_group_alerts.ts b/x-pack/plugins/observability_solution/apm/server/routes/service_groups/get_service_group_alerts.ts index 8516527d82b9a..d934863f37e9c 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/service_groups/get_service_group_alerts.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/service_groups/get_service_group_alerts.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { kqlQuery } from '@kbn/observability-plugin/server'; -import { ALERT_RULE_PRODUCER, ALERT_STATUS } from '@kbn/rule-data-utils'; +import { kqlQuery, termQuery, termsQuery } from '@kbn/observability-plugin/server'; +import { ALERT_RULE_PRODUCER, ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { Logger } from '@kbn/core/server'; +import { observabilityFeatureId } from '@kbn/observability-shared-plugin/common'; import { ApmPluginRequestHandlerContext } from '../typings'; import { SavedServiceGroup } from '../../../common/service_groups'; import { ApmAlertsClient } from '../../lib/helpers/get_apm_alerts_client'; @@ -42,8 +43,8 @@ export async function getServiceGroupAlerts({ query: { bool: { filter: [ - { term: { [ALERT_RULE_PRODUCER]: 'apm' } }, - { term: { [ALERT_STATUS]: 'active' } }, + ...termsQuery(ALERT_RULE_PRODUCER, 'apm', observabilityFeatureId), + ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ], }, }, diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_transaction_groups_alerts.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_transaction_groups_alerts.ts index 5bdb47cbb1f50..de9c73a30a186 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_transaction_groups_alerts.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_transaction_groups_alerts.ts @@ -5,13 +5,20 @@ * 2.0. */ -import { kqlQuery, termQuery, rangeQuery, wildcardQuery } from '@kbn/observability-plugin/server'; +import { + kqlQuery, + termQuery, + rangeQuery, + wildcardQuery, + termsQuery, +} from '@kbn/observability-plugin/server'; import { ALERT_RULE_PRODUCER, ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_RULE_PARAMETERS, } from '@kbn/rule-data-utils'; +import { observabilityFeatureId } from '@kbn/observability-shared-plugin/common'; import { SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_TYPE } from '../../../common/es_fields/apm'; import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; import { AggregationType } from '../../../common/rules/apm_rule_types'; @@ -59,7 +66,7 @@ export async function getServiceTransactionGroupsAlerts({ query: { bool: { filter: [ - ...termQuery(ALERT_RULE_PRODUCER, 'apm'), + ...termsQuery(ALERT_RULE_PRODUCER, 'apm', observabilityFeatureId), ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...rangeQuery(start, end), ...kqlQuery(kuery), diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts index b225f3ab70ef1..01a125f456443 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts @@ -5,13 +5,20 @@ * 2.0. */ -import { kqlQuery, termQuery, rangeQuery, wildcardQuery } from '@kbn/observability-plugin/server'; +import { + kqlQuery, + termQuery, + rangeQuery, + wildcardQuery, + termsQuery, +} from '@kbn/observability-plugin/server'; import { ALERT_RULE_PRODUCER, ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_UUID, } from '@kbn/rule-data-utils'; +import { observabilityFeatureId } from '@kbn/observability-shared-plugin/common'; import { SERVICE_NAME } from '../../../../common/es_fields/apm'; import { ServiceGroup } from '../../../../common/service_groups'; import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; @@ -51,7 +58,7 @@ export async function getServicesAlerts({ query: { bool: { filter: [ - ...termQuery(ALERT_RULE_PRODUCER, 'apm'), + ...termsQuery(ALERT_RULE_PRODUCER, 'apm', observabilityFeatureId), ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...rangeQuery(start, end), ...kqlQuery(kuery), diff --git a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts similarity index 86% rename from x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts index 8b72afc194e35..d71daed401c2d 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts @@ -10,7 +10,9 @@ import { errorCountActionVariables } from '@kbn/apm-plugin/server/routes/alerts/ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { SupertestWithRoleScopeType } from '../../../../services'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { createApmRule, fetchServiceInventoryAlertCounts, @@ -24,15 +26,17 @@ import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results'; import { waitForActiveRule } from './helpers/wait_for_active_rule'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('supertest'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); const es = getService('es'); const logger = getService('log'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + describe('error count threshold alert', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let supertest: SupertestWithRoleScopeType; - registry.when('error count threshold alert', { config: 'basic', archives: [] }, () => { const javaErrorMessage = 'a java error'; const phpErrorMessage = 'a php error'; @@ -50,7 +54,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { ], }; - before(() => { + before(async () => { + supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + withInternalHeaders: true, + }); + const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) .instance('instance'); @@ -95,13 +103,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]; }); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + return Promise.all([ apmSynthtraceEsClient.index(events), apmSynthtraceEsClient.index(phpEvents), ]); }); - after(() => apmSynthtraceEsClient.clean()); + after(async () => { + await apmSynthtraceEsClient.clean(); + await supertest.destroy(); + }); describe('create rule without kql filter', () => { let ruleId: string; @@ -141,6 +154,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let results: Array>; before(async () => { + await waitForActiveRule({ ruleId, supertest }); results = await waitForIndexConnectorResults({ es, minCount: 2 }); }); @@ -151,6 +165,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); + it('checks if rule is active', async () => { + const ruleStatus = await waitForActiveRule({ ruleId, supertest }); + expect(ruleStatus).to.be('active'); + }); + it('has the right keys', async () => { const phpEntry = results.find((result) => result.serviceName === 'opbeans-php')!; expect(Object.keys(phpEntry).sort()).to.eql([ @@ -170,7 +189,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has the right values', () => { const phpEntry = results.find((result) => result.serviceName === 'opbeans-php')!; - expect(omit(phpEntry, 'alertDetailsUrl')).to.eql({ + expect(omit(phpEntry, 'alertDetailsUrl', 'viewInAppUrl')).to.eql({ environment: 'production', interval: '1 hr', reason: @@ -181,9 +200,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { errorGroupingName: 'a php error', threshold: '1', triggerValue: '30', - viewInAppUrl: - 'http://mockedPublicBaseUrl/app/apm/services/opbeans-php/errors?environment=production', }); + + const url = new URL(phpEntry.viewInAppUrl); + + expect(url.pathname).to.equal('/app/apm/services/opbeans-php/errors'); + expect(url.searchParams.get('environment')).to.equal('production'); }); }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/alerts/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts new file mode 100644 index 0000000000000..27cc2b840d12b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper.ts @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Client, errors } from '@elastic/elasticsearch'; +import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import pRetry from 'p-retry'; +import { ApmRuleType } from '@kbn/rule-data-utils'; +import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; +import type { ApmApiClient } from '../../../../../services/apm_api'; +import type { SupertestWithRoleScopeType } from '../../../../../services'; + +export const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-*'; +export const APM_ACTION_VARIABLE_INDEX = 'apm-index-connector-test'; + +export async function createApmRule({ + supertest, + name, + ruleTypeId, + params, + actions = [], +}: { + supertest: SupertestWithRoleScopeType; + ruleTypeId: T; + name: string; + params: ApmRuleParamsType[T]; + actions?: any[]; +}) { + try { + const { body } = await supertest.post(`/api/alerting/rule`).send({ + params, + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], + name, + rule_type_id: ruleTypeId, + actions, + }); + return body; + } catch (error: any) { + throw new Error(`[Rule] Creating a rule failed: ${error}`); + } +} + +function getTimerange() { + return { + start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + end: new Date(Date.now() + 5 * 60 * 1000).toISOString(), + }; +} + +export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) { + const timerange = getTimerange(); + const serviceInventoryResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + kuery: '', + probability: 1, + documentType: ApmDocumentType.ServiceTransactionMetric, + rollupInterval: RollupInterval.SixtyMinutes, + useDurationSummary: true, + }, + }, + }); + + return serviceInventoryResponse.body.items.reduce>((acc, item) => { + return { ...acc, [item.serviceName]: item.alertsCount ?? 0 }; + }, {}); +} + +export async function fetchServiceTabAlertCount({ + apmApiClient, + serviceName, +}: { + apmApiClient: ApmApiClient; + serviceName: string; +}) { + const timerange = getTimerange(); + const alertsCountReponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count', + params: { + path: { + serviceName, + }, + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + return alertsCountReponse.body.alertsCount; +} + +export async function runRuleSoon({ + ruleId, + supertest, +}: { + ruleId: string; + supertest: SupertestWithRoleScopeType; +}): Promise> { + return pRetry( + async () => { + try { + const response = await supertest.post(`/internal/alerting/rule/${ruleId}/_run_soon`); + // Sometimes the rule may already be running, which returns a 200. Try until it isn't + if (response.status !== 204) { + throw new Error(`runRuleSoon got ${response.status} status`); + } + return response; + } catch (error) { + throw new Error(`[Rule] Running a rule ${ruleId} failed: ${error}`); + } + }, + { retries: 10 } + ); +} + +export async function deleteAlertsByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { + await es.deleteByQuery({ + index: APM_ALERTS_INDEX, + query: { term: { 'kibana.alert.rule.uuid': ruleId } }, + }); +} + +export async function deleteRuleById({ + supertest, + ruleId, +}: { + supertest: SupertestWithRoleScopeType; + ruleId: string; +}) { + await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo'); +} + +export async function deleteApmRules(supertest: SupertestWithRoleScopeType) { + const res = await supertest.get( + `/api/alerting/rules/_find?filter=alert.attributes.consumer:apm&per_page=10000` + ); + + return Promise.all( + res.body.data.map((rule: any) => deleteRuleById({ supertest, ruleId: rule.id })) + ); +} + +export function deleteApmAlerts(es: Client) { + return es.deleteByQuery({ + index: APM_ALERTS_INDEX, + conflicts: 'proceed', + query: { match_all: {} }, + }); +} + +export async function clearKibanaApmEventLog(es: Client) { + return es.deleteByQuery({ + index: '.kibana-event-log-*', + query: { term: { 'kibana.alert.rule.consumer': 'apm' } }, + }); +} + +export type ApmAlertFields = ParsedTechnicalFields & ObservabilityApmAlert; + +export async function createIndexConnector({ + supertest, + name, +}: { + supertest: SupertestWithRoleScopeType; + name: string; +}) { + const { body } = await supertest.post(`/api/actions/connector`).send({ + name, + config: { + index: APM_ACTION_VARIABLE_INDEX, + refresh: true, + }, + connector_type_id: '.index', + }); + + return body.id as string; +} + +export function getIndexAction({ + actionId, + actionVariables, +}: { + actionId: string; + actionVariables: Array<{ name: string }>; +}) { + return { + group: 'threshold_met', + id: actionId, + params: { + documents: [ + actionVariables.reduce>((acc, actionVariable) => { + acc[actionVariable.name] = `{{context.${actionVariable.name}}}`; + return acc; + }, {}), + ], + }, + frequency: { + notify_when: 'onActionGroupChange', + throttle: null, + summary: false, + }, + }; +} + +export async function deleteAllActionConnectors({ + supertest, + es, +}: { + supertest: SupertestWithRoleScopeType; + es: Client; +}): Promise { + const res = await supertest.get(`/api/actions/connectors`); + + const body = res.body as Array<{ id: string; connector_type_id: string; name: string }>; + return Promise.all( + body.map(({ id }) => { + return deleteActionConnector({ supertest, actionId: id }); + }) + ); +} + +async function deleteActionConnector({ + supertest, + actionId, +}: { + supertest: SupertestWithRoleScopeType; + actionId: string; +}) { + return supertest.delete(`/api/actions/connector/${actionId}`); +} + +export async function deleteActionConnectorIndex(es: Client) { + try { + await es.indices.delete({ index: APM_ACTION_VARIABLE_INDEX }); + } catch (e) { + if (e instanceof errors.ResponseError && e.statusCode === 404) { + return; + } + + throw e; + } +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts new file mode 100644 index 0000000000000..b94c2bcc52ee3 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/cleanup_rule_and_alert_state.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Client } from '@elastic/elasticsearch'; +import { ToolingLog } from '@kbn/tooling-log'; +import type { SupertestWithRoleScopeType } from '../../../../../services'; +import { + clearKibanaApmEventLog, + deleteApmRules, + deleteApmAlerts, + deleteActionConnectorIndex, + deleteAllActionConnectors, +} from './alerting_api_helper'; + +export async function cleanupRuleAndAlertState({ + es, + supertest, + logger, +}: { + es: Client; + supertest: SupertestWithRoleScopeType; + logger: ToolingLog; +}) { + try { + await Promise.all([ + deleteApmRules(supertest), + deleteApmAlerts(es), + clearKibanaApmEventLog(es), + deleteActionConnectorIndex(es), + deleteAllActionConnectors({ supertest, es }), + ]); + } catch (e) { + logger.error(`An error occured while cleaning up the state: ${e}`); + } +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_active_apm_alerts.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_active_apm_alerts.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts new file mode 100644 index 0000000000000..989875522c720 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_rule.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import pRetry from 'p-retry'; +import type { SupertestWithRoleScopeType } from '../../../../../services'; + +const RETRIES_COUNT = 10; + +export async function waitForActiveRule({ + ruleId, + supertest, + logger, +}: { + ruleId: string; + supertest: SupertestWithRoleScopeType; + logger?: ToolingLog; +}): Promise> { + return pRetry( + async () => { + const response = await supertest.get(`/api/alerting/rule/${ruleId}`); + const status = response.body?.execution_status?.status; + const expectedStatus = 'active'; + + if (status !== expectedStatus) { + throw new Error(`Expected: ${expectedStatus}: got ${status}`); + } + + return status; + }, + { + retries: RETRIES_COUNT, + onFailedAttempt: (error) => { + if (logger) { + logger.info(`Attempt ${error.attemptNumber}/${RETRIES_COUNT}: Waiting for active rule`); + } + }, + } + ); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts new file mode 100644 index 0000000000000..334631b354cd1 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Client } from '@elastic/elasticsearch'; +import type { + AggregationsAggregate, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import pRetry from 'p-retry'; +import { ApmAlertFields, APM_ALERTS_INDEX } from './alerting_api_helper'; + +async function getAlertByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { + const response = (await es.search({ + index: APM_ALERTS_INDEX, + body: { + query: { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + }, + })) as SearchResponse>; + + return response.hits.hits.map((hit) => hit._source) as ApmAlertFields[]; +} + +export async function waitForAlertsForRule({ + es, + ruleId, + minimumAlertCount = 1, +}: { + es: Client; + ruleId: string; + minimumAlertCount?: number; +}) { + return pRetry( + async () => { + const alerts = await getAlertByRuleId({ es, ruleId }); + const actualAlertCount = alerts.length; + if (actualAlertCount < minimumAlertCount) { + throw new Error(`Expected ${minimumAlertCount} but got ${actualAlertCount} alerts`); + } + + return alerts; + }, + { retries: 5 } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_index_connector_results.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_index_connector_results.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/alerts/helpers/wait_for_index_connector_results.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_index_connector_results.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts new file mode 100644 index 0000000000000..71661e4cbc8bc --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('alerts', () => { + loadTestFile(require.resolve('./error_count_threshold.spec.ts')); + loadTestFile(require.resolve('./preview_chart_error_count.spec.ts')); + loadTestFile(require.resolve('./preview_chart_error_rate.spec.ts')); + loadTestFile(require.resolve('./preview_chart_transaction_duration.spec.ts')); + loadTestFile(require.resolve('./transaction_duration.spec.ts')); + loadTestFile(require.resolve('./transaction_error_rate.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts similarity index 50% rename from x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts index f09dbf1ad9184..d6792400fc2bc 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts @@ -5,21 +5,21 @@ * 2.0. */ +import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; +import expect from '@kbn/expect'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { + ERROR_GROUP_ID, SERVICE_ENVIRONMENT, SERVICE_NAME, - ERROR_GROUP_ID, -} from '@kbn/apm-plugin/common/es_fields/apm'; -import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; -import { getErrorGroupingKey } from '@kbn/apm-synthtrace-client/src/lib/apm/instance'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +} from '@kbn/observability-shared-plugin/common'; +import { generateLongIdWithSeed } from '@kbn/apm-synthtrace-client/src/lib/utils/generate_id'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateErrorData } from './generate_data'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -54,31 +54,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); - registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => { - it('error_count (without data)', async () => { - const options = getOptions(); - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series).to.eql([]); - }); - }); - - registry.when.skip(`with data loaded`, { config: 'basic', archives: [] }, () => { - // FLAKY: https://github.com/elastic/kibana/issues/172769 - describe('error_count: with data loaded', () => { - beforeEach(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - afterEach(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { + describe('preview chart error count', () => { + describe(`without data loaded`, () => { + it('error_count (without data)', async () => { const options = getOptions(); const response = await apmApiClient.readUser({ @@ -87,230 +65,254 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); expect(response.status).to.be(200); - expect( - response.body.errorCountChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); + expect(response.body.errorCountChartPreview.series).to.eql([]); }); + }); - it('with error grouping key', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - errorGroupingKey: `${getErrorGroupingKey('Error 1')}`, - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', - ...options, + describe(`with data loaded`, () => { + describe('error_count: with data loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await Promise.all([ + generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); }); - expect(response.status).to.be(200); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production', y: 250 }]); - }); + after(() => apmSynthtraceEsClient.clean()); - it('with no group by parameter', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', - }); + it('with data', async () => { + const options = getOptions(); - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series.length).to.equal(1); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production', y: 375 }]); - }); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorCountChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT], + it('with error grouping key', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + errorGroupingKey: `${generateLongIdWithSeed('Error 1')}`, + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, }, - }, - }; + }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production', y: 250 }]); }); - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series.length).to.equal(1); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production', y: 375 }]); - }); + it('with no group by parameter', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); - it('with group by on error grouping key', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + expect(response.status).to.be(200); + expect(response.body.errorCountChartPreview.series.length).to.equal(1); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production', y: 375 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT], + }, }, - }, - }; + }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', - }); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series.length).to.equal(2); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: `synth-go_production_${getErrorGroupingKey('Error 1')}`, - y: 250, - }, - { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, - y: 125, - }, - ]); - }); + expect(response.status).to.be(200); + expect(response.body.errorCountChartPreview.series.length).to.equal(1); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production', y: 375 }]); + }); - it('with group by on error grouping key and filter on error grouping key', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - errorGroupingKey: `${getErrorGroupingKey('Error 0')}`, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + it('with group by on error grouping key', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + }, }, - }, - }; + }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorCountChartPreview.series.length).to.equal(2); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: `synth-go_production_${generateLongIdWithSeed('Error 1')}`, + y: 250, + }, + { + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, + y: 125, + }, + ]); }); - expect(response.status).to.be(200); - expect(response.body.errorCountChartPreview.series.length).to.equal(1); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, - y: 125, - }, - ]); - }); + it('with group by on error grouping key and filter on error grouping key', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + errorGroupingKey: `${generateLongIdWithSeed('Error 0')}`, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); - it('with empty service name', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: '', - environment: 'ENVIRONMENT_ALL', - interval: '5m', + expect(response.status).to.be(200); + expect(response.body.errorCountChartPreview.series.length).to.equal(1); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, + y: 125, }, - }, - }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + ]); }); - expect(response.status).to.be(200); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production', y: 375 }, - { name: 'synth-java_production', y: 375 }, - ]); - }); - - it('with empty service name and group by on error grouping key', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: '', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + it('with empty service name', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: '', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, }, - }, - }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }; + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production', y: 375 }, + { name: 'synth-java_production', y: 375 }, + ]); }); - expect(response.status).to.be(200); - expect( - response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: `synth-go_production_${getErrorGroupingKey('Error 1')}`, - y: 250, - }, - { - name: `synth-java_production_${getErrorGroupingKey('Error 1')}`, - y: 250, - }, - { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, - y: 125, - }, - { - name: `synth-java_production_${getErrorGroupingKey('Error 0')}`, - y: 125, - }, - ]); + it('with empty service name and group by on error grouping key', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: '', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, ERROR_GROUP_ID], + }, + }, + }; + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/error_count/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorCountChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: `synth-go_production_${generateLongIdWithSeed('Error 1')}`, + y: 250, + }, + { + name: `synth-java_production_${generateLongIdWithSeed('Error 1')}`, + y: 250, + }, + { + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, + y: 125, + }, + { + name: `synth-java_production_${generateLongIdWithSeed('Error 0')}`, + y: 125, + }, + ]); + }); }); }); - }); - registry.when.skip( - `with data loaded and using KQL filter`, - { config: 'basic', archives: [] }, - () => { - // FLAKY: https://github.com/elastic/kibana/issues/176975 + describe(`with data loaded and using KQL filter`, () => { describe('error_count: with data loaded and using KQL filter', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); }); @@ -340,7 +342,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ...getOptionsWithFilterQuery().params.query, searchConfiguration: JSON.stringify({ query: { - query: `service.name: synth-go and error.grouping_key: ${getErrorGroupingKey( + query: `service.name: synth-go and error.grouping_key: ${generateLongIdWithSeed( 'Error 1' )}`, language: 'kuery', @@ -430,11 +432,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { })) ).to.eql([ { - name: `synth-go_production_${getErrorGroupingKey('Error 1')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 1')}`, y: 250, }, { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, y: 125, }, ]); @@ -447,7 +449,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ...getOptionsWithFilterQuery().params.query, searchConfiguration: JSON.stringify({ query: { - query: `service.name: synth-go and error.grouping_key: ${getErrorGroupingKey( + query: `service.name: synth-go and error.grouping_key: ${generateLongIdWithSeed( 'Error 0' )}`, language: 'kuery', @@ -472,7 +474,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { })) ).to.eql([ { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, y: 125, }, ]); @@ -539,24 +541,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { })) ).to.eql([ { - name: `synth-go_production_${getErrorGroupingKey('Error 1')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 1')}`, y: 250, }, { - name: `synth-java_production_${getErrorGroupingKey('Error 1')}`, + name: `synth-java_production_${generateLongIdWithSeed('Error 1')}`, y: 250, }, { - name: `synth-go_production_${getErrorGroupingKey('Error 0')}`, + name: `synth-go_production_${generateLongIdWithSeed('Error 0')}`, y: 125, }, { - name: `synth-java_production_${getErrorGroupingKey('Error 0')}`, + name: `synth-java_production_${generateLongIdWithSeed('Error 0')}`, y: 125, }, ]); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts new file mode 100644 index 0000000000000..3e5c0753fbc1d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts @@ -0,0 +1,607 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '@kbn/observability-shared-plugin/common'; +import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; +import expect from '@kbn/expect'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { generateErrorData } from './generate_data'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + const getOptions = () => ({ + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }); + + const getOptionsWithFilterQuery = () => ({ + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + interval: '5m', + searchConfiguration: JSON.stringify({ + query: { + query: 'service.name: synth-go and transaction.type: request', + language: 'kuery', + }, + }), + serviceName: undefined, + transactionType: undefined, + transactionName: undefined, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + describe('preview chart error rate', () => { + describe(`without data loaded`, () => { + it('transaction_error_rate without data', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series).to.eql([]); + }); + }); + + describe(`with data loaded`, () => { + describe('transaction_error_rate: with data loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await Promise.all([ + generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('with data', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); + + it('with transaction name', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + transactionName: 'GET /banana', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 50 }]); + }); + + it('with nonexistent transaction name', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + transactionName: 'foo', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series).to.eql([]); + }); + + it('with no group by parameter', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); + }); + + it('with group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(2); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: 'synth-go_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-go_production_request_GET /apple', + y: 25, + }, + ]); + }); + + it('with group by on transaction name and filter on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + transactionName: 'GET /apple', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 25 }]); + }); + + it('with empty service name, transaction name and transaction type', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: '', + transactionName: '', + transactionType: '', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request', y: 37.5 }, + { name: 'synth-java_production_request', y: 37.5 }, + ]); + }); + + it('with empty service name, transaction name, transaction type and group by on transaction name', async () => { + const options = { + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: '', + transactionName: '', + transactionType: '', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: 'synth-go_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-java_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-go_production_request_GET /apple', + y: 25, + }, + { + name: 'synth-java_production_request_GET /apple', + y: 25, + }, + ]); + }); + }); + }); + + describe(`with data loaded and using KQL filter`, () => { + describe('transaction_error_rate: with data loaded and using KQL filter', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await Promise.all([ + generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('with data', async () => { + const options = getOptionsWithFilterQuery(); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); + + it('with transaction name in filter query', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: GET /banana', + language: 'kuery', + }, + }), + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 50 }]); + }); + + it('with nonexistent transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: foo', + language: 'kuery', + }, + }), + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series).to.eql([]); + }); + + it('with no group by parameter', async () => { + const options = getOptionsWithFilterQuery(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); + }); + + it('with group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(2); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: 'synth-go_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-go_production_request_GET /apple', + y: 25, + }, + ]); + }); + + it('with group by on transaction name and filter on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: GET /apple', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview.series.length).to.equal(1); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 25 }]); + }); + + it('with empty filter query', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: '', + language: 'kuery', + }, + }), + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request', y: 37.5 }, + { name: 'synth-java_production_request', y: 37.5 }, + ]); + }); + + it('with empty filter query and group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: '', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { + name: 'synth-go_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-java_production_request_GET /banana', + y: 50, + }, + { + name: 'synth-go_production_request_GET /apple', + y: 25, + }, + { + name: 'synth-java_production_request_GET /apple', + y: 25, + }, + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_transaction_duration.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_transaction_duration.spec.ts new file mode 100644 index 0000000000000..af7f83c393a68 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_transaction_duration.spec.ts @@ -0,0 +1,564 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '@kbn/observability-shared-plugin/common'; +import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; +import expect from '@kbn/expect'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { generateLatencyData } from './generate_data'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + const getOptions = () => ({ + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + serviceName: 'synth-go', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }); + + const getOptionsWithFilterQuery = () => ({ + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + interval: '5m', + searchConfiguration: JSON.stringify({ + query: { + query: 'service.name: synth-go and transaction.type: request', + language: 'kuery', + }, + }), + serviceName: undefined, + transactionType: undefined, + transactionName: undefined, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + describe('preview chart transaction duration', () => { + describe(`without data loaded`, () => { + it('transaction_duration (without data)', async () => { + const options = getOptions(); + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series).to.eql([]); + }); + }); + + describe(`with data loaded`, () => { + describe('transaction_duration: with data loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + await Promise.all([ + generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('with data', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); + + it('with transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + transactionName: 'GET /banana', + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 5000 }]); + }); + + it('with nonexistent transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + transactionName: 'foo', + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series).to.eql([]); + }); + + it('with no group by parameter', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); + }); + + it('with group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(2); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request_GET /apple', y: 10000 }, + { name: 'synth-go_production_request_GET /banana', y: 5000 }, + ]); + }); + + it('with group by on transaction name and filter on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + transactionName: 'GET /apple', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 10000 }]); + }); + + it('with empty service name, transaction name and transaction type', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + serviceName: '', + transactionName: '', + transactionType: '', + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request', y: 7500 }, + { name: 'synth-java_production_request', y: 7500 }, + ]); + }); + + it('with empty service name, transaction name, transaction type and group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + serviceName: '', + transactionName: '', + transactionType: '', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(4); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request_GET /apple', y: 10000 }, + { name: 'synth-java_production_request_GET /apple', y: 10000 }, + { name: 'synth-go_production_request_GET /banana', y: 5000 }, + { name: 'synth-java_production_request_GET /banana', y: 5000 }, + ]); + }); + }); + }); + + describe(`with data loaded and using KQL filter`, () => { + describe('transaction_duration: with data loaded and using KQL filter', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await Promise.all([ + generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }), + generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('with data', async () => { + const options = getOptionsWithFilterQuery(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.some((item: PreviewChartResponseItem) => + item.data.some((coordinate) => coordinate.x && coordinate.y) + ) + ).to.equal(true); + }); + + it('with transaction name in filter query', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: GET /banana', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 5000 }]); + }); + + it('with nonexistent transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: foo', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series).to.eql([]); + }); + + it('with no group by parameter', async () => { + const options = getOptionsWithFilterQuery(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); + }); + + it('with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); + }); + + it('with group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(2); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request_GET /apple', y: 10000 }, + { name: 'synth-go_production_request_GET /banana', y: 5000 }, + ]); + }); + + it('with group by on transaction name and filter on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: + 'service.name: synth-go and transaction.type: request and transaction.name: GET /apple', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(1); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 10000 }]); + }); + + it('with empty filter query', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: '', + language: 'kuery', + }, + }), + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request', y: 7500 }, + { name: 'synth-java_production_request', y: 7500 }, + ]); + }); + + it('with empty filter query and group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptionsWithFilterQuery().params.query, + searchConfiguration: JSON.stringify({ + query: { + query: '', + language: 'kuery', + }, + }), + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.series.length).to.equal(4); + expect( + response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ + name: item.name, + y: item.data[0].y, + })) + ).to.eql([ + { name: 'synth-go_production_request_GET /apple', y: 10000 }, + { name: 'synth-java_production_request_GET /apple', y: 10000 }, + { name: 'synth-go_production_request_GET /banana', y: 5000 }, + { name: 'synth-java_production_request_GET /banana', y: 5000 }, + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts similarity index 87% rename from x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts index 0d7460ff5be50..c23baa32c17dc 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts @@ -11,7 +11,9 @@ import { transactionDurationActionVariables } from '@kbn/apm-plugin/server/route import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { SupertestWithRoleScopeType } from '../../../../services'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { createApmRule, fetchServiceInventoryAlertCounts, @@ -25,13 +27,12 @@ import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; import { waitForActiveRule } from './helpers/wait_for_active_rule'; import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('supertest'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); const es = getService('es'); const logger = getService('log'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const ruleParams = { threshold: 3000, @@ -44,8 +45,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { groupBy: ['service.name', 'service.environment', 'transaction.type', 'transaction.name'], }; - registry.when('transaction duration alert', { config: 'basic', archives: [] }, () => { - before(() => { + describe('transaction duration alert', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let supertest: SupertestWithRoleScopeType; + + before(async () => { + supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + withInternalHeaders: true, + }); + const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) .instance('instance'); @@ -68,11 +76,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { .success(), ]; }); + + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); return apmSynthtraceEsClient.index(events); }); after(async () => { await apmSynthtraceEsClient.clean(); + await supertest.destroy(); }); describe('create rule for opbeans-java without kql filter', () => { @@ -133,7 +144,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('populates the document with the correct values', async () => { - expect(omit(results[0], 'alertDetailsUrl')).to.eql({ + expect(omit(results[0], 'alertDetailsUrl', 'viewInAppUrl')).to.eql({ environment: 'production', interval: '5 mins', reason: @@ -143,9 +154,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { transactionName: 'tx-java', threshold: '3000', triggerValue: '5,000 ms', - viewInAppUrl: - 'http://mockedPublicBaseUrl/app/apm/services/opbeans-java?transactionType=request&environment=production', }); + + const url = new URL(results[0].viewInAppUrl); + + expect(url.pathname).to.equal('/app/apm/services/opbeans-java'); + expect(url.searchParams.get('transactionType')).to.equal('request'); + expect(url.searchParams.get('environment')).to.equal('production'); }); }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts similarity index 88% rename from x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts index 509d839ecef6d..e3f3bde6e6bf8 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts @@ -10,7 +10,9 @@ import { transactionErrorRateActionVariables } from '@kbn/apm-plugin/server/rout import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { SupertestWithRoleScopeType } from '../../../../services'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { createApmRule, fetchServiceInventoryAlertCounts, @@ -24,16 +26,22 @@ import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; import { waitForActiveRule } from './helpers/wait_for_active_rule'; import { waitForIndexConnectorResults } from './helpers/wait_for_index_connector_results'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('supertest'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); const es = getService('es'); const logger = getService('log'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + describe('transaction error rate alert', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let supertest: SupertestWithRoleScopeType; + + before(async () => { + supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + withInternalHeaders: true, + }); - registry.when('transaction error rate alert', { config: 'basic', archives: [] }, () => { - before(() => { const opbeansJava = apm .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) .instance('instance'); @@ -66,11 +74,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { .success(), ]; }); + + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); return apmSynthtraceEsClient.index(events); }); after(async () => { await apmSynthtraceEsClient.clean(); + await supertest.destroy(); }); describe('create rule without kql query', () => { @@ -142,7 +153,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('has the right values', () => { - expect(omit(results[0], 'alertDetailsUrl')).to.eql({ + expect(omit(results[0], 'alertDetailsUrl', 'viewInAppUrl')).to.eql({ environment: 'production', interval: '5 mins', reason: @@ -152,9 +163,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { threshold: '40', transactionType: 'request', triggerValue: '50', - viewInAppUrl: - 'http://mockedPublicBaseUrl/app/apm/services/opbeans-java?transactionType=request&environment=production', }); + + const url = new URL(results[0].viewInAppUrl); + + expect(url.pathname).to.equal('/app/apm/services/opbeans-java'); + expect(url.searchParams.get('transactionType')).to.equal('request'); + expect(url.searchParams.get('environment')).to.equal('production'); }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index a62c11d40b1af..0d1055c58299b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -12,5 +12,6 @@ export default function apmApiIntegrationTests({ }: DeploymentAgnosticFtrProviderContext) { describe('APM', function () { loadTestFile(require.resolve('./agent_explorer')); + loadTestFile(require.resolve('./alerts')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/services/apm_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/apm_api.ts index 26d92997a6021..c3f43b57902e8 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/apm_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/apm_api.ts @@ -135,3 +135,5 @@ export function ApmApiProvider(context: DeploymentAgnosticFtrProviderContext) { writeUser: createApmApiClient(context, 'editor'), }; } + +export type ApmApiClient = ReturnType; diff --git a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts index 033d64e8f12e8..61bdfd97d2254 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts @@ -12,12 +12,12 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { range } from 'lodash'; import { ML_ANOMALY_SEVERITY } from '@kbn/ml-anomaly-utils/anomaly_severity'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createAndRunApmMlJobs } from '../../common/utils/create_and_run_apm_ml_jobs'; -import { createApmRule } from './helpers/alerting_api_helper'; +import { waitForAlertsForRule } from '../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_alerts_for_rule'; import { waitForActiveRule } from './helpers/wait_for_active_rule'; -import { waitForAlertsForRule } from './helpers/wait_for_alerts_for_rule'; +import { createApmRule } from './helpers/alerting_api_helper'; import { cleanupRuleAndAlertState } from './helpers/cleanup_rule_and_alert_state'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createAndRunApmMlJobs } from '../../common/utils/create_and_run_apm_ml_jobs'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts index 5da6ee4f860d0..4ae1085eea053 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/helpers/alerting_api_helper.ts @@ -5,16 +5,13 @@ * 2.0. */ -import { Client, errors } from '@elastic/elasticsearch'; +import { Client } from '@elastic/elasticsearch'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import pRetry from 'p-retry'; import type { Agent as SuperTestAgent } from 'supertest'; import { ApmRuleType } from '@kbn/rule-data-utils'; import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema'; -import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ObservabilityApmAlert } from '@kbn/alerts-as-data-utils'; -import { ApmApiClient } from '../../../common/config'; export const APM_ALERTS_INDEX = '.alerts-observability.apm.alerts-*'; export const APM_ACTION_VARIABLE_INDEX = 'apm-index-connector-test'; @@ -53,59 +50,6 @@ export async function createApmRule({ } } -function getTimerange() { - return { - start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - end: new Date(Date.now() + 5 * 60 * 1000).toISOString(), - }; -} - -export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) { - const timerange = getTimerange(); - const serviceInventoryResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - ...timerange, - environment: 'ENVIRONMENT_ALL', - kuery: '', - probability: 1, - documentType: ApmDocumentType.ServiceTransactionMetric, - rollupInterval: RollupInterval.SixtyMinutes, - useDurationSummary: true, - }, - }, - }); - - return serviceInventoryResponse.body.items.reduce>((acc, item) => { - return { ...acc, [item.serviceName]: item.alertsCount ?? 0 }; - }, {}); -} - -export async function fetchServiceTabAlertCount({ - apmApiClient, - serviceName, -}: { - apmApiClient: ApmApiClient; - serviceName: string; -}) { - const timerange = getTimerange(); - const alertsCountReponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count', - params: { - path: { - serviceName, - }, - query: { - ...timerange, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - - return alertsCountReponse.body.alertsCount; -} - export async function runRuleSoon({ ruleId, supertest, @@ -132,13 +76,6 @@ export async function runRuleSoon({ ); } -export async function deleteAlertsByRuleId({ es, ruleId }: { es: Client; ruleId: string }) { - await es.deleteByQuery({ - index: APM_ALERTS_INDEX, - query: { term: { 'kibana.alert.rule.uuid': ruleId } }, - }); -} - export async function deleteRuleById({ supertest, ruleId, @@ -159,71 +96,6 @@ export async function deleteApmRules(supertest: SuperTestAgent) { ); } -export function deleteApmAlerts(es: Client) { - return es.deleteByQuery({ - index: APM_ALERTS_INDEX, - conflicts: 'proceed', - query: { match_all: {} }, - }); -} - -export async function clearKibanaApmEventLog(es: Client) { - return es.deleteByQuery({ - index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'apm' } }, - }); -} - -export type ApmAlertFields = ParsedTechnicalFields & ObservabilityApmAlert; - -export async function createIndexConnector({ - supertest, - name, -}: { - supertest: SuperTestAgent; - name: string; -}) { - const { body } = await supertest - .post(`/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name, - config: { - index: APM_ACTION_VARIABLE_INDEX, - refresh: true, - }, - connector_type_id: '.index', - }); - - return body.id as string; -} - -export function getIndexAction({ - actionId, - actionVariables, -}: { - actionId: string; - actionVariables: Array<{ name: string }>; -}) { - return { - group: 'threshold_met', - id: actionId, - params: { - documents: [ - actionVariables.reduce>((acc, actionVariable) => { - acc[actionVariable.name] = `{{context.${actionVariable.name}}}`; - return acc; - }, {}), - ], - }, - frequency: { - notify_when: 'onActionGroupChange', - throttle: null, - summary: false, - }, - }; -} - export async function deleteAllActionConnectors({ supertest, es, @@ -241,6 +113,8 @@ export async function deleteAllActionConnectors({ ); } +export type ApmAlertFields = ParsedTechnicalFields & ObservabilityApmAlert; + async function deleteActionConnector({ supertest, actionId, @@ -250,15 +124,3 @@ async function deleteActionConnector({ }) { return supertest.delete(`/api/actions/connector/${actionId}`).set('kbn-xsrf', 'foo'); } - -export async function deleteActionConnectorIndex(es: Client) { - try { - await es.indices.delete({ index: APM_ACTION_VARIABLE_INDEX }); - } catch (e) { - if (e instanceof errors.ResponseError && e.statusCode === 404) { - return; - } - - throw e; - } -} diff --git a/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts b/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts index 2fae6c9643ff7..c3cce2d498c87 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/helpers/cleanup_rule_and_alert_state.ts @@ -9,12 +9,11 @@ import { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; import type { Agent as SuperTestAgent } from 'supertest'; import { + deleteActionConnectorIndex, clearKibanaApmEventLog, - deleteApmRules, deleteApmAlerts, - deleteActionConnectorIndex, - deleteAllActionConnectors, -} from './alerting_api_helper'; +} from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/alerting_api_helper'; +import { deleteApmRules, deleteAllActionConnectors } from './alerting_api_helper'; export async function cleanupRuleAndAlertState({ es, diff --git a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts deleted file mode 100644 index 3af62d826fd31..0000000000000 --- a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - SERVICE_ENVIRONMENT, - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '@kbn/apm-plugin/common/es_fields/apm'; -import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { generateErrorData } from './generate_data'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - const getOptions = () => ({ - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }); - - const getOptionsWithFilterQuery = () => ({ - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - interval: '5m', - searchConfiguration: JSON.stringify({ - query: { - query: 'service.name: synth-go and transaction.type: request', - language: 'kuery', - }, - }), - serviceName: undefined, - transactionType: undefined, - transactionName: undefined, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - - registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => { - it('transaction_error_rate without data', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series).to.eql([]); - }); - }); - - registry.when(`with data loaded`, { config: 'basic', archives: [] }, () => { - // FLAKY: https://github.com/elastic/kibana/issues/176977 - describe('transaction_error_rate: with data loaded', () => { - before(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); - }); - - it('with transaction name', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - transactionName: 'GET /banana', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 50 }]); - }); - - it('with nonexistent transaction name', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - transactionName: 'foo', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series).to.eql([]); - }); - - it('with no group by parameter', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); - }); - - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); - }); - - it('with group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(2); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: 'synth-go_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-go_production_request_GET /apple', - y: 25, - }, - ]); - }); - - it('with group by on transaction name and filter on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - transactionName: 'GET /apple', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 25 }]); - }); - - it.skip('with empty service name, transaction name and transaction type', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: '', - transactionName: '', - transactionType: '', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request', y: 37.5 }, - { name: 'synth-java_production_request', y: 37.5 }, - ]); - }); - - it('with empty service name, transaction name, transaction type and group by on transaction name', async () => { - const options = { - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: '', - transactionName: '', - transactionType: '', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: 'synth-go_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-java_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-go_production_request_GET /apple', - y: 25, - }, - { - name: 'synth-java_production_request_GET /apple', - y: 25, - }, - ]); - }); - }); - }); - - registry.when(`with data loaded and using KQL filter`, { config: 'basic', archives: [] }, () => { - // FLAKY: https://github.com/elastic/kibana/issues/176983 - describe('transaction_error_rate: with data loaded and using KQL filter', () => { - before(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { - const options = getOptionsWithFilterQuery(); - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); - }); - - it('with transaction name in filter query', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: GET /banana', - language: 'kuery', - }, - }), - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 50 }]); - }); - - it('with nonexistent transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: foo', - language: 'kuery', - }, - }), - }, - }, - }; - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series).to.eql([]); - }); - - it('with no group by parameter', async () => { - const options = getOptionsWithFilterQuery(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); - }); - - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 37.5 }]); - }); - - it('with group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(2); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: 'synth-go_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-go_production_request_GET /apple', - y: 25, - }, - ]); - }); - - it('with group by on transaction name and filter on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: GET /apple', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.errorRateChartPreview.series.length).to.equal(1); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 25 }]); - }); - - it('with empty filter query', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: '', - language: 'kuery', - }, - }), - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request', y: 37.5 }, - { name: 'synth-java_production_request', y: 37.5 }, - ]); - }); - - it.skip('with empty filter query and group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: '', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.errorRateChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { - name: 'synth-go_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-java_production_request_GET /banana', - y: 50, - }, - { - name: 'synth-go_production_request_GET /apple', - y: 25, - }, - { - name: 'synth-java_production_request_GET /apple', - y: 25, - }, - ]); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts deleted file mode 100644 index a677ce11cdb0c..0000000000000 --- a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - SERVICE_ENVIRONMENT, - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '@kbn/apm-plugin/common/es_fields/apm'; -import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { generateLatencyData } from './generate_data'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - const getOptions = () => ({ - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - serviceName: 'synth-go', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - interval: '5m', - }, - }, - }); - - const getOptionsWithFilterQuery = () => ({ - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - interval: '5m', - searchConfiguration: JSON.stringify({ - query: { - query: 'service.name: synth-go and transaction.type: request', - language: 'kuery', - }, - }), - serviceName: undefined, - transactionType: undefined, - transactionName: undefined, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - - registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => { - it('transaction_duration (without data)', async () => { - const options = getOptions(); - - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - ...options, - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series).to.eql([]); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/176989 - registry.when(`with data loaded`, { config: 'basic', archives: [] }, () => { - // FLAKY: https://github.com/elastic/kibana/issues/176986 - // Failing: See https://github.com/elastic/kibana/issues/176989 - describe('transaction_duration: with data loaded', () => { - before(async () => { - await generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); - }); - - it('with transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - transactionName: 'GET /banana', - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 5000 }]); - }); - - it('with nonexistent transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - transactionName: 'foo', - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series).to.eql([]); - }); - - it('with no group by parameter', async () => { - const options = getOptions(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); - }); - - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); - }); - - it('with group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(2); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request_GET /apple', y: 10000 }, - { name: 'synth-go_production_request_GET /banana', y: 5000 }, - ]); - }); - - it('with group by on transaction name and filter on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - transactionName: 'GET /apple', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 10000 }]); - }); - - it('with empty service name, transaction name and transaction type', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - serviceName: '', - transactionName: '', - transactionType: '', - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request', y: 7500 }, - { name: 'synth-java_production_request', y: 7500 }, - ]); - }); - - it('with empty service name, transaction name, transaction type and group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptions().params.query, - serviceName: '', - transactionName: '', - transactionType: '', - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(4); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request_GET /apple', y: 10000 }, - { name: 'synth-java_production_request_GET /apple', y: 10000 }, - { name: 'synth-go_production_request_GET /banana', y: 5000 }, - { name: 'synth-java_production_request_GET /banana', y: 5000 }, - ]); - }); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/176989 - registry.when(`with data loaded and using KQL filter`, { config: 'basic', archives: [] }, () => { - describe('transaction_duration: with data loaded and using KQL filter', () => { - before(async () => { - await generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); - await generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('with data', async () => { - const options = getOptionsWithFilterQuery(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.some((item: PreviewChartResponseItem) => - item.data.some((coordinate) => coordinate.x && coordinate.y) - ) - ).to.equal(true); - }); - - it('with transaction name in filter query', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: GET /banana', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 5000 }]); - }); - - it('with nonexistent transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: foo', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series).to.eql([]); - }); - - it('with no group by parameter', async () => { - const options = getOptionsWithFilterQuery(); - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); - }); - - it('with default group by fields', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request', y: 7500 }]); - }); - - it('with group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(2); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request_GET /apple', y: 10000 }, - { name: 'synth-go_production_request_GET /banana', y: 5000 }, - ]); - }); - - it('with group by on transaction name and filter on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: - 'service.name: synth-go and transaction.type: request and transaction.name: GET /apple', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(1); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([{ name: 'synth-go_production_request_GET /apple', y: 10000 }]); - }); - - it('with empty filter query', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: '', - language: 'kuery', - }, - }), - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request', y: 7500 }, - { name: 'synth-java_production_request', y: 7500 }, - ]); - }); - - it('with empty filter query and group by on transaction name', async () => { - const options = { - params: { - query: { - ...getOptionsWithFilterQuery().params.query, - searchConfiguration: JSON.stringify({ - query: { - query: '', - language: 'kuery', - }, - }), - groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], - }, - }, - }; - - const response = await apmApiClient.readUser({ - ...options, - endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', - }); - - expect(response.status).to.be(200); - expect(response.body.latencyChartPreview.series.length).to.equal(4); - expect( - response.body.latencyChartPreview.series.map((item: PreviewChartResponseItem) => ({ - name: item.name, - y: item.data[0].y, - })) - ).to.eql([ - { name: 'synth-go_production_request_GET /apple', y: 10000 }, - { name: 'synth-java_production_request_GET /apple', y: 10000 }, - { name: 'synth-go_production_request_GET /banana', y: 5000 }, - { name: 'synth-java_production_request_GET /banana', y: 5000 }, - ]); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts index 044006e273348..93c63875e28b8 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts @@ -7,10 +7,10 @@ import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { ApmRuleType } from '@kbn/rule-data-utils'; import expect from '@kbn/expect'; +import { waitForActiveApmAlert } from '../../../../api_integration/deployment_agnostic/apis/observability/apm/alerts/helpers/wait_for_active_apm_alerts'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { createApmRule } from '../../alerts/helpers/alerting_api_helper'; import { cleanupRuleAndAlertState } from '../../alerts/helpers/cleanup_rule_and_alert_state'; -import { waitForActiveApmAlert } from '../../alerts/helpers/wait_for_active_apm_alerts'; import { createServiceGroupApi, deleteAllServiceGroups,