diff --git a/x-pack/test/apm_api_integration/tests/custom_dashboards/api_helper.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/api_helper.ts similarity index 93% rename from x-pack/test/apm_api_integration/tests/custom_dashboards/api_helper.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/api_helper.ts index a0fb0e976d10..0c75efa8ad9b 100644 --- a/x-pack/test/apm_api_integration/tests/custom_dashboards/api_helper.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/api_helper.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { ApmApiClient } from '../../common/config'; +import { ApmApiProvider } from '../../../../services/apm_api'; + +export type ApmApiClient = ReturnType; export async function getServiceDashboardApi( apmApiClient: ApmApiClient, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/custom_dashboards.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/custom_dashboards.spec.ts new file mode 100644 index 000000000000..51013ff171bb --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/custom_dashboards.spec.ts @@ -0,0 +1,193 @@ +/* + * 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 type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { + getServiceDashboardApi, + getLinkServiceDashboardApi, + deleteAllServiceDashboard, +} from './api_helper'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = '2023-08-22T00:00:00.000Z'; + const end = '2023-08-22T00:15:00.000Z'; + + describe('Service dashboards', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await getServiceDashboardApi(apmApiClient, 'synth-go', start, end); + expect(response.status).to.be(200); + expect(response.body.serviceDashboards).to.eql([]); + }); + }); + + describe('when data is loaded', () => { + const range = timerange(new Date(start).getTime(), new Date(end).getTime()); + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + const goInstance = apm + .service({ + name: 'synth-go', + environment: 'production', + agentName: 'go', + }) + .instance('go-instance'); + + const javaInstance = apm + .service({ + name: 'synth-java', + environment: 'production', + agentName: 'java', + }) + .instance('java-instance'); + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + return apmSynthtraceEsClient.index([ + range + .interval('1s') + .rate(4) + .generator((timestamp) => + goInstance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(1000) + .success() + ), + range + .interval('1s') + .rate(4) + .generator((timestamp) => + javaInstance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(1000) + .success() + ), + ]); + }); + + after(() => { + return apmSynthtraceEsClient.clean(); + }); + + afterEach(async () => { + await deleteAllServiceDashboard(apmApiClient, 'synth-go', start, end); + }); + + describe('and when data is not loaded', () => { + it('creates a new service dashboard', async () => { + const serviceDashboard = { + dashboardSavedObjectId: 'dashboard-saved-object-id', + serviceFiltersEnabled: true, + kuery: 'service.name: synth-go', + }; + const createResponse = await getLinkServiceDashboardApi({ + apmApiClient, + ...serviceDashboard, + }); + expect(createResponse.status).to.be(200); + expect(createResponse.body).to.have.property('id'); + expect(createResponse.body).to.have.property('updatedAt'); + + expect(createResponse.body).to.have.property( + 'dashboardSavedObjectId', + serviceDashboard.dashboardSavedObjectId + ); + expect(createResponse.body).to.have.property('kuery', serviceDashboard.kuery); + expect(createResponse.body).to.have.property( + 'serviceEnvironmentFilterEnabled', + serviceDashboard.serviceFiltersEnabled + ); + expect(createResponse.body).to.have.property( + 'serviceNameFilterEnabled', + serviceDashboard.serviceFiltersEnabled + ); + + const dasboardForGoService = await getServiceDashboardApi( + apmApiClient, + 'synth-go', + start, + end + ); + const dashboardForJavaService = await getServiceDashboardApi( + apmApiClient, + 'synth-java', + start, + end + ); + expect(dashboardForJavaService.body.serviceDashboards.length).to.be(0); + expect(dasboardForGoService.body.serviceDashboards.length).to.be(1); + }); + + it('updates the existing linked service dashboard', async () => { + const serviceDashboard = { + dashboardSavedObjectId: 'dashboard-saved-object-id', + serviceFiltersEnabled: true, + kuery: 'service.name: synth-go or agent.name: java', + }; + + await getLinkServiceDashboardApi({ + apmApiClient, + ...serviceDashboard, + }); + + const dasboardForGoService = await getServiceDashboardApi( + apmApiClient, + 'synth-go', + start, + end + ); + + const updateResponse = await getLinkServiceDashboardApi({ + apmApiClient, + customDashboardId: dasboardForGoService.body.serviceDashboards[0].id, + ...serviceDashboard, + serviceFiltersEnabled: true, + }); + + expect(updateResponse.status).to.be(200); + + const updateddasboardForGoService = await getServiceDashboardApi( + apmApiClient, + 'synth-go', + start, + end + ); + expect(updateddasboardForGoService.body.serviceDashboards.length).to.be(1); + expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property( + 'serviceEnvironmentFilterEnabled', + true + ); + expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property( + 'serviceNameFilterEnabled', + true + ); + expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property( + 'kuery', + 'service.name: synth-go or agent.name: java' + ); + + const dashboardForJavaService = await getServiceDashboardApi( + apmApiClient, + 'synth-java', + start, + end + ); + expect(dashboardForJavaService.body.serviceDashboards.length).to.be(1); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts new file mode 100644 index 000000000000..77f12ca0f88d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts @@ -0,0 +1,14 @@ +/* + * 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('custom_dashboards', () => { + loadTestFile(require.resolve('./custom_dashboards.spec.ts')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/dependency_metrics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/dependency_metrics.spec.ts new file mode 100644 index 000000000000..2821807ef2c2 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/dependency_metrics.spec.ts @@ -0,0 +1,306 @@ +/* + * 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 { sum } from 'lodash'; +import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; +import { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; +import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { SupertestReturnType } from '../../../../services/apm_api'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; +import { generateOperationData, generateOperationDataConfig } from './generate_operation_data'; + +const { + ES_BULK_DURATION, + ES_BULK_RATE, + ES_SEARCH_DURATION, + ES_SEARCH_FAILURE_RATE, + ES_SEARCH_SUCCESS_RATE, + ES_SEARCH_UNKNOWN_RATE, + REDIS_SET_RATE, +} = generateOperationDataConfig; + +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; + + async function callApi({ + dependencyName, + searchServiceDestinationMetrics, + spanName = '', + metric, + kuery = '', + environment = ENVIRONMENT_ALL.value, + }: { + dependencyName: string; + searchServiceDestinationMetrics: boolean; + spanName?: string; + metric: TMetricName; + kuery?: string; + environment?: string; + }): Promise> { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/dependencies/charts/${ + metric as 'latency' | 'throughput' | 'error_rate' + }`, + params: { + query: { + dependencyName, + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment, + kuery, + offset: '', + spanName, + searchServiceDestinationMetrics, + }, + }, + }); + } + + function avg(coordinates: Coordinate[]) { + const values = coordinates + .filter((coord): coord is { x: number; y: number } => isFiniteNumber(coord.y)) + .map((coord) => coord.y); + + return roundNumber(sum(values) / values.length); + } + + describe('Dependency metrics', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const { body, status } = await callApi({ + dependencyName: 'elasticsearch', + metric: 'latency', + searchServiceDestinationMetrics: true, + }); + + expect(status).to.be(200); + expect(body.currentTimeseries.filter((val) => isFiniteNumber(val.y))).to.empty(); + expect( + (body.comparisonTimeseries || [])?.filter((val) => isFiniteNumber(val.y)) + ).to.empty(); + }); + }); + + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + await generateOperationData({ + apmSynthtraceEsClient, + start, + end, + }); + }); + + describe('without spanName', () => { + describe('without a kuery or environment', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'latency', + }); + + const searchRate = + ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const bulkRate = ES_BULK_RATE; + + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); + + it('returns the correct throughput', async () => { + const response = await callApi({ + dependencyName: 'redis', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'throughput', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); + }); + + it('returns the correct failure rate', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'error_rate', + }); + + const expectedErrorRate = + ES_SEARCH_FAILURE_RATE / (ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE); + + expect(avg(response.body.currentTimeseries)).to.eql(expectedErrorRate); + }); + }); + + describe('with a kuery', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'latency', + kuery: `event.outcome:unknown`, + }); + + const searchRate = ES_SEARCH_UNKNOWN_RATE; + const bulkRate = ES_BULK_RATE; + + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); + + it('returns the correct throughput', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'throughput', + kuery: `event.outcome:unknown`, + }); + + const searchRate = ES_SEARCH_UNKNOWN_RATE; + const bulkRate = ES_BULK_RATE; + + expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); + }); + + it('returns the correct failure rate', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'error_rate', + kuery: 'event.outcome:success', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(0); + }); + }); + + describe('with an environment', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'latency', + environment: 'production', + }); + + const searchRate = ES_SEARCH_UNKNOWN_RATE; + const bulkRate = 0; + + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); + + it('returns the correct throughput', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'throughput', + environment: 'production', + }); + + const searchRate = + ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const bulkRate = 0; + + expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); + }); + + it('returns the correct failure rate', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'error_rate', + environment: 'development', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(null); + }); + }); + }); + + describe('with spanName', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: false, + spanName: '/_search', + metric: 'latency', + }); + + const searchRate = + ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const bulkRate = 0; + + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); + + it('returns the correct throughput', async () => { + const response = await callApi({ + dependencyName: 'redis', + searchServiceDestinationMetrics: false, + spanName: 'SET', + metric: 'throughput', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); + }); + + it('returns the correct failure rate', async () => { + const response = await callApi({ + dependencyName: 'elasticsearch', + searchServiceDestinationMetrics: false, + spanName: '/_bulk', + metric: 'error_rate', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(null); + }); + }); + + after(() => apmSynthtraceEsClient.clean()); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/generate_data.ts diff --git a/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/generate_operation_data.ts similarity index 97% rename from x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/generate_operation_data.ts index 97d7c35e2373..be6ca630973b 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/generate_operation_data.ts @@ -5,7 +5,7 @@ * 2.0. */ import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export const generateOperationDataConfig = { ES_SEARCH_DURATION: 100, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts new file mode 100644 index 000000000000..2acf449ce923 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts @@ -0,0 +1,20 @@ +/* + * 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('custom_dashboards', () => { + loadTestFile(require.resolve('./dependency_metrics.spec.ts')); + loadTestFile(require.resolve('./metadata.spec.ts')); + loadTestFile(require.resolve('./service_dependencies.spec.ts')); + loadTestFile(require.resolve('./top_dependencies.spec.ts')); + loadTestFile(require.resolve('./top_operations.spec.ts')); + loadTestFile(require.resolve('./top_spans.spec.ts')); + loadTestFile(require.resolve('./upstream_services.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/metadata.spec.ts similarity index 69% rename from x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/metadata.spec.ts index 33b81c859911..849cb1529058 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/metadata.spec.ts @@ -5,13 +5,13 @@ * 2.0. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { dataConfig, generateData } from './generate_data'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -29,24 +29,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Dependency metadata when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Dependency metadata', () => { + describe('when data is not loaded', () => { it('handles empty state', async () => { const { status, body } = await callApi(); expect(status).to.be(200); expect(body.metadata).to.empty(); }); - } - ); + }); + + describe('when data is generated', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); - // FLAKY: https://github.com/elastic/kibana/issues/177122 - registry.when( - 'Dependency metadata when data is generated', - { config: 'basic', archives: [] }, - () => { after(() => apmSynthtraceEsClient.clean()); it('returns correct metadata for the dependency', async () => { @@ -61,6 +60,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { await apmSynthtraceEsClient.clean(); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/service_dependencies.spec.ts similarity index 52% rename from x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/service_dependencies.spec.ts index be6b0898030c..4105989e5509 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/service_dependencies.spec.ts @@ -6,13 +6,13 @@ */ import expect from '@kbn/expect'; import { DependencyNode } from '@kbn/apm-plugin/common/connections'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateData } from './generate_data'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; const dependencyName = 'elasticsearch'; @@ -34,61 +34,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Dependency for service when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Dependency for service', () => { + describe('when data is not loaded', () => { it('handles empty state #1', async () => { const { status, body } = await callApi(); expect(status).to.be(200); expect(body.serviceDependencies).to.empty(); }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177123 - registry.when('Dependency for services', { config: 'basic', archives: [] }, () => { - describe('when data is loaded', () => { - before(async () => { - await generateData({ apmSynthtraceEsClient, start, end }); - }); - after(() => apmSynthtraceEsClient.clean()); - - it('returns a list of dependencies for a service', async () => { - const { status, body } = await callApi(); - - expect(status).to.be(200); - expect( - body.serviceDependencies.map( - ({ location }) => (location as DependencyNode).dependencyName - ) - ).to.eql([dependencyName]); - - const currentStatsLatencyValues = - body.serviceDependencies[0].currentStats.latency.timeseries; - expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); - }); }); - }); - - registry.when( - 'Dependency for service breakdown when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state #2', async () => { - const { status, body } = await callApi(); - expect(status).to.be(200); - expect(body.serviceDependencies).to.empty(); - }); - } - ); + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; - // FLAKY: https://github.com/elastic/kibana/issues/177125 - registry.when('Dependency for services breakdown', { config: 'basic', archives: [] }, () => { - describe('when data is loaded - breakdown', () => { before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ apmSynthtraceEsClient, start, end }); }); after(() => apmSynthtraceEsClient.clean()); diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts similarity index 86% rename from x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts index acdea5a3d54d..0fa88b67d337 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts @@ -7,16 +7,16 @@ import expect from '@kbn/expect'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { NodeType, DependencyNode } from '@kbn/apm-plugin/common/connections'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { dataConfig, generateData } from './generate_data'; -import { roundNumber } from '../../utils'; +import { roundNumber } from '../utils/common'; type TopDependencies = APIReturnType<'GET /internal/apm/dependencies/top_dependencies'>; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -37,24 +37,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Top dependencies when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Top dependencies', () => { + describe('when data is not loaded', () => { it('handles empty state', async () => { const { status, body } = await callApi(); expect(status).to.be(200); expect(body.dependencies).to.empty(); }); - } - ); + }); - // FLAKY: https://github.com/elastic/kibana/issues/177126 - registry.when('Top dependencies', { config: 'basic', archives: [] }, () => { describe('when data is generated', () => { let topDependencies: TopDependencies; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ apmSynthtraceEsClient, start, end }); const response = await callApi(); topDependencies = response.body; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_operations.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_operations.spec.ts new file mode 100644 index 000000000000..10863eca07cf --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_operations.spec.ts @@ -0,0 +1,280 @@ +/* + * 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 { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; +import { ValuesType } from 'utility-types'; +import { DependencyOperation } from '@kbn/apm-plugin/server/routes/dependencies/get_top_dependency_operations'; +import { meanBy } from 'lodash'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; +import { generateOperationData, generateOperationDataConfig } from './generate_operation_data'; + +type TopOperations = APIReturnType<'GET /internal/apm/dependencies/operations'>['operations']; + +const { + ES_BULK_DURATION, + ES_BULK_RATE, + ES_SEARCH_DURATION, + ES_SEARCH_FAILURE_RATE, + ES_SEARCH_SUCCESS_RATE, + ES_SEARCH_UNKNOWN_RATE, + REDIS_SET_DURATION, + REDIS_SET_RATE, +} = generateOperationDataConfig; + +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; + + async function callApi({ + dependencyName, + environment = ENVIRONMENT_ALL.value, + kuery = '', + searchServiceDestinationMetrics = false, + }: { + dependencyName: string; + environment?: string; + kuery?: string; + searchServiceDestinationMetrics?: boolean; + }) { + return await apmApiClient + .readUser({ + endpoint: 'GET /internal/apm/dependencies/operations', + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment, + kuery, + dependencyName, + searchServiceDestinationMetrics, + }, + }, + }) + .then(({ body }) => body.operations); + } + + describe('Top operations', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const operations = await callApi({ dependencyName: 'elasticsearch' }); + expect(operations).to.empty(); + }); + }); + + describe('when data is generated', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + return generateOperationData({ + apmSynthtraceEsClient, + start, + end, + }); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('requested for elasticsearch', () => { + let response: TopOperations; + let searchOperation: ValuesType; + let bulkOperation: ValuesType; + + before(async () => { + response = await callApi({ dependencyName: 'elasticsearch' }); + searchOperation = response.find((op) => op.spanName === '/_search')!; + bulkOperation = response.find((op) => op.spanName === '/_bulk')!; + }); + + it('returns the correct operations', () => { + expect(response.length).to.eql(2); + + expect(searchOperation).to.be.ok(); + expect(bulkOperation).to.be.ok(); + }); + + it('returns the correct latency', () => { + expect(searchOperation.latency).to.eql(ES_SEARCH_DURATION * 1000); + expect(bulkOperation.latency).to.eql(ES_BULK_DURATION * 1000); + }); + + it('returns the correct throughput', () => { + const expectedSearchThroughput = roundNumber( + ES_SEARCH_UNKNOWN_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE + ); + const expectedBulkThroughput = ES_BULK_RATE; + + expect(roundNumber(searchOperation.throughput)).to.eql(expectedSearchThroughput); + expect(roundNumber(bulkOperation.throughput)).to.eql(expectedBulkThroughput); + + expect( + searchOperation.timeseries.throughput + .map((bucket) => bucket.y) + .every((val) => val === expectedSearchThroughput) + ); + }); + + it('returns the correct failure rate', () => { + const expectedSearchFailureRate = + ES_SEARCH_FAILURE_RATE / (ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE); + const expectedBulkFailureRate = null; + + expect(searchOperation.failureRate).to.be(expectedSearchFailureRate); + + expect(bulkOperation.failureRate).to.be(expectedBulkFailureRate); + + expect( + searchOperation.timeseries.failureRate + .map((bucket) => bucket.y) + .every((val) => val === expectedSearchFailureRate) + ); + + expect( + bulkOperation.timeseries.failureRate + .map((bucket) => bucket.y) + .every((val) => val === expectedBulkFailureRate) + ); + }); + + it('returns the correct impact', () => { + expect(searchOperation.impact).to.eql(0); + expect(bulkOperation.impact).to.eql(100); + }); + }); + + describe('requested for redis', () => { + let response: TopOperations; + let setOperation: ValuesType; + + before(async () => { + response = await callApi({ dependencyName: 'redis' }); + setOperation = response.find((op) => op.spanName === 'SET')!; + }); + + it('returns the correct operations', () => { + expect(response.length).to.eql(1); + + expect(setOperation).to.be.ok(); + }); + + it('returns the correct latency', () => { + expect(setOperation.latency).to.eql(REDIS_SET_DURATION * 1000); + }); + + it('returns the correct throughput', () => { + expect(roundNumber(setOperation.throughput)).to.eql(roundNumber(REDIS_SET_RATE)); + }); + }); + + describe('requested for a specific service', () => { + let response: TopOperations; + let searchOperation: ValuesType; + let bulkOperation: ValuesType | undefined; + + before(async () => { + response = await callApi({ + dependencyName: 'elasticsearch', + kuery: `service.name:"synth-go"`, + }); + searchOperation = response.find((op) => op.spanName === '/_search')!; + bulkOperation = response.find((op) => op.spanName === '/_bulk'); + }); + + it('returns the correct operations', () => { + expect(response.length).to.eql(1); + + expect(searchOperation).to.be.ok(); + expect(bulkOperation).not.to.be.ok(); + }); + }); + + describe('requested for a specific environment', () => { + let response: TopOperations; + let searchOperation: ValuesType | undefined; + let bulkOperation: ValuesType; + + before(async () => { + response = await callApi({ + dependencyName: 'elasticsearch', + environment: 'development', + }); + searchOperation = response.find((op) => op.spanName === '/_search'); + bulkOperation = response.find((op) => op.spanName === '/_bulk')!; + }); + + it('returns the correct operations', () => { + expect(response.length).to.eql(1); + + expect(searchOperation).not.to.be.ok(); + expect(bulkOperation).to.be.ok(); + }); + }); + + describe('Compare span metrics and span events', () => { + let bulkOperationSpanEventsResponse: ValuesType; + let bulkOperationSpanMetricsResponse: ValuesType; + + before(async () => { + const [spanEventsResponse, spanMetricsResponse] = await Promise.all([ + callApi({ dependencyName: 'elasticsearch', searchServiceDestinationMetrics: false }), + callApi({ dependencyName: 'elasticsearch', searchServiceDestinationMetrics: true }), + ]); + function findBulkOperation(op: DependencyOperation) { + return op.spanName === '/_bulk'; + } + bulkOperationSpanEventsResponse = spanEventsResponse.find(findBulkOperation)!; + bulkOperationSpanMetricsResponse = spanMetricsResponse.find(findBulkOperation)!; + }); + + it('returns same latency', () => { + expect(bulkOperationSpanEventsResponse.latency).to.eql( + bulkOperationSpanMetricsResponse.latency + ); + + const meanSpanMetrics = meanBy( + bulkOperationSpanEventsResponse.timeseries.latency.filter(({ y }) => y !== null), + 'y' + ); + const meanSpanEvents = meanBy( + bulkOperationSpanMetricsResponse.timeseries.latency.filter(({ y }) => y !== null), + 'y' + ); + expect(meanSpanMetrics).to.eql(meanSpanEvents); + }); + + it('returns same throughput', () => { + expect(bulkOperationSpanEventsResponse.throughput).to.eql( + bulkOperationSpanMetricsResponse.throughput + ); + + const meanSpanMetrics = meanBy( + bulkOperationSpanEventsResponse.timeseries.throughput.filter(({ y }) => y !== 0), + 'y' + ); + const meanSpanEvents = meanBy( + bulkOperationSpanMetricsResponse.timeseries.throughput.filter(({ y }) => y !== 0), + 'y' + ); + expect(meanSpanMetrics).to.eql(meanSpanEvents); + }); + + it('returns same impact', () => { + expect(bulkOperationSpanEventsResponse.impact).to.eql( + bulkOperationSpanMetricsResponse.impact + ); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_spans.spec.ts similarity index 92% rename from x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_spans.spec.ts index 1002f6dc09ee..c7e5907edad0 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_spans.spec.ts @@ -8,12 +8,12 @@ import expect from '@kbn/expect'; import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { omit, uniq } from 'lodash'; -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 registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -50,10 +50,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Top dependency spans when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Top dependency spans', () => { + describe('when data is not loaded', () => { it('handles empty state', async () => { const { body, status } = await callApi({ dependencyName: 'elasticsearch', @@ -63,14 +61,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(status).to.be(200); expect(body.spans).to.empty(); }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177135 - registry.when( - 'Top dependency spans when data is loaded', - { config: 'basic', archives: [] }, - () => { + }); + + describe('when data is loaded', () => { const javaInstance = apm .service({ name: 'java', environment: 'production', agentName: 'java' }) .instance('instance-a'); @@ -78,8 +71,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { const goInstance = apm .service({ name: 'go', environment: 'development', agentName: 'go' }) .instance('instance-a'); + let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') @@ -240,6 +236,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); after(() => apmSynthtraceEsClient.clean()); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/upstream_services.spec.ts similarity index 76% rename from x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/upstream_services.spec.ts index 1a7e958881d9..42d0a66c31a8 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/upstream_services.spec.ts @@ -6,13 +6,13 @@ */ import expect from '@kbn/expect'; import { ServiceNode } from '@kbn/apm-plugin/common/connections'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateData } from './generate_data'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; const dependencyName = 'elasticsearch'; @@ -34,23 +34,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Dependency upstream services when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Dependency upstream services', () => { + describe('when data is not loaded', () => { it('handles empty state', async () => { const { status, body } = await callApi(); expect(status).to.be(200); expect(body.services).to.empty(); }); - } - ); + }); - // FLAKY: https://github.com/elastic/kibana/issues/177137 - registry.when('Dependency upstream services', { config: 'basic', archives: [] }, () => { describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ apmSynthtraceEsClient, start, end }); }); after(() => apmSynthtraceEsClient.clean()); 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 a62c11d40b1a..abc7f72945e2 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -12,5 +12,7 @@ export default function apmApiIntegrationTests({ }: DeploymentAgnosticFtrProviderContext) { describe('APM', function () { loadTestFile(require.resolve('./agent_explorer')); + loadTestFile(require.resolve('./custom_dashboards')); + loadTestFile(require.resolve('./dependencies')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts new file mode 100644 index 000000000000..8f5ff9822e22 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts @@ -0,0 +1,13 @@ +/* + * 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 { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; +import { Maybe } from '@kbn/apm-plugin/typings/common'; + +export function roundNumber(num: Maybe) { + return isFiniteNumber(num) ? Number(num.toPrecision(4)) : null; +} diff --git a/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts b/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts deleted file mode 100644 index cc957bfc4771..000000000000 --- a/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts +++ /dev/null @@ -1,195 +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 { apm, timerange } from '@kbn/apm-synthtrace-client'; - -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - getServiceDashboardApi, - getLinkServiceDashboardApi, - deleteAllServiceDashboard, -} from './api_helper'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('apmSynthtraceEsClient'); - - const start = '2023-08-22T00:00:00.000Z'; - const end = '2023-08-22T00:15:00.000Z'; - - registry.when( - 'Service dashboards when data is not loaded', - { config: 'basic', archives: [] }, - () => { - describe('when data is not loaded', () => { - it('handles empty state', async () => { - const response = await getServiceDashboardApi(apmApiClient, 'synth-go', start, end); - expect(response.status).to.be(200); - expect(response.body.serviceDashboards).to.eql([]); - }); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177119 - registry.when('Service dashboards when data is loaded', { config: 'basic', archives: [] }, () => { - const range = timerange(new Date(start).getTime(), new Date(end).getTime()); - - const goInstance = apm - .service({ - name: 'synth-go', - environment: 'production', - agentName: 'go', - }) - .instance('go-instance'); - - const javaInstance = apm - .service({ - name: 'synth-java', - environment: 'production', - agentName: 'java', - }) - .instance('java-instance'); - - before(async () => { - return synthtrace.index([ - range - .interval('1s') - .rate(4) - .generator((timestamp) => - goInstance - .transaction({ transactionName: 'GET /api' }) - .timestamp(timestamp) - .duration(1000) - .success() - ), - range - .interval('1s') - .rate(4) - .generator((timestamp) => - javaInstance - .transaction({ transactionName: 'GET /api' }) - .timestamp(timestamp) - .duration(1000) - .success() - ), - ]); - }); - - after(() => { - return synthtrace.clean(); - }); - - afterEach(async () => { - await deleteAllServiceDashboard(apmApiClient, 'synth-go', start, end); - }); - - describe('and when data is not loaded', () => { - it('creates a new service dashboard', async () => { - const serviceDashboard = { - dashboardSavedObjectId: 'dashboard-saved-object-id', - serviceFiltersEnabled: true, - kuery: 'service.name: synth-go', - }; - const createResponse = await getLinkServiceDashboardApi({ - apmApiClient, - ...serviceDashboard, - }); - expect(createResponse.status).to.be(200); - expect(createResponse.body).to.have.property('id'); - expect(createResponse.body).to.have.property('updatedAt'); - - expect(createResponse.body).to.have.property( - 'dashboardSavedObjectId', - serviceDashboard.dashboardSavedObjectId - ); - expect(createResponse.body).to.have.property('kuery', serviceDashboard.kuery); - expect(createResponse.body).to.have.property( - 'serviceEnvironmentFilterEnabled', - serviceDashboard.serviceFiltersEnabled - ); - expect(createResponse.body).to.have.property( - 'serviceNameFilterEnabled', - serviceDashboard.serviceFiltersEnabled - ); - - const dasboardForGoService = await getServiceDashboardApi( - apmApiClient, - 'synth-go', - start, - end - ); - const dashboardForJavaService = await getServiceDashboardApi( - apmApiClient, - 'synth-java', - start, - end - ); - expect(dashboardForJavaService.body.serviceDashboards.length).to.be(0); - expect(dasboardForGoService.body.serviceDashboards.length).to.be(1); - }); - - it('updates the existing linked service dashboard', async () => { - const serviceDashboard = { - dashboardSavedObjectId: 'dashboard-saved-object-id', - serviceFiltersEnabled: true, - kuery: 'service.name: synth-go or agent.name: java', - }; - - await getLinkServiceDashboardApi({ - apmApiClient, - ...serviceDashboard, - }); - - const dasboardForGoService = await getServiceDashboardApi( - apmApiClient, - 'synth-go', - start, - end - ); - - const updateResponse = await getLinkServiceDashboardApi({ - apmApiClient, - customDashboardId: dasboardForGoService.body.serviceDashboards[0].id, - ...serviceDashboard, - serviceFiltersEnabled: true, - }); - - expect(updateResponse.status).to.be(200); - - const updateddasboardForGoService = await getServiceDashboardApi( - apmApiClient, - 'synth-go', - start, - end - ); - expect(updateddasboardForGoService.body.serviceDashboards.length).to.be(1); - expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property( - 'serviceEnvironmentFilterEnabled', - true - ); - expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property( - 'serviceNameFilterEnabled', - true - ); - expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property( - 'kuery', - 'service.name: synth-go or agent.name: java' - ); - - const dashboardForJavaService = await getServiceDashboardApi( - apmApiClient, - 'synth-java', - start, - end - ); - expect(dashboardForJavaService.body.serviceDashboards.length).to.be(1); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts deleted file mode 100644 index c9cb6aea7130..000000000000 --- a/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts +++ /dev/null @@ -1,304 +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 { sum } from 'lodash'; -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; -import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; -import { generateOperationData, generateOperationDataConfig } from './generate_operation_data'; -import { SupertestReturnType } from '../../common/apm_api_supertest'; - -const { - ES_BULK_DURATION, - ES_BULK_RATE, - ES_SEARCH_DURATION, - ES_SEARCH_FAILURE_RATE, - ES_SEARCH_SUCCESS_RATE, - ES_SEARCH_UNKNOWN_RATE, - REDIS_SET_RATE, -} = generateOperationDataConfig; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - async function callApi({ - dependencyName, - searchServiceDestinationMetrics, - spanName = '', - metric, - kuery = '', - environment = ENVIRONMENT_ALL.value, - }: { - dependencyName: string; - searchServiceDestinationMetrics: boolean; - spanName?: string; - metric: TMetricName; - kuery?: string; - environment?: string; - }): Promise> { - return await apmApiClient.readUser({ - endpoint: `GET /internal/apm/dependencies/charts/${ - metric as 'latency' | 'throughput' | 'error_rate' - }`, - params: { - query: { - dependencyName, - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment, - kuery, - offset: '', - spanName, - searchServiceDestinationMetrics, - }, - }, - }); - } - - function avg(coordinates: Coordinate[]) { - const values = coordinates - .filter((coord): coord is { x: number; y: number } => isFiniteNumber(coord.y)) - .map((coord) => coord.y); - - return roundNumber(sum(values) / values.length); - } - - registry.when( - 'Dependency metrics when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state', async () => { - const { body, status } = await callApi({ - dependencyName: 'elasticsearch', - metric: 'latency', - searchServiceDestinationMetrics: true, - }); - - expect(status).to.be(200); - expect(body.currentTimeseries.filter((val) => isFiniteNumber(val.y))).to.empty(); - expect( - (body.comparisonTimeseries || [])?.filter((val) => isFiniteNumber(val.y)) - ).to.empty(); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177121 - registry.when('Dependency metrics when data is loaded', { config: 'basic', archives: [] }, () => { - before(async () => { - await generateOperationData({ - apmSynthtraceEsClient, - start, - end, - }); - }); - - describe('without spanName', () => { - describe('without a kuery or environment', () => { - it('returns the correct latency', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'latency', - }); - - const searchRate = - ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; - const bulkRate = ES_BULK_RATE; - - expect(avg(response.body.currentTimeseries)).to.eql( - roundNumber( - ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / - (searchRate + bulkRate)) * - 1000 - ) - ); - }); - - it('returns the correct throughput', async () => { - const response = await callApi({ - dependencyName: 'redis', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'throughput', - }); - - expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); - }); - - it('returns the correct failure rate', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'error_rate', - }); - - const expectedErrorRate = - ES_SEARCH_FAILURE_RATE / (ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE); - - expect(avg(response.body.currentTimeseries)).to.eql(expectedErrorRate); - }); - }); - - describe('with a kuery', () => { - it('returns the correct latency', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'latency', - kuery: `event.outcome:unknown`, - }); - - const searchRate = ES_SEARCH_UNKNOWN_RATE; - const bulkRate = ES_BULK_RATE; - - expect(avg(response.body.currentTimeseries)).to.eql( - roundNumber( - ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / - (searchRate + bulkRate)) * - 1000 - ) - ); - }); - - it('returns the correct throughput', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'throughput', - kuery: `event.outcome:unknown`, - }); - - const searchRate = ES_SEARCH_UNKNOWN_RATE; - const bulkRate = ES_BULK_RATE; - - expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); - }); - - it('returns the correct failure rate', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'error_rate', - kuery: 'event.outcome:success', - }); - - expect(avg(response.body.currentTimeseries)).to.eql(0); - }); - }); - - describe('with an environment', () => { - it('returns the correct latency', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'latency', - environment: 'production', - }); - - const searchRate = ES_SEARCH_UNKNOWN_RATE; - const bulkRate = 0; - - expect(avg(response.body.currentTimeseries)).to.eql( - roundNumber( - ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / - (searchRate + bulkRate)) * - 1000 - ) - ); - }); - - it('returns the correct throughput', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'throughput', - environment: 'production', - }); - - const searchRate = - ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; - const bulkRate = 0; - - expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); - }); - - it('returns the correct failure rate', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: true, - spanName: '', - metric: 'error_rate', - environment: 'development', - }); - - expect(avg(response.body.currentTimeseries)).to.eql(null); - }); - }); - }); - - describe('with spanName', () => { - it('returns the correct latency', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: false, - spanName: '/_search', - metric: 'latency', - }); - - const searchRate = ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; - const bulkRate = 0; - - expect(avg(response.body.currentTimeseries)).to.eql( - roundNumber( - ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / - (searchRate + bulkRate)) * - 1000 - ) - ); - }); - - it('returns the correct throughput', async () => { - const response = await callApi({ - dependencyName: 'redis', - searchServiceDestinationMetrics: false, - spanName: 'SET', - metric: 'throughput', - }); - - expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); - }); - - it('returns the correct failure rate', async () => { - const response = await callApi({ - dependencyName: 'elasticsearch', - searchServiceDestinationMetrics: false, - spanName: '/_bulk', - metric: 'error_rate', - }); - - expect(avg(response.body.currentTimeseries)).to.eql(null); - }); - }); - - after(() => apmSynthtraceEsClient.clean()); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts deleted file mode 100644 index 20a38369bcd5..000000000000 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts +++ /dev/null @@ -1,275 +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 { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; -import { ValuesType } from 'utility-types'; -import { DependencyOperation } from '@kbn/apm-plugin/server/routes/dependencies/get_top_dependency_operations'; -import { meanBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; -import { generateOperationData, generateOperationDataConfig } from './generate_operation_data'; - -type TopOperations = APIReturnType<'GET /internal/apm/dependencies/operations'>['operations']; - -const { - ES_BULK_DURATION, - ES_BULK_RATE, - ES_SEARCH_DURATION, - ES_SEARCH_FAILURE_RATE, - ES_SEARCH_SUCCESS_RATE, - ES_SEARCH_UNKNOWN_RATE, - REDIS_SET_DURATION, - REDIS_SET_RATE, -} = generateOperationDataConfig; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - async function callApi({ - dependencyName, - environment = ENVIRONMENT_ALL.value, - kuery = '', - searchServiceDestinationMetrics = false, - }: { - dependencyName: string; - environment?: string; - kuery?: string; - searchServiceDestinationMetrics?: boolean; - }) { - return await apmApiClient - .readUser({ - endpoint: 'GET /internal/apm/dependencies/operations', - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment, - kuery, - dependencyName, - searchServiceDestinationMetrics, - }, - }, - }) - .then(({ body }) => body.operations); - } - - registry.when('Top operations when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const operations = await callApi({ dependencyName: 'elasticsearch' }); - expect(operations).to.empty(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177128 - registry.when('Top operations when data is generated', { config: 'basic', archives: [] }, () => { - before(() => - generateOperationData({ - apmSynthtraceEsClient, - start, - end, - }) - ); - - after(() => apmSynthtraceEsClient.clean()); - - describe('requested for elasticsearch', () => { - let response: TopOperations; - let searchOperation: ValuesType; - let bulkOperation: ValuesType; - - before(async () => { - response = await callApi({ dependencyName: 'elasticsearch' }); - searchOperation = response.find((op) => op.spanName === '/_search')!; - bulkOperation = response.find((op) => op.spanName === '/_bulk')!; - }); - - it('returns the correct operations', () => { - expect(response.length).to.eql(2); - - expect(searchOperation).to.be.ok(); - expect(bulkOperation).to.be.ok(); - }); - - it('returns the correct latency', () => { - expect(searchOperation.latency).to.eql(ES_SEARCH_DURATION * 1000); - expect(bulkOperation.latency).to.eql(ES_BULK_DURATION * 1000); - }); - - it('returns the correct throughput', () => { - const expectedSearchThroughput = roundNumber( - ES_SEARCH_UNKNOWN_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE - ); - const expectedBulkThroughput = ES_BULK_RATE; - - expect(roundNumber(searchOperation.throughput)).to.eql(expectedSearchThroughput); - expect(roundNumber(bulkOperation.throughput)).to.eql(expectedBulkThroughput); - - expect( - searchOperation.timeseries.throughput - .map((bucket) => bucket.y) - .every((val) => val === expectedSearchThroughput) - ); - }); - - it('returns the correct failure rate', () => { - const expectedSearchFailureRate = - ES_SEARCH_FAILURE_RATE / (ES_SEARCH_SUCCESS_RATE + ES_SEARCH_FAILURE_RATE); - const expectedBulkFailureRate = null; - - expect(searchOperation.failureRate).to.be(expectedSearchFailureRate); - - expect(bulkOperation.failureRate).to.be(expectedBulkFailureRate); - - expect( - searchOperation.timeseries.failureRate - .map((bucket) => bucket.y) - .every((val) => val === expectedSearchFailureRate) - ); - - expect( - bulkOperation.timeseries.failureRate - .map((bucket) => bucket.y) - .every((val) => val === expectedBulkFailureRate) - ); - }); - - it('returns the correct impact', () => { - expect(searchOperation.impact).to.eql(0); - expect(bulkOperation.impact).to.eql(100); - }); - }); - - describe('requested for redis', () => { - let response: TopOperations; - let setOperation: ValuesType; - - before(async () => { - response = await callApi({ dependencyName: 'redis' }); - setOperation = response.find((op) => op.spanName === 'SET')!; - }); - - it('returns the correct operations', () => { - expect(response.length).to.eql(1); - - expect(setOperation).to.be.ok(); - }); - - it('returns the correct latency', () => { - expect(setOperation.latency).to.eql(REDIS_SET_DURATION * 1000); - }); - - it('returns the correct throughput', () => { - expect(roundNumber(setOperation.throughput)).to.eql(roundNumber(REDIS_SET_RATE)); - }); - }); - - describe('requested for a specific service', () => { - let response: TopOperations; - let searchOperation: ValuesType; - let bulkOperation: ValuesType | undefined; - - before(async () => { - response = await callApi({ - dependencyName: 'elasticsearch', - kuery: `service.name:"synth-go"`, - }); - searchOperation = response.find((op) => op.spanName === '/_search')!; - bulkOperation = response.find((op) => op.spanName === '/_bulk'); - }); - - it('returns the correct operations', () => { - expect(response.length).to.eql(1); - - expect(searchOperation).to.be.ok(); - expect(bulkOperation).not.to.be.ok(); - }); - }); - - describe('requested for a specific environment', () => { - let response: TopOperations; - let searchOperation: ValuesType | undefined; - let bulkOperation: ValuesType; - - before(async () => { - response = await callApi({ - dependencyName: 'elasticsearch', - environment: 'development', - }); - searchOperation = response.find((op) => op.spanName === '/_search'); - bulkOperation = response.find((op) => op.spanName === '/_bulk')!; - }); - - it('returns the correct operations', () => { - expect(response.length).to.eql(1); - - expect(searchOperation).not.to.be.ok(); - expect(bulkOperation).to.be.ok(); - }); - }); - - describe('Compare span metrics and span events', () => { - let bulkOperationSpanEventsResponse: ValuesType; - let bulkOperationSpanMetricsResponse: ValuesType; - - before(async () => { - const [spanEventsResponse, spanMetricsResponse] = await Promise.all([ - callApi({ dependencyName: 'elasticsearch', searchServiceDestinationMetrics: false }), - callApi({ dependencyName: 'elasticsearch', searchServiceDestinationMetrics: true }), - ]); - function findBulkOperation(op: DependencyOperation) { - return op.spanName === '/_bulk'; - } - bulkOperationSpanEventsResponse = spanEventsResponse.find(findBulkOperation)!; - bulkOperationSpanMetricsResponse = spanMetricsResponse.find(findBulkOperation)!; - }); - - it('returns same latency', () => { - expect(bulkOperationSpanEventsResponse.latency).to.eql( - bulkOperationSpanMetricsResponse.latency - ); - - const meanSpanMetrics = meanBy( - bulkOperationSpanEventsResponse.timeseries.latency.filter(({ y }) => y !== null), - 'y' - ); - const meanSpanEvents = meanBy( - bulkOperationSpanMetricsResponse.timeseries.latency.filter(({ y }) => y !== null), - 'y' - ); - expect(meanSpanMetrics).to.eql(meanSpanEvents); - }); - - it('returns same throughput', () => { - expect(bulkOperationSpanEventsResponse.throughput).to.eql( - bulkOperationSpanMetricsResponse.throughput - ); - - const meanSpanMetrics = meanBy( - bulkOperationSpanEventsResponse.timeseries.throughput.filter(({ y }) => y !== 0), - 'y' - ); - const meanSpanEvents = meanBy( - bulkOperationSpanMetricsResponse.timeseries.throughput.filter(({ y }) => y !== 0), - 'y' - ); - expect(meanSpanMetrics).to.eql(meanSpanEvents); - }); - - it('returns same impact', () => { - expect(bulkOperationSpanEventsResponse.impact).to.eql( - bulkOperationSpanMetricsResponse.impact - ); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/utils.ts b/x-pack/test/apm_api_integration/utils.ts index 8cb818faac52..8f5ff9822e22 100644 --- a/x-pack/test/apm_api_integration/utils.ts +++ b/x-pack/test/apm_api_integration/utils.ts @@ -5,14 +5,9 @@ * 2.0. */ -import { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; import { Maybe } from '@kbn/apm-plugin/typings/common'; export function roundNumber(num: Maybe) { return isFiniteNumber(num) ? Number(num.toPrecision(4)) : null; } - -export function removeEmptyCoordinates(coordinates: Coordinate[]) { - return coordinates.filter(({ y }) => isFiniteNumber(y)); -}