From 71bb4fa6f26713781be2e656eb44e244bc9d6cb9 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Thu, 14 Nov 2024 13:31:17 +0100 Subject: [PATCH] [APM] Migrate service tests to deployment agnostic (#199812) ## Summary Closes https://github.com/elastic/kibana/issues/198988 Part of https://github.com/elastic/kibana/issues/193245 This PR contains the changes to migrate `service` test folder to Deployment-agnostic testing strategy. >[!NOTE] > `top_services.spec.ts` and `throughput.spec.ts` were partially migrated and `annotations.spec.ts` was not migrated ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` It's recommended to be run against [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` - [ ] ~(OPTIONAL, only if a test has been unskipped) Run flaky test suite~ - [x] local run for serverless - [x] local run for stateful - [x] MKI run for serverless --------- Co-authored-by: Sergi Romeu --- .../apm/alerts/error_count_threshold.spec.ts | 13 +- .../apm/alerts/transaction_duration.spec.ts | 13 +- .../apm/alerts/transaction_error_rate.spec.ts | 13 +- .../observability/apm/constants/archiver.ts | 4 - .../apis/observability/apm/index.ts | 1 + ...error_groups_detailed_statistics.spec.snap | 0 ...chive_services_detailed_statistics.spec.ts | 49 ++ .../apm/services/derived_annotations.spec.ts | 172 ++++++ .../error_groups_detailed_statistics.spec.ts | 23 +- .../error_groups_main_statistics.spec.ts | 23 +- .../services/error_groups/generate_data.ts | 0 .../error_groups/get_error_group_ids.ts | 4 +- .../get_service_node_metadata.spec.ts | 32 +- .../apis/observability/apm/services/index.ts | 24 + .../apm}/services/service_alerts.spec.ts | 96 ++-- .../services/service_details/generate_data.ts | 0 .../service_details/service_details.spec.ts | 120 ++++ .../service_infra_metrics.spec.ts | 28 +- .../services/service_icons/generate_data.ts | 0 .../service_icons/service_icons.spec.ts | 80 +++ .../services_detailed_statistics.spec.ts | 119 ++-- .../apm/services/throughput.spec.ts | 541 ++++++++++++++++++ .../apm/services/top_services.spec.ts | 364 ++++++++++++ .../apm/services/transaction_types.spec.ts | 92 +++ .../services/alerting_api.ts | 19 + .../tests/services/agent.spec.ts | 59 -- ...chive_services_detailed_statistics.spec.ts | 32 -- .../services/derived_annotations.spec.ts | 171 ------ .../service_details/service_details.spec.ts | 121 ---- .../service_icons/service_icons.spec.ts | 77 --- .../tests/services/throughput.spec.ts | 535 ----------------- .../tests/services/top_services.spec.ts | 351 ------------ .../tests/services/transaction_types.spec.ts | 92 --- 33 files changed, 1634 insertions(+), 1634 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/__snapshots__/error_groups_detailed_statistics.spec.snap (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/archive_services_detailed_statistics.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/derived_annotations.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/error_groups/error_groups_detailed_statistics.spec.ts (91%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/error_groups/error_groups_main_statistics.spec.ts (83%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/error_groups/generate_data.ts (100%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/error_groups/get_error_group_ids.ts (88%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/get_service_node_metadata.spec.ts (79%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/service_alerts.spec.ts (63%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/service_details/generate_data.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_details.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/service_details/service_infra_metrics.spec.ts (89%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/service_icons/generate_data.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/service_icons.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/services_detailed_statistics.spec.ts (87%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/throughput.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/top_services.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/transaction_types.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/agent.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/derived_annotations.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/throughput.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts index 16493e8220f68..e0b9e1b022b4f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts @@ -11,7 +11,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services'; +import type { RoleCredentials } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { fetchServiceInventoryAlertCounts, @@ -23,7 +23,6 @@ import { } from './helpers/alerting_helper'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { - const roleScopedSupertest = getService('roleScopedSupertest'); const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); const alertingApi = getService('alertingApi'); @@ -31,7 +30,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('error count threshold alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; let roleAuthc: RoleCredentials; const javaErrorMessage = 'a java error'; @@ -52,14 +50,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }; before(async () => { - supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( - 'viewer', - { - withInternalHeaders: true, - useCookieHeader: true, - } - ); - roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); const opbeansJava = apm @@ -116,7 +106,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertestViewerWithCookieCredentials.destroy(); await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts index 0cd3446359557..0fab5d6e35853 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts @@ -12,7 +12,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services'; +import type { RoleCredentials } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { fetchServiceInventoryAlertCounts, @@ -24,7 +24,6 @@ import { } from './helpers/alerting_helper'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { - const roleScopedSupertest = getService('roleScopedSupertest'); const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); const alertingApi = getService('alertingApi'); @@ -43,18 +42,9 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('transaction duration alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; let roleAuthc: RoleCredentials; before(async () => { - supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( - 'viewer', - { - withInternalHeaders: true, - useCookieHeader: true, - } - ); - roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); const opbeansJava = apm @@ -86,7 +76,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertestViewerWithCookieCredentials.destroy(); await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts index e538ff0e6a3ba..637786195cd21 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts @@ -11,7 +11,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services'; +import type { RoleCredentials } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { fetchServiceInventoryAlertCounts, @@ -23,7 +23,6 @@ import { } from './helpers/alerting_helper'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { - const roleScopedSupertest = getService('roleScopedSupertest'); const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); const alertingApi = getService('alertingApi'); @@ -31,18 +30,9 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('transaction error rate alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; let roleAuthc: RoleCredentials; before(async () => { - supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( - 'viewer', - { - withInternalHeaders: true, - useCookieHeader: true, - } - ); - roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); const opbeansJava = apm @@ -84,7 +74,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertestViewerWithCookieCredentials.destroy(); await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts index 6afc2e9eca63b..1c81efc0966d0 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts @@ -7,24 +7,20 @@ type ArchiveName = | '8.0.0' - | 'apm_8.0.0' | 'apm_mappings_only_8.0.0' | 'infra_metrics_and_apm' | 'metrics_8.0.0' - | 'ml_8.0.0' | 'observability_overview' | 'rum_8.0.0' | 'rum_test_data'; export const ARCHIVER_ROUTES: { [key in ArchiveName]: string } = { '8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0', - 'apm_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0', 'apm_mappings_only_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0', infra_metrics_and_apm: 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/infra_metrics_and_apm', 'metrics_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0', - 'ml_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/ml_8.0.0', observability_overview: 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/observability_overview', 'rum_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_8.0.0', 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 0cf1116f4140a..c09e1dd4b99b4 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 @@ -23,6 +23,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./correlations')); loadTestFile(require.resolve('./entities')); loadTestFile(require.resolve('./cold_start')); + loadTestFile(require.resolve('./services')); loadTestFile(require.resolve('./historical_data')); loadTestFile(require.resolve('./observability_overview')); loadTestFile(require.resolve('./latency')); diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_detailed_statistics.spec.snap b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/__snapshots__/error_groups_detailed_statistics.spec.snap similarity index 100% rename from x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_detailed_statistics.spec.snap rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/__snapshots__/error_groups_detailed_statistics.spec.snap diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/archive_services_detailed_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/archive_services_detailed_statistics.spec.ts new file mode 100644 index 0000000000000..9c00ed2a471bf --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/archive_services_detailed_statistics.spec.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + + const start = '2021-10-01T00:00:00.000Z'; + const end = '2021-10-01T01:00:00.000Z'; + + const serviceNames = ['opbeans-java', 'opbeans-go']; + + describe('Services detailed statistics', () => { + describe('Services detailed statistics when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: `POST /internal/apm/services/detailed_statistics`, + params: { + query: { + start, + end, + environment: 'ENVIRONMENT_ALL', + kuery: '', + offset: '1d', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + }, + body: { + serviceNames: JSON.stringify(serviceNames), + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.be.empty(); + expect(response.body.previousPeriod).to.be.empty(); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/derived_annotations.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/derived_annotations.spec.ts new file mode 100644 index 0000000000000..3af97dea84c72 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/derived_annotations.spec.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; + +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function annotationApiTests({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const es = getService('es'); + const samlAuth = getService('samlAuth'); + + const dates = [ + new Date('2021-02-01T00:00:00.000Z'), + new Date('2021-02-01T01:00:00.000Z'), + new Date('2021-02-01T02:00:00.000Z'), + new Date('2021-02-01T03:00:00.000Z'), + ]; + + const indexName = 'apm-8.0.0-transaction'; + + describe('Derived deployment annotations', () => { + describe('when there are multiple service versions', () => { + let roleAuthc: RoleCredentials; + let response: APIReturnType<'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31'>; + + before(async () => { + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('viewer'); + const indexExists = await es.indices.exists({ index: indexName }); + if (indexExists) { + await es.indices.delete({ + index: indexName, + }); + } + + await es.indices.create({ + index: indexName, + body: { + mappings: { + properties: { + service: { + properties: { + name: { + type: 'keyword', + }, + version: { + type: 'keyword', + }, + environment: { + type: 'keyword', + }, + }, + }, + transaction: { + properties: { + type: { + type: 'keyword', + }, + duration: { + type: 'long', + }, + }, + }, + processor: { + properties: { + event: { + type: 'keyword', + }, + }, + }, + }, + }, + }, + }); + + const docs = dates.flatMap((date, index) => { + const baseAnnotation = { + transaction: { + type: 'request', + duration: 1000000, + }, + + service: { + name: 'opbeans-java', + environment: 'production', + version: index + 1, + }, + processor: { + event: 'transaction', + }, + }; + return [ + { + ...baseAnnotation, + '@timestamp': date.toISOString(), + }, + { + ...baseAnnotation, + '@timestamp': new Date(date.getTime() + 30000), + }, + { + ...baseAnnotation, + '@timestamp': new Date(date.getTime() + 60000), + }, + ]; + }); + + await es.bulk({ + index: indexName, + body: docs.flatMap((doc) => [{ index: {} }, doc]), + refresh: true, + }); + + response = ( + await apmApiClient.readUser({ + endpoint: 'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + start: dates[1].toISOString(), + end: dates[2].toISOString(), + environment: 'production', + }, + }, + roleAuthc, + }) + ).body; + }); + + after(async () => { + await es.indices.delete({ + index: indexName, + }); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); + + it('annotations are displayed for the service versions in the given time range', async () => { + expect(response.annotations.length).to.be(2); + expect(response.annotations[0]['@timestamp']).to.be(dates[1].getTime()); + expect(response.annotations[1]['@timestamp']).to.be(dates[2].getTime()); + + expectSnapshot(response.annotations[0]).toMatchInline(` + Object { + "@timestamp": 1612141200000, + "id": "2", + "text": "2", + "type": "version", + } + `); + }); + + it('annotations are not displayed for the service versions outside of the given time range', () => { + expect( + response.annotations.some((annotation) => { + return ( + annotation['@timestamp'] !== dates[0].getTime() && + annotation['@timestamp'] !== dates[2].getTime() + ); + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_detailed_statistics.spec.ts similarity index 91% rename from x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_detailed_statistics.spec.ts index 620d705f4463f..e01ea0b618deb 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_detailed_statistics.spec.ts @@ -14,17 +14,17 @@ import { APIReturnType, } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { config, generateData } from './generate_data'; import { getErrorGroupIds } from './get_error_group_ids'; type ErrorGroupsDetailedStatistics = APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>; -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 serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -52,23 +52,20 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Error groups detailed statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Error groups detailed statistics', () => { + describe('when data is not loaded', () => { it('handles empty state', async () => { const response = await callApi(); expect(response.status).to.be(200); expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); }); - } - ); + }); - // FLAKY: https://github.com/elastic/kibana/issues/177656 - registry.when('Error groups detailed statistics', { config: 'basic', archives: [] }, () => { describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE } = config; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_main_statistics.spec.ts similarity index 83% rename from x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_main_statistics.spec.ts index 3377cdabb3847..f6f8f99140c65 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_main_statistics.spec.ts @@ -11,16 +11,16 @@ import { APIReturnType, } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { generateData, config } from './generate_data'; type ErrorGroupsMainStatistics = APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>; -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 serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -46,24 +46,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Error groups main statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Error groups main statistics', () => { + describe(' when data is not loaded', () => { it('handles empty state', async () => { const response = await callApi(); expect(response.status).to.be(200); expect(response.body.errorGroups).to.empty(); }); - } - ); + }); - // FLAKY: https://github.com/elastic/kibana/issues/177664 - registry.when('Error groups main statistics', { config: 'basic', archives: [] }, () => { describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE, ERROR_NAME_1, ERROR_NAME_2 } = config; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/generate_data.ts diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/get_error_group_ids.ts similarity index 88% rename from x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/get_error_group_ids.ts index 914f6962d9bce..e9487e5e099c9 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/get_error_group_ids.ts @@ -5,7 +5,7 @@ * 2.0. */ import { take } from 'lodash'; -import { ApmServices } from '../../../common/config'; +import type { ApmApiClient } from '../../../../../services/apm_api'; export async function getErrorGroupIds({ apmApiClient, @@ -14,7 +14,7 @@ export async function getErrorGroupIds({ serviceName = 'opbeans-java', count = 5, }: { - apmApiClient: Awaited>; + apmApiClient: ApmApiClient; start: number; end: number; serviceName?: string; diff --git a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/get_service_node_metadata.spec.ts similarity index 79% rename from x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/get_service_node_metadata.spec.ts index 6644ed8bc7d1c..e1ac83609b86f 100644 --- a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/get_service_node_metadata.spec.ts @@ -9,12 +9,12 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); - 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; @@ -38,10 +38,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Service node metadata when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Service node metadata', () => { + describe('when data is not loaded', () => { it('handles the empty state', async () => { const response = await callApi(); @@ -54,15 +52,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { } `); }); - } - ); + }); + + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; - // FLAKY: https://github.com/elastic/kibana/issues/177513 - registry.when( - 'Service node metadata when data is loaded', - { config: 'basic', archives: [] }, - () => { before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); const instance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance(instanceName); @@ -94,6 +90,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { } `); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/index.ts new file mode 100644 index 0000000000000..2beba223d9dc2 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/index.ts @@ -0,0 +1,24 @@ +/* + * 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('Services', () => { + loadTestFile(require.resolve('./error_groups/error_groups_detailed_statistics.spec.ts')); + loadTestFile(require.resolve('./error_groups/error_groups_main_statistics.spec.ts')); + loadTestFile(require.resolve('./service_details/service_details.spec.ts')); + loadTestFile(require.resolve('./service_icons/service_icons.spec.ts')); + loadTestFile(require.resolve('./archive_services_detailed_statistics.spec.ts')); + loadTestFile(require.resolve('./derived_annotations.spec.ts')); + loadTestFile(require.resolve('./get_service_node_metadata.spec.ts')); + loadTestFile(require.resolve('./service_alerts.spec.ts')); + loadTestFile(require.resolve('./services_detailed_statistics.spec.ts')); + loadTestFile(require.resolve('./top_services.spec.ts')); + loadTestFile(require.resolve('./transaction_types.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_alerts.spec.ts similarity index 63% rename from x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_alerts.spec.ts index e3324546c84d5..09d9176a03a0a 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_alerts.spec.ts @@ -8,23 +8,25 @@ import expect from '@kbn/expect'; import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { ApmRuleType } from '@kbn/rule-data-utils'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { waitForAlertsForRule } from '../alerts/helpers/wait_for_alerts_for_rule'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createApmRule, runRuleSoon, ApmAlertFields } from '../alerts/helpers/alerting_api_helper'; -import { waitForActiveRule } from '../alerts/helpers/wait_for_active_rule'; -import { cleanupRuleAndAlertState } from '../alerts/helpers/cleanup_rule_and_alert_state'; - -export default function ServiceAlerts({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const supertest = getService('supertest'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const es = getService('es'); +import type { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { + APM_ACTION_VARIABLE_INDEX, + APM_ALERTS_INDEX, + ApmAlertFields, +} from '../alerts/helpers/alerting_helper'; + +export default function ServiceAlerts({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const alertingApi = getService('alertingApi'); + const samlAuth = getService('samlAuth'); + const synthtrace = getService('synthtrace'); + const dayInMs = 24 * 60 * 60 * 1000; const start = Date.now() - dayInMs; const end = Date.now() + dayInMs; const goService = 'synth-go'; - const logger = getService('log'); async function getServiceAlerts({ serviceName, @@ -46,27 +48,33 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { }); } - function createRule() { - return createApmRule({ - supertest, - name: `Latency threshold | ${goService}`, - params: { - serviceName: goService, - transactionType: undefined, - windowSize: 5, - windowUnit: 'h', - threshold: 100, - aggregationType: AggregationType.Avg, - environment: 'testing', - groupBy: ['service.name', 'service.environment', 'transaction.type', 'transaction.name'], - }, - ruleTypeId: ApmRuleType.TransactionDuration, - }); - } + describe('Service alerts', () => { + let roleAuthc: RoleCredentials; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + function createRule() { + return alertingApi.createRule({ + name: `Latency threshold | ${goService}`, + params: { + serviceName: goService, + transactionType: undefined, + windowSize: 5, + windowUnit: 'h', + threshold: 100, + aggregationType: AggregationType.Avg, + environment: 'testing', + groupBy: ['service.name', 'service.environment', 'transaction.type', 'transaction.name'], + }, + ruleTypeId: ApmRuleType.TransactionDuration, + roleAuthc, + consumer: 'apm', + }); + } - // FLAKY: https://github.com/elastic/kibana/issues/177512 - registry.when('Service alerts', { config: 'basic', archives: [] }, () => { before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + const synthServices = [ apm .service({ name: goService, environment: 'testing', agentName: 'go' }) @@ -115,6 +123,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { after(async () => { await apmSynthtraceEsClient.clean(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); describe('with alerts', () => { @@ -124,20 +133,35 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { before(async () => { const createdRule = await createRule(); ruleId = createdRule.id; - alerts = await waitForAlertsForRule({ es, ruleId }); + alerts = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ALERTS_INDEX, + ruleId, + }) + ).hits.hits.map((hit) => hit._source) as ApmAlertFields[]; }); after(async () => { - await cleanupRuleAndAlertState({ es, supertest, logger }); + await alertingApi.cleanUpAlerts({ + roleAuthc, + ruleId, + alertIndexName: APM_ALERTS_INDEX, + connectorIndexName: APM_ACTION_VARIABLE_INDEX, + consumer: 'apm', + }); }); it('checks if rule is active', async () => { - const ruleStatus = await waitForActiveRule({ ruleId, supertest }); + const ruleStatus = await alertingApi.waitForRuleStatus({ + roleAuthc, + ruleId, + expectedStatus: 'active', + }); expect(ruleStatus).to.be('active'); }); it('should successfully run the rule', async () => { - const response = await runRuleSoon({ ruleId, supertest }); + const response = await alertingApi.runRule(roleAuthc, ruleId); expect(response.status).to.be(204); }); diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_details.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_details.spec.ts new file mode 100644 index 0000000000000..8fb9195bb6800 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_details.spec.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { first } from 'lodash'; +import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { dataConfig, generateData } from './generate_data'; + +type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const { + service: { name: serviceName }, + } = dataConfig; + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + async function callApi() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'production', + }, + }, + }); + } + + describe('Service details', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body).to.empty(); + }); + }); + + describe('when data is generated', () => { + let body: ServiceDetails; + let status: number; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ apmSynthtraceEsClient, start, end }); + const response = await callApi(); + body = response.body; + status = response.status; + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns correct HTTP status', () => { + expect(status).to.be(200); + }); + + it('returns correct cloud details', () => { + const { cloud } = dataConfig; + const { + provider, + availabilityZone, + region, + machineType, + projectName, + serviceName: cloudServiceName, + } = cloud; + + expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone); + expect(first(body?.cloud?.machineTypes)).to.be(machineType); + expect(body?.cloud?.provider).to.be(provider); + expect(body?.cloud?.projectName).to.be(projectName); + expect(body?.cloud?.serviceName).to.be(cloudServiceName); + expect(first(body?.cloud?.regions)).to.be(region); + }); + + it('returns correct container details', () => { + expect(body?.container?.totalNumberInstances).to.be(1); + }); + + it('returns correct serverless details', () => { + const { cloud, serverless } = dataConfig; + const { serviceName: cloudServiceName } = cloud; + const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless; + + expect(body?.serverless?.type).to.be(cloudServiceName); + expect(body?.serverless?.functionNames).to.have.length(2); + expect(body?.serverless?.functionNames).to.contain(firstFunctionName); + expect(body?.serverless?.functionNames).to.contain(secondFunctionName); + expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType); + }); + + it('returns correct service details', () => { + const { service } = dataConfig; + const { version, runtime, framework, agent } = service; + const { name: runTimeName, version: runTimeVersion } = runtime; + const { name: agentName, version: agentVersion } = agent; + + expect(body?.service?.framework).to.be(framework); + expect(body?.service?.agent.name).to.be(agentName); + expect(body?.service?.agent.version).to.be(agentVersion); + expect(body?.service?.runtime?.name).to.be(runTimeName); + expect(body?.service?.runtime?.version).to.be(runTimeVersion); + expect(first(body?.service?.versions)).to.be(version); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/service_infra_metrics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_infra_metrics.spec.ts similarity index 89% rename from x-pack/test/apm_api_integration/tests/services/service_details/service_infra_metrics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_infra_metrics.spec.ts index 194b266e927e8..92a8412961749 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_details/service_infra_metrics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_infra_metrics.spec.ts @@ -7,25 +7,31 @@ import expect from '@kbn/expect'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; +import archives_metadata from '../../../../../../../apm_api_integration/common/fixtures/es_archiver/archives_metadata'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../../constants/archiver'; type ServiceOverviewInstanceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>; type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); const archiveName = 'infra_metrics_and_apm'; + const esArchiver = getService('esArchiver'); const { start, end } = archives_metadata[archiveName]; - registry.when( - 'When data is loaded', - { config: 'basic', archives: ['infra_metrics_and_apm'] }, - () => { + describe('Service infra metrics', () => { + describe('When data is loaded', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES[archiveName]); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES[archiveName]); + }); + describe('fetch service instance', () => { it('handles empty infra metrics data for a service node', async () => { const response = await apmApiClient.readUser({ @@ -169,6 +175,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(body.kubernetes?.replicasets).to.eql([]); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/service_icons.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/service_icons.spec.ts new file mode 100644 index 0000000000000..635c6079f9c18 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/service_icons.spec.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { getServerlessTypeFromCloudData } from '@kbn/apm-plugin/common/serverless'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { dataConfig, generateData } from './generate_data'; + +type ServiceIconMetadata = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/icons'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const { serviceName } = dataConfig; + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + async function callApi() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metadata/icons', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); + } + + describe('Service icons', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body).to.empty(); + }); + }); + + describe('when data is generated', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let body: ServiceIconMetadata; + let status: number; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ apmSynthtraceEsClient, start, end }); + const response = await callApi(); + body = response.body; + status = response.status; + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns correct HTTP status', () => { + expect(status).to.be(200); + }); + + it('returns correct metadata', () => { + const { agentName, cloud } = dataConfig; + const { provider, serviceName: cloudServiceName, provider: cloudProvider } = cloud; + + expect(body.agentName).to.be(agentName); + expect(body.cloudProvider).to.be(provider); + expect(body.containerType).to.be('Kubernetes'); + expect(body.serverlessType).to.be( + getServerlessTypeFromCloudData(cloudProvider, cloudServiceName) + ); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/services_detailed_statistics.spec.ts similarity index 87% rename from x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/services_detailed_statistics.spec.ts index 0a33450e7f980..17302aed8b653 100644 --- a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/services_detailed_statistics.spec.ts @@ -13,55 +13,21 @@ import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { uniq, map } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; type ServicesDetailedStatisticsReturn = APIReturnType<'POST /internal/apm/services/detailed_statistics'>; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - - const apmApiClient = getService('apmApiClient'); - - const synthtrace = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = '2021-01-01T00:00:00.000Z'; const end = '2021-01-01T00:59:59.999Z'; const serviceNames = ['my-service']; - registry.when( - 'Services detailed statistics when data is generated', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `POST /internal/apm/services/detailed_statistics`, - params: { - query: { - start, - end, - environment: 'ENVIRONMENT_ALL', - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - _inspect: true, - }, - body: { - serviceNames: JSON.stringify(serviceNames), - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.be.empty(); - expect(response.body.previousPeriod).to.be.empty(); - }); - } - ); - async function getStats( overrides?: Partial< APIClientRequestParamsOf<'POST /internal/apm/services/detailed_statistics'>['params']['query'] @@ -90,12 +56,38 @@ export default function ApiTest({ getService }: FtrProviderContext) { return response.body; } - // FLAKY: https://github.com/elastic/kibana/issues/177511 - registry.when( - 'Services detailed statistics when data is generated', - { config: 'basic', archives: [] }, - () => { + describe('Services detailed statistics', () => { + describe('when data is not generated', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: `POST /internal/apm/services/detailed_statistics`, + params: { + query: { + start, + end, + environment: 'ENVIRONMENT_ALL', + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + _inspect: true, + }, + body: { + serviceNames: JSON.stringify(serviceNames), + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.be.empty(); + expect(response.body.previousPeriod).to.be.empty(); + }); + }); + + describe('when data is generated', () => { let servicesDetailedStatistics: ServicesDetailedStatisticsReturn; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; const instance = apm.service('my-service', 'production', 'java').instance('instance'); @@ -103,12 +95,28 @@ export default function ApiTest({ getService }: FtrProviderContext) { const EXPECTED_LATENCY = 1000; const EXPECTED_FAILURE_RATE = 0.25; + function checkStats() { + const stats = servicesDetailedStatistics.currentPeriod['my-service']; + + expect(stats).not.empty(); + + expect(uniq(map(stats.throughput, 'y'))).eql([EXPECTED_TPM], 'tpm'); + + expect(uniq(map(stats.latency, 'y'))).eql([EXPECTED_LATENCY * 1000], 'latency'); + + expect(uniq(map(stats.transactionErrorRate, 'y'))).eql( + [EXPECTED_FAILURE_RATE], + 'errorRate' + ); + } + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval( '1m' ); - await synthtrace.index([ + await apmSynthtraceEsClient.index([ interval.rate(3).generator((timestamp) => { return instance .transaction('GET /api') @@ -133,22 +141,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtrace.clean()); - - function checkStats() { - const stats = servicesDetailedStatistics.currentPeriod['my-service']; - - expect(stats).not.empty(); - - expect(uniq(map(stats.throughput, 'y'))).eql([EXPECTED_TPM], 'tpm'); - - expect(uniq(map(stats.latency, 'y'))).eql([EXPECTED_LATENCY * 1000], 'latency'); - - expect(uniq(map(stats.transactionErrorRate, 'y'))).eql( - [EXPECTED_FAILURE_RATE], - 'errorRate' - ); - } + after(() => apmSynthtraceEsClient.clean()); describe('and transaction metrics are used', () => { before(async () => { @@ -184,6 +177,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { checkStats(); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/throughput.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/throughput.spec.ts new file mode 100644 index 0000000000000..d3183916b593b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/throughput.spec.ts @@ -0,0 +1,541 @@ +/* + * 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 { apm, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { buildQueryFromFilters } from '@kbn/es-query'; +import { first, last, meanBy } from 'lodash'; +import moment from 'moment'; +import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; +import { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; + +type ThroughputReturn = APIReturnType<'GET /internal/apm/services/{serviceName}/throughput'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'synth-go'; + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + async function callApi( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/throughput'>['params'] + >, + processorEvent: 'transaction' | 'metric' = 'metric' + ) { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'synth-go', + ...overrides?.path, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + ...(processorEvent === 'metric' + ? { + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + } + : { + documentType: ApmDocumentType.TransactionEvent, + rollupInterval: RollupInterval.None, + bucketSizeInSeconds: 30, + }), + }, + }, + }); + return response; + } + describe('Throughput when data is not loaded', () => { + describe('Twhen data is not loaded', () => { + it('handles the empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.currentPeriod.length).to.be(0); + expect(response.body.previousPeriod.length).to.be(0); + }); + }); + + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + describe('Throughput chart api', () => { + const GO_PROD_RATE = 50; + const GO_DEV_RATE = 5; + const JAVA_PROD_RATE = 45; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + const serviceGoProdInstance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); + const serviceGoDevInstance = apm + .service({ name: serviceName, environment: 'development', agentName: 'go' }) + .instance('instance-b'); + + const serviceJavaInstance = apm + .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) + .instance('instance-c'); + + await apmSynthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction({ transactionName: 'GET /api/product/list' }) + .duration(1000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(GO_DEV_RATE) + .generator((timestamp) => + serviceGoDevInstance + .transaction({ transactionName: 'GET /api/product/:id' }) + .duration(1000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(JAVA_PROD_RATE) + .generator((timestamp) => + serviceJavaInstance + .transaction({ transactionName: 'POST /api/product/buy' }) + .duration(1000) + .timestamp(timestamp) + ), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('compare transactions and metrics based throughput', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; + + before(async () => { + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi({}, 'metric'), + callApi({}, 'transaction'), + ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; + }); + + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE)) + ); + }); + + it('has a bucket size of 30 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(30); + }); + + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); + }); + + describe('production environment', () => { + let throughput: ThroughputReturn; + + before(async () => { + const throughputResponse = await callApi({ query: { environment: 'production' } }); + throughput = throughputResponse.body; + }); + + it('returns some data', () => { + expect(throughput.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns correct average throughput', () => { + const throughputMean = meanBy(throughput.currentPeriod, 'y'); + expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE)); + }); + }); + + describe('when synth-java is selected', () => { + let throughput: ThroughputReturn; + + before(async () => { + const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } }); + throughput = throughputResponse.body; + }); + + it('returns some data', () => { + expect(throughput.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns throughput related to java agent', () => { + const throughputMean = meanBy(throughput.currentPeriod, 'y'); + expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE)); + }); + }); + + describe('time comparisons', () => { + let throughputResponse: ThroughputReturn; + + before(async () => { + const response = await callApi({ + query: { + start: moment(end).subtract(7, 'minutes').toISOString(), + end: new Date(end).toISOString(), + offset: '7m', + }, + }); + throughputResponse = response.body; + }); + + it('returns some data', () => { + expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); + expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0); + + const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) => + isFiniteNumber(y) + ); + const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) => + isFiniteNumber(y) + ); + + expect(hasCurrentPeriodData).to.equal(true); + expect(hasPreviousPeriodData).to.equal(true); + }); + + it('has same start time for both periods', () => { + expect(first(throughputResponse.currentPeriod)?.x).to.equal( + first(throughputResponse.previousPeriod)?.x + ); + }); + + it('has same end time for both periods', () => { + expect(last(throughputResponse.currentPeriod)?.x).to.equal( + last(throughputResponse.previousPeriod)?.x + ); + }); + + it('returns same number of buckets for both periods', () => { + expect(throughputResponse.currentPeriod.length).to.be( + throughputResponse.previousPeriod.length + ); + }); + + it('has same mean value for both periods', () => { + const currentPeriodMean = meanBy( + throughputResponse.currentPeriod.filter( + (item) => isFiniteNumber(item.y) && item.y > 0 + ), + 'y' + ); + const previousPeriodMean = meanBy( + throughputResponse.previousPeriod.filter( + (item) => isFiniteNumber(item.y) && item.y > 0 + ), + 'y' + ); + const currentPeriod = throughputResponse.currentPeriod; + const bucketSize = currentPeriod[1].x - currentPeriod[0].x; + const durationAsMinutes = bucketSize / 1000 / 60; + [currentPeriodMean, previousPeriodMean].every((value) => + expect(roundNumber(value)).to.be.equal( + roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes) + ) + ); + }); + }); + + describe('handles kuery', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; + + before(async () => { + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi( + { + query: { + kuery: 'transaction.name : "GET /api/product/list"', + }, + }, + 'metric' + ), + callApi( + { + query: { + kuery: 'transaction.name : "GET /api/product/list"', + }, + }, + 'transaction' + ), + ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; + }); + + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE)) + ); + }); + + it('has a bucket size of 30 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(30); + }); + + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); + }); + + describe('handles filters', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; + const filters = [ + { + meta: { + disabled: false, + negate: false, + alias: null, + key: 'transaction.name', + params: ['GET /api/product/list'], + type: 'phrases', + }, + query: { + bool: { + minimum_should_match: 1, + should: { + match_phrase: { + 'transaction.name': 'GET /api/product/list', + }, + }, + }, + }, + }, + ]; + const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined)); + + before(async () => { + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi( + { + query: { + filters: serializedFilters, + }, + }, + 'metric' + ), + callApi( + { + query: { + filters: serializedFilters, + }, + }, + 'transaction' + ), + ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; + }); + + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE)) + ); + }); + + it('has a bucket size of 30 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(30); + }); + + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); + }); + + describe('handles negate filters', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; + const filters = [ + { + meta: { + disabled: false, + negate: true, + alias: null, + key: 'transaction.name', + params: ['GET /api/product/list'], + type: 'phrases', + }, + query: { + bool: { + minimum_should_match: 1, + should: { + match_phrase: { + 'transaction.name': 'GET /api/product/list', + }, + }, + }, + }, + }, + ]; + const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined)); + + before(async () => { + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi( + { + query: { + filters: serializedFilters, + }, + }, + 'metric' + ), + callApi( + { + query: { + filters: serializedFilters, + }, + }, + 'transaction' + ), + ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; + }); + + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_DEV_RATE)) + ); + }); + + it('has a bucket size of 30 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(30); + }); + + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); + }); + + describe('handles bad filters request', () => { + it('throws bad request error', async () => { + try { + await callApi({ + query: { environment: 'production', filters: '{}}' }, + }); + } catch (error) { + expect(error.res.status).to.be(400); + } + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/top_services.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/top_services.spec.ts new file mode 100644 index 0000000000000..0aea10a68367a --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/top_services.spec.ts @@ -0,0 +1,364 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = '2021-10-01T00:00:00.000Z'; + const end = '2021-10-01T01:00:00.000Z'; + + describe('Top services', () => { + describe('APM Services Overview with a basic license when data is not generated', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services`, + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.items.length).to.be(0); + expect(response.body.maxCountExceeded).to.be(false); + expect(response.body.serviceOverflowCount).to.be(0); + }); + }); + + describe('APM Services Overview with a basic license when data is generated', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let response: { + status: number; + body: APIReturnType<'GET /internal/apm/services'>; + }; + + const range = timerange(new Date(start).getTime(), new Date(end).getTime()); + const transactionInterval = range.interval('1s'); + const metricInterval = range.interval('30s'); + + const errorInterval = range.interval('5s'); + + const multipleEnvServiceProdInstance = apm + .service({ name: 'multiple-env-service', environment: 'production', agentName: 'go' }) + .instance('multiple-env-service-production'); + + const multipleEnvServiceDevInstance = apm + .service({ name: 'multiple-env-service', environment: 'development', agentName: 'go' }) + .instance('multiple-env-service-development'); + + const metricOnlyInstance = apm + .service({ name: 'metric-only-service', environment: 'production', agentName: 'java' }) + .instance('metric-only-production'); + + const errorOnlyInstance = apm + .service({ name: 'error-only-service', environment: 'production', agentName: 'java' }) + .instance('error-only-production'); + + const config = { + multiple: { + prod: { + rps: 4, + duration: 1000, + }, + dev: { + rps: 1, + duration: 500, + }, + }, + }; + + function checkStats() { + const multipleEnvService = response.body.items.find( + (item) => item.serviceName === 'multiple-env-service' + ); + + const totalRps = config.multiple.prod.rps + config.multiple.dev.rps; + + expect(multipleEnvService).to.eql({ + serviceName: 'multiple-env-service', + transactionType: 'request', + environments: ['production', 'development'], + agentName: 'go', + latency: + 1000 * + ((config.multiple.prod.duration * config.multiple.prod.rps + + config.multiple.dev.duration * config.multiple.dev.rps) / + totalRps), + throughput: totalRps * 60, + transactionErrorRate: + config.multiple.dev.rps / (config.multiple.prod.rps + config.multiple.dev.rps), + }); + } + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + return apmSynthtraceEsClient.index([ + transactionInterval + .rate(config.multiple.prod.rps) + .generator((timestamp) => + multipleEnvServiceProdInstance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(config.multiple.prod.duration) + .success() + ), + transactionInterval + .rate(config.multiple.dev.rps) + .generator((timestamp) => + multipleEnvServiceDevInstance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(config.multiple.dev.duration) + .failure() + ), + transactionInterval + .rate(config.multiple.prod.rps) + .generator((timestamp) => + multipleEnvServiceDevInstance + .transaction({ transactionName: 'non-request', transactionType: 'rpc' }) + .timestamp(timestamp) + .duration(config.multiple.prod.duration) + .success() + ), + metricInterval.rate(1).generator((timestamp) => + metricOnlyInstance + .appMetrics({ + 'system.memory.actual.free': 1, + 'system.cpu.total.norm.pct': 1, + 'system.memory.total': 1, + 'system.process.cpu.total.norm.pct': 1, + }) + .timestamp(timestamp) + ), + errorInterval + .rate(1) + .generator((timestamp) => + errorOnlyInstance.error({ message: 'Foo' }).timestamp(timestamp) + ), + ]); + }); + + after(() => { + return apmSynthtraceEsClient.clean(); + }); + + describe('when no additional filters are applied', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns a successful response', () => { + expect(response.status).to.be(200); + }); + + it('returns the correct statistics', () => { + checkStats(); + }); + + it('returns services without transaction data', () => { + const serviceNames = response.body.items.map((item) => item.serviceName); + + expect(serviceNames).to.contain('metric-only-service'); + + expect(serviceNames).to.contain('error-only-service'); + }); + }); + + describe('when applying an environment filter', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: 'production', + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns data only for that environment', () => { + const multipleEnvService = response.body.items.find( + (item) => item.serviceName === 'multiple-env-service' + ); + + const totalRps = config.multiple.prod.rps; + + expect(multipleEnvService).to.eql({ + serviceName: 'multiple-env-service', + transactionType: 'request', + environments: ['production'], + agentName: 'go', + latency: 1000 * ((config.multiple.prod.duration * config.multiple.prod.rps) / totalRps), + throughput: totalRps * 60, + transactionErrorRate: 0, + }); + }); + }); + + describe('when applying a kuery filter', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: 'service.node.name:"multiple-env-service-development"', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns data for that kuery filter only', () => { + const multipleEnvService = response.body.items.find( + (item) => item.serviceName === 'multiple-env-service' + ); + + const totalRps = config.multiple.dev.rps; + + expect(multipleEnvService).to.eql({ + serviceName: 'multiple-env-service', + transactionType: 'request', + environments: ['development'], + agentName: 'go', + latency: 1000 * ((config.multiple.dev.duration * config.multiple.dev.rps) / totalRps), + throughput: totalRps * 60, + transactionErrorRate: 1, + }); + }); + }); + + describe('when excluding default transaction types', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: 'not (transaction.type:request)', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns data for the top transaction type that is not a default', () => { + const multipleEnvService = response.body.items.find( + (item) => item.serviceName === 'multiple-env-service' + ); + + expect(multipleEnvService?.transactionType).to.eql('rpc'); + }); + }); + + describe('when using service transaction metrics', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: '', + probability: 1, + documentType: ApmDocumentType.ServiceTransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns services without transaction data', () => { + const serviceNames = response.body.items.map((item) => item.serviceName); + + expect(serviceNames).to.contain('metric-only-service'); + + expect(serviceNames).to.contain('error-only-service'); + }); + + it('returns the correct statistics', () => { + checkStats(); + }); + }); + + describe('when using rolled up data', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.TenMinutes, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns the correct statistics', () => { + checkStats(); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/transaction_types.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/transaction_types.spec.ts new file mode 100644 index 0000000000000..b76f05f60631b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/transaction_types.spec.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = '2023-10-28T00:00:00.000Z'; + const end = '2023-10-28T00:14:59.999Z'; + + const serviceName = 'opbeans-node'; + + async function getTransactionTypes() { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types', + params: { + path: { serviceName }, + query: { + start, + end, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + }, + }, + }); + + return response; + } + + describe('Transaction types', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await getTransactionTypes(); + + expect(response.status).to.be(200); + + expect(response.body.transactionTypes.length).to.be(0); + }); + }); + + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval( + '1m' + ); + + const instance = apm.service(serviceName, 'production', 'node').instance('instance'); + + await apmSynthtraceEsClient.index([ + interval.rate(3).generator((timestamp) => { + return instance + .transaction({ transactionName: 'GET /api', transactionType: 'request' }) + .duration(1000) + .outcome('success') + .timestamp(timestamp); + }), + interval.rate(1).generator((timestamp) => { + return instance + .transaction({ transactionName: 'rm -rf *', transactionType: 'worker' }) + .duration(100) + .outcome('failure') + .timestamp(timestamp); + }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + it('displays available tx types', async () => { + const response = await getTransactionTypes(); + + expect(response.status).to.be(200); + expect(response.body.transactionTypes.length).to.be.greaterThan(0); + + expect(response.body.transactionTypes).to.eql(['request', 'worker']); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts index dd09804b5da83..ee1047d6024ca 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts @@ -1099,6 +1099,25 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide return body; }, + async runRule(roleAuthc: RoleCredentials, ruleId: string) { + return await retry.tryForTime(retryTimeout, async () => { + try { + const response = await supertestWithoutAuth + .post(`/internal/alerting/rule/${ruleId}/_run_soon`) + .set(samlAuth.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .expect(204); + + 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}`); + } + }); + }, + async findInRules(roleAuthc: RoleCredentials, ruleId: string) { const response = await supertestWithoutAuth .get('/api/alerting/rules/_find') diff --git a/x-pack/test/apm_api_integration/tests/services/agent.spec.ts b/x-pack/test/apm_api_integration/tests/services/agent.spec.ts deleted file mode 100644 index 69f1938192b9e..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/agent.spec.ts +++ /dev/null @@ -1,59 +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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; - - registry.when('Agent name when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/agent', - params: { - path: { serviceName: 'opbeans-node' }, - query: { - start, - end, - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body).to.eql({}); - }); - }); - - registry.when( - 'Agent name when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the agent name', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/agent', - params: { - path: { serviceName: 'opbeans-node' }, - query: { - start, - end, - }, - }, - }); - - expect(response.status).to.be(200); - - expect(response.body).to.eql({ agentName: 'nodejs', runtimeName: 'node' }); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/archive_services_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/archive_services_detailed_statistics.spec.ts index 8fed930dec356..61feba7b15d42 100644 --- a/x-pack/test/apm_api_integration/tests/services/archive_services_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/archive_services_detailed_statistics.spec.ts @@ -27,38 +27,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { start, end } = metadata; const serviceNames = ['opbeans-java', 'opbeans-go']; - registry.when( - 'Services detailed statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `POST /internal/apm/services/detailed_statistics`, - params: { - query: { - start, - end, - environment: 'ENVIRONMENT_ALL', - kuery: '', - offset: '1d', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - }, - body: { - serviceNames: JSON.stringify(serviceNames), - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.be.empty(); - expect(response.body.previousPeriod).to.be.empty(); - }); - } - ); - registry.when( 'Services detailed statistics when data is loaded', { config: 'basic', archives: [archiveName] }, diff --git a/x-pack/test/apm_api_integration/tests/services/derived_annotations.spec.ts b/x-pack/test/apm_api_integration/tests/services/derived_annotations.spec.ts deleted file mode 100644 index 6fe079cba963e..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/derived_annotations.spec.ts +++ /dev/null @@ -1,171 +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 expect from '@kbn/expect'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; - -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function annotationApiTests({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const es = getService('es'); - - const dates = [ - new Date('2021-02-01T00:00:00.000Z'), - new Date('2021-02-01T01:00:00.000Z'), - new Date('2021-02-01T02:00:00.000Z'), - new Date('2021-02-01T03:00:00.000Z'), - ]; - - const indexName = 'apm-8.0.0-transaction'; - - registry.when( - 'Derived deployment annotations with a basic license', - { config: 'basic', archives: [] }, - () => { - describe('when there are multiple service versions', () => { - let response: APIReturnType<'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31'>; - - before(async () => { - const indexExists = await es.indices.exists({ index: indexName }); - if (indexExists) { - await es.indices.delete({ - index: indexName, - }); - } - - await es.indices.create({ - index: indexName, - body: { - mappings: { - properties: { - service: { - properties: { - name: { - type: 'keyword', - }, - version: { - type: 'keyword', - }, - environment: { - type: 'keyword', - }, - }, - }, - transaction: { - properties: { - type: { - type: 'keyword', - }, - duration: { - type: 'long', - }, - }, - }, - processor: { - properties: { - event: { - type: 'keyword', - }, - }, - }, - }, - }, - }, - }); - - const docs = dates.flatMap((date, index) => { - const baseAnnotation = { - transaction: { - type: 'request', - duration: 1000000, - }, - - service: { - name: 'opbeans-java', - environment: 'production', - version: index + 1, - }, - processor: { - event: 'transaction', - }, - }; - return [ - { - ...baseAnnotation, - '@timestamp': date.toISOString(), - }, - { - ...baseAnnotation, - '@timestamp': new Date(date.getTime() + 30000), - }, - { - ...baseAnnotation, - '@timestamp': new Date(date.getTime() + 60000), - }, - ]; - }); - - await es.bulk({ - index: indexName, - body: docs.flatMap((doc) => [{ index: {} }, doc]), - refresh: true, - }); - - response = ( - await apmApiClient.readUser({ - endpoint: 'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31', - params: { - path: { - serviceName: 'opbeans-java', - }, - query: { - start: dates[1].toISOString(), - end: dates[2].toISOString(), - environment: 'production', - }, - }, - }) - ).body; - }); - - it('annotations are displayed for the service versions in the given time range', async () => { - expect(response.annotations.length).to.be(2); - expect(response.annotations[0]['@timestamp']).to.be(dates[1].getTime()); - expect(response.annotations[1]['@timestamp']).to.be(dates[2].getTime()); - - expectSnapshot(response.annotations[0]).toMatchInline(` - Object { - "@timestamp": 1612141200000, - "id": "2", - "text": "2", - "type": "version", - } - `); - }); - - it('annotations are not displayed for the service versions outside of the given time range', () => { - expect( - response.annotations.some((annotation) => { - return ( - annotation['@timestamp'] !== dates[0].getTime() && - annotation['@timestamp'] !== dates[2].getTime() - ); - }) - ); - }); - - after(async () => { - await es.indices.delete({ - index: indexName, - }); - }); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts deleted file mode 100644 index 4d8b250ec623c..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts +++ /dev/null @@ -1,121 +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 expect from '@kbn/expect'; -import { first } from 'lodash'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { dataConfig, generateData } from './generate_data'; - -type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const { - service: { name: serviceName }, - } = dataConfig; - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - async function callApi() { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details', - params: { - path: { serviceName }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'production', - }, - }, - }); - } - - registry.when( - 'Service details when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state', async () => { - const { status, body } = await callApi(); - - expect(status).to.be(200); - expect(body).to.empty(); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177663 - registry.when('Service details when data is generated', { config: 'basic', archives: [] }, () => { - let body: ServiceDetails; - let status: number; - - before(async () => { - await generateData({ apmSynthtraceEsClient, start, end }); - const response = await callApi(); - body = response.body; - status = response.status; - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('returns correct HTTP status', () => { - expect(status).to.be(200); - }); - - it('returns correct cloud details', () => { - const { cloud } = dataConfig; - const { - provider, - availabilityZone, - region, - machineType, - projectName, - serviceName: cloudServiceName, - } = cloud; - - expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone); - expect(first(body?.cloud?.machineTypes)).to.be(machineType); - expect(body?.cloud?.provider).to.be(provider); - expect(body?.cloud?.projectName).to.be(projectName); - expect(body?.cloud?.serviceName).to.be(cloudServiceName); - expect(first(body?.cloud?.regions)).to.be(region); - }); - - it('returns correct container details', () => { - expect(body?.container?.totalNumberInstances).to.be(1); - }); - - it('returns correct serverless details', () => { - const { cloud, serverless } = dataConfig; - const { serviceName: cloudServiceName } = cloud; - const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless; - - expect(body?.serverless?.type).to.be(cloudServiceName); - expect(body?.serverless?.functionNames).to.have.length(2); - expect(body?.serverless?.functionNames).to.contain(firstFunctionName); - expect(body?.serverless?.functionNames).to.contain(secondFunctionName); - expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType); - }); - - it('returns correct service details', () => { - const { service } = dataConfig; - const { version, runtime, framework, agent } = service; - const { name: runTimeName, version: runTimeVersion } = runtime; - const { name: agentName, version: agentVersion } = agent; - - expect(body?.service?.framework).to.be(framework); - expect(body?.service?.agent.name).to.be(agentName); - expect(body?.service?.agent.version).to.be(agentVersion); - expect(body?.service?.runtime?.name).to.be(runTimeName); - expect(body?.service?.runtime?.version).to.be(runTimeVersion); - expect(first(body?.service?.versions)).to.be(version); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts deleted file mode 100644 index 3516edd1800cb..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts +++ /dev/null @@ -1,77 +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 expect from '@kbn/expect'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { getServerlessTypeFromCloudData } from '@kbn/apm-plugin/common/serverless'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { dataConfig, generateData } from './generate_data'; - -type ServiceIconMetadata = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/icons'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const { serviceName } = dataConfig; - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - async function callApi() { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metadata/icons', - params: { - path: { serviceName }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - }, - }, - }); - } - - registry.when('Service icons when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const { status, body } = await callApi(); - - expect(status).to.be(200); - expect(body).to.empty(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177662 - registry.when('Service icons when data is generated', { config: 'basic', archives: [] }, () => { - let body: ServiceIconMetadata; - let status: number; - - before(async () => { - await generateData({ apmSynthtraceEsClient, start, end }); - const response = await callApi(); - body = response.body; - status = response.status; - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('returns correct HTTP status', () => { - expect(status).to.be(200); - }); - - it('returns correct metadata', () => { - const { agentName, cloud } = dataConfig; - const { provider, serviceName: cloudServiceName, provider: cloudProvider } = cloud; - - expect(body.agentName).to.be(agentName); - expect(body.cloudProvider).to.be(provider); - expect(body.containerType).to.be('Kubernetes'); - expect(body.serverlessType).to.be( - getServerlessTypeFromCloudData(cloudProvider, cloudServiceName) - ); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts deleted file mode 100644 index d8b3227890f1d..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts +++ /dev/null @@ -1,535 +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 { apm, timerange } from '@kbn/apm-synthtrace-client'; -import expect from '@kbn/expect'; -import { buildQueryFromFilters } from '@kbn/es-query'; -import { first, last, meanBy } from 'lodash'; -import moment from 'moment'; -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; - -type ThroughputReturn = APIReturnType<'GET /internal/apm/services/{serviceName}/throughput'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const serviceName = 'synth-go'; - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - async function callApi( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/throughput'>['params'] - >, - processorEvent: 'transaction' | 'metric' = 'metric' - ) { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/throughput', - params: { - path: { - serviceName: 'synth-go', - ...overrides?.path, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - kuery: '', - ...overrides?.query, - ...(processorEvent === 'metric' - ? { - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - } - : { - documentType: ApmDocumentType.TransactionEvent, - rollupInterval: RollupInterval.None, - bucketSizeInSeconds: 30, - }), - }, - }, - }); - return response; - } - - registry.when('Throughput when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await callApi(); - expect(response.status).to.be(200); - expect(response.body.currentPeriod.length).to.be(0); - expect(response.body.previousPeriod.length).to.be(0); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177510 - registry.when('Throughput when data is loaded', { config: 'basic', archives: [] }, () => { - describe('Throughput chart api', () => { - const GO_PROD_RATE = 50; - const GO_DEV_RATE = 5; - const JAVA_PROD_RATE = 45; - - before(async () => { - const serviceGoProdInstance = apm - .service({ name: serviceName, environment: 'production', agentName: 'go' }) - .instance('instance-a'); - const serviceGoDevInstance = apm - .service({ name: serviceName, environment: 'development', agentName: 'go' }) - .instance('instance-b'); - - const serviceJavaInstance = apm - .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) - .instance('instance-c'); - - await apmSynthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction({ transactionName: 'GET /api/product/list' }) - .duration(1000) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(GO_DEV_RATE) - .generator((timestamp) => - serviceGoDevInstance - .transaction({ transactionName: 'GET /api/product/:id' }) - .duration(1000) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(JAVA_PROD_RATE) - .generator((timestamp) => - serviceJavaInstance - .transaction({ transactionName: 'POST /api/product/buy' }) - .duration(1000) - .timestamp(timestamp) - ), - ]); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('compare transactions and metrics based throughput', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; - - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi({}, 'metric'), - callApi({}, 'transaction'), - ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; - }); - - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE)) - ); - }); - - it('has a bucket size of 30 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(30); - }); - - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); - }); - - describe('production environment', () => { - let throughput: ThroughputReturn; - - before(async () => { - const throughputResponse = await callApi({ query: { environment: 'production' } }); - throughput = throughputResponse.body; - }); - - it('returns some data', () => { - expect(throughput.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns correct average throughput', () => { - const throughputMean = meanBy(throughput.currentPeriod, 'y'); - expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE)); - }); - }); - - describe('when synth-java is selected', () => { - let throughput: ThroughputReturn; - - before(async () => { - const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } }); - throughput = throughputResponse.body; - }); - - it('returns some data', () => { - expect(throughput.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns throughput related to java agent', () => { - const throughputMean = meanBy(throughput.currentPeriod, 'y'); - expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE)); - }); - }); - - describe('time comparisons', () => { - let throughputResponse: ThroughputReturn; - - before(async () => { - const response = await callApi({ - query: { - start: moment(end).subtract(7, 'minutes').toISOString(), - end: new Date(end).toISOString(), - offset: '7m', - }, - }); - throughputResponse = response.body; - }); - - it('returns some data', () => { - expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); - expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0); - - const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) => - isFiniteNumber(y) - ); - const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) => - isFiniteNumber(y) - ); - - expect(hasCurrentPeriodData).to.equal(true); - expect(hasPreviousPeriodData).to.equal(true); - }); - - it('has same start time for both periods', () => { - expect(first(throughputResponse.currentPeriod)?.x).to.equal( - first(throughputResponse.previousPeriod)?.x - ); - }); - - it('has same end time for both periods', () => { - expect(last(throughputResponse.currentPeriod)?.x).to.equal( - last(throughputResponse.previousPeriod)?.x - ); - }); - - it('returns same number of buckets for both periods', () => { - expect(throughputResponse.currentPeriod.length).to.be( - throughputResponse.previousPeriod.length - ); - }); - - it('has same mean value for both periods', () => { - const currentPeriodMean = meanBy( - throughputResponse.currentPeriod.filter((item) => isFiniteNumber(item.y) && item.y > 0), - 'y' - ); - const previousPeriodMean = meanBy( - throughputResponse.previousPeriod.filter( - (item) => isFiniteNumber(item.y) && item.y > 0 - ), - 'y' - ); - const currentPeriod = throughputResponse.currentPeriod; - const bucketSize = currentPeriod[1].x - currentPeriod[0].x; - const durationAsMinutes = bucketSize / 1000 / 60; - [currentPeriodMean, previousPeriodMean].every((value) => - expect(roundNumber(value)).to.be.equal( - roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes) - ) - ); - }); - }); - - describe('handles kuery', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; - - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi( - { - query: { - kuery: 'transaction.name : "GET /api/product/list"', - }, - }, - 'metric' - ), - callApi( - { - query: { - kuery: 'transaction.name : "GET /api/product/list"', - }, - }, - 'transaction' - ), - ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; - }); - - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE)) - ); - }); - - it('has a bucket size of 30 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(30); - }); - - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); - }); - - describe('handles filters', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; - const filters = [ - { - meta: { - disabled: false, - negate: false, - alias: null, - key: 'transaction.name', - params: ['GET /api/product/list'], - type: 'phrases', - }, - query: { - bool: { - minimum_should_match: 1, - should: { - match_phrase: { - 'transaction.name': 'GET /api/product/list', - }, - }, - }, - }, - }, - ]; - const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined)); - - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi( - { - query: { - filters: serializedFilters, - }, - }, - 'metric' - ), - callApi( - { - query: { - filters: serializedFilters, - }, - }, - 'transaction' - ), - ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; - }); - - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE)) - ); - }); - - it('has a bucket size of 30 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(30); - }); - - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); - }); - - describe('handles negate filters', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; - const filters = [ - { - meta: { - disabled: false, - negate: true, - alias: null, - key: 'transaction.name', - params: ['GET /api/product/list'], - type: 'phrases', - }, - query: { - bool: { - minimum_should_match: 1, - should: { - match_phrase: { - 'transaction.name': 'GET /api/product/list', - }, - }, - }, - }, - }, - ]; - const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined)); - - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi( - { - query: { - filters: serializedFilters, - }, - }, - 'metric' - ), - callApi( - { - query: { - filters: serializedFilters, - }, - }, - 'transaction' - ), - ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; - }); - - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_DEV_RATE)) - ); - }); - - it('has a bucket size of 30 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(30); - }); - - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); - }); - - describe('handles bad filters request', () => { - it('throws bad request error', async () => { - try { - await callApi({ - query: { environment: 'production', filters: '{}}' }, - }); - } catch (error) { - expect(error.res.status).to.be(400); - } - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts index 48b394c092638..41012d8836268 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; @@ -20,7 +19,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('apmSynthtraceEsClient'); const archiveName = 'apm_8.0.0'; @@ -30,355 +28,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { const archiveStart = archiveRange.start; const archiveEnd = archiveRange.end; - const start = '2021-10-01T00:00:00.000Z'; - const end = '2021-10-01T01:00:00.000Z'; - - registry.when( - 'APM Services Overview with a basic license when data is not generated', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services`, - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body.items.length).to.be(0); - expect(response.body.maxCountExceeded).to.be(false); - expect(response.body.serviceOverflowCount).to.be(0); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177509 - registry.when( - 'APM Services Overview with a basic license when data is generated', - { config: 'basic', archives: [] }, - () => { - let response: { - status: number; - body: APIReturnType<'GET /internal/apm/services'>; - }; - - const range = timerange(new Date(start).getTime(), new Date(end).getTime()); - const transactionInterval = range.interval('1s'); - const metricInterval = range.interval('30s'); - - const errorInterval = range.interval('5s'); - - const multipleEnvServiceProdInstance = apm - .service({ name: 'multiple-env-service', environment: 'production', agentName: 'go' }) - .instance('multiple-env-service-production'); - - const multipleEnvServiceDevInstance = apm - .service({ name: 'multiple-env-service', environment: 'development', agentName: 'go' }) - .instance('multiple-env-service-development'); - - const metricOnlyInstance = apm - .service({ name: 'metric-only-service', environment: 'production', agentName: 'java' }) - .instance('metric-only-production'); - - const errorOnlyInstance = apm - .service({ name: 'error-only-service', environment: 'production', agentName: 'java' }) - .instance('error-only-production'); - - const config = { - multiple: { - prod: { - rps: 4, - duration: 1000, - }, - dev: { - rps: 1, - duration: 500, - }, - }, - }; - - function checkStats() { - const multipleEnvService = response.body.items.find( - (item) => item.serviceName === 'multiple-env-service' - ); - - const totalRps = config.multiple.prod.rps + config.multiple.dev.rps; - - expect(multipleEnvService).to.eql({ - serviceName: 'multiple-env-service', - transactionType: 'request', - environments: ['production', 'development'], - agentName: 'go', - latency: - 1000 * - ((config.multiple.prod.duration * config.multiple.prod.rps + - config.multiple.dev.duration * config.multiple.dev.rps) / - totalRps), - throughput: totalRps * 60, - transactionErrorRate: - config.multiple.dev.rps / (config.multiple.prod.rps + config.multiple.dev.rps), - }); - } - - before(async () => { - return synthtrace.index([ - transactionInterval - .rate(config.multiple.prod.rps) - .generator((timestamp) => - multipleEnvServiceProdInstance - .transaction({ transactionName: 'GET /api' }) - .timestamp(timestamp) - .duration(config.multiple.prod.duration) - .success() - ), - transactionInterval - .rate(config.multiple.dev.rps) - .generator((timestamp) => - multipleEnvServiceDevInstance - .transaction({ transactionName: 'GET /api' }) - .timestamp(timestamp) - .duration(config.multiple.dev.duration) - .failure() - ), - transactionInterval - .rate(config.multiple.prod.rps) - .generator((timestamp) => - multipleEnvServiceDevInstance - .transaction({ transactionName: 'non-request', transactionType: 'rpc' }) - .timestamp(timestamp) - .duration(config.multiple.prod.duration) - .success() - ), - metricInterval.rate(1).generator((timestamp) => - metricOnlyInstance - .appMetrics({ - 'system.memory.actual.free': 1, - 'system.cpu.total.norm.pct': 1, - 'system.memory.total': 1, - 'system.process.cpu.total.norm.pct': 1, - }) - .timestamp(timestamp) - ), - errorInterval - .rate(1) - .generator((timestamp) => - errorOnlyInstance.error({ message: 'Foo' }).timestamp(timestamp) - ), - ]); - }); - - after(() => { - return synthtrace.clean(); - }); - - describe('when no additional filters are applied', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns a successful response', () => { - expect(response.status).to.be(200); - }); - - it('returns the correct statistics', () => { - checkStats(); - }); - - it('returns services without transaction data', () => { - const serviceNames = response.body.items.map((item) => item.serviceName); - - expect(serviceNames).to.contain('metric-only-service'); - - expect(serviceNames).to.contain('error-only-service'); - }); - }); - - describe('when applying an environment filter', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: 'production', - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns data only for that environment', () => { - const multipleEnvService = response.body.items.find( - (item) => item.serviceName === 'multiple-env-service' - ); - - const totalRps = config.multiple.prod.rps; - - expect(multipleEnvService).to.eql({ - serviceName: 'multiple-env-service', - transactionType: 'request', - environments: ['production'], - agentName: 'go', - latency: 1000 * ((config.multiple.prod.duration * config.multiple.prod.rps) / totalRps), - throughput: totalRps * 60, - transactionErrorRate: 0, - }); - }); - }); - - describe('when applying a kuery filter', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: 'service.node.name:"multiple-env-service-development"', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns data for that kuery filter only', () => { - const multipleEnvService = response.body.items.find( - (item) => item.serviceName === 'multiple-env-service' - ); - - const totalRps = config.multiple.dev.rps; - - expect(multipleEnvService).to.eql({ - serviceName: 'multiple-env-service', - transactionType: 'request', - environments: ['development'], - agentName: 'go', - latency: 1000 * ((config.multiple.dev.duration * config.multiple.dev.rps) / totalRps), - throughput: totalRps * 60, - transactionErrorRate: 1, - }); - }); - }); - - describe('when excluding default transaction types', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: 'not (transaction.type:request)', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns data for the top transaction type that is not a default', () => { - const multipleEnvService = response.body.items.find( - (item) => item.serviceName === 'multiple-env-service' - ); - - expect(multipleEnvService?.transactionType).to.eql('rpc'); - }); - }); - - describe('when using service transaction metrics', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: '', - probability: 1, - documentType: ApmDocumentType.ServiceTransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns services without transaction data', () => { - const serviceNames = response.body.items.map((item) => item.serviceName); - - expect(serviceNames).to.contain('metric-only-service'); - - expect(serviceNames).to.contain('error-only-service'); - }); - - it('returns the correct statistics', () => { - checkStats(); - }); - }); - - describe('when using rolled up data', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.TenMinutes, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns the correct statistics', () => { - checkStats(); - }); - }); - } - ); - registry.when( 'APM Services Overview with a trial license when data is loaded', { config: 'trial', archives: [archiveName] }, diff --git a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts b/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts deleted file mode 100644 index 57dbbb1c6ee49..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts +++ /dev/null @@ -1,92 +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 expect from '@kbn/expect'; -import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('apmSynthtraceEsClient'); - - const start = '2023-10-28T00:00:00.000Z'; - const end = '2023-10-28T00:14:59.999Z'; - - const serviceName = 'opbeans-node'; - - async function getTransactionTypes() { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types', - params: { - path: { serviceName }, - query: { - start, - end, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - }, - }, - }); - - return response; - } - - registry.when( - 'Transaction types when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state', async () => { - const response = await getTransactionTypes(); - - expect(response.status).to.be(200); - - expect(response.body.transactionTypes.length).to.be(0); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177521 - registry.when('Transaction types when data is loaded', { config: 'basic', archives: [] }, () => { - before(async () => { - const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval( - '1m' - ); - - const instance = apm.service(serviceName, 'production', 'node').instance('instance'); - - await synthtrace.index([ - interval.rate(3).generator((timestamp) => { - return instance - .transaction({ transactionName: 'GET /api', transactionType: 'request' }) - .duration(1000) - .outcome('success') - .timestamp(timestamp); - }), - interval.rate(1).generator((timestamp) => { - return instance - .transaction({ transactionName: 'rm -rf *', transactionType: 'worker' }) - .duration(100) - .outcome('failure') - .timestamp(timestamp); - }), - ]); - }); - - after(() => synthtrace.clean()); - it('displays available tx types', async () => { - const response = await getTransactionTypes(); - - expect(response.status).to.be(200); - expect(response.body.transactionTypes.length).to.be.greaterThan(0); - - expect(response.body.transactionTypes).to.eql(['request', 'worker']); - }); - }); -}