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 aaf095cfb942..ff5a4618e33c 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 @@ -26,5 +26,6 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./observability_overview')); loadTestFile(require.resolve('./latency')); loadTestFile(require.resolve('./infrastructure')); + loadTestFile(require.resolve('./service_groups')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts new file mode 100644 index 000000000000..e88208d48a9b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts @@ -0,0 +1,18 @@ +/* + * 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('service_groups', () => { + loadTestFile(require.resolve('./save_service_group.spec.ts')); + loadTestFile( + require.resolve('./service_group_with_overflow/service_group_with_overflow.spec.ts') + ); + loadTestFile(require.resolve('./service_group_count/service_group_count.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/service_groups/save_service_group.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts similarity index 78% rename from x-pack/test/apm_api_integration/tests/service_groups/save_service_group.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts index b3dfdcdeb515..a0ed02739cf9 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/save_service_group.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts @@ -5,20 +5,19 @@ * 2.0. */ import expect from '@kbn/expect'; -import { ApmApiError } from '../../common/apm_api_supertest'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { expectToReject } from '../../common/utils/expect_to_reject'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ApmApiError } from '../../../../../../apm_api_integration/common/apm_api_supertest'; +import { expectToReject } from '../../../../../../apm_api_integration/common/utils/expect_to_reject'; import { createServiceGroupApi, deleteAllServiceGroups, getServiceGroupsApi, } from './service_groups_api_methods'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); - registry.when('Service group create', { config: 'basic', archives: [] }, () => { + describe('Service group create', () => { afterEach(async () => { await deleteAllServiceGroups(apmApiClient); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/generate_data.ts new file mode 100644 index 000000000000..7f9b1487bb8e --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/generate_data.ts @@ -0,0 +1,74 @@ +/* + * 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 type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; + +export async function generateData({ + apmSynthtraceEsClient, + start, + end, +}: { + apmSynthtraceEsClient: ApmSynthtraceEsClient; + start: number; + end: number; +}) { + const synthServices = [ + apm + .service({ name: 'synth-go', environment: 'testing', agentName: 'go' }) + .instance('instance-1'), + apm + .service({ name: 'synth-java', environment: 'testing', agentName: 'java' }) + .instance('instance-2'), + apm + .service({ name: 'opbeans-node', environment: 'testing', agentName: 'nodejs' }) + .instance('instance-3'), + ]; + + await apmSynthtraceEsClient.index( + synthServices.map((service) => + timerange(start, end) + .interval('5m') + .rate(1) + .generator((timestamp) => + service + .transaction({ + transactionName: 'GET /api/product/list', + transactionType: 'request', + }) + .duration(2000) + .timestamp(timestamp) + .children( + service + .span({ + spanName: '/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) + .destination('elasticsearch') + .duration(100) + .success() + .timestamp(timestamp), + service + .span({ + spanName: '/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) + .destination('elasticsearch') + .duration(300) + .success() + .timestamp(timestamp) + ) + .errors( + service.error({ message: 'error 1', type: 'foo' }).timestamp(timestamp), + service.error({ message: 'error 2', type: 'foo' }).timestamp(timestamp), + service.error({ message: 'error 3', type: 'bar' }).timestamp(timestamp) + ) + ) + ) + ); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts new file mode 100644 index 000000000000..21ac03197a42 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts @@ -0,0 +1,63 @@ +/* + * 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 { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { + createServiceGroupApi, + deleteAllServiceGroups, + getServiceGroupCounts, +} from '../service_groups_api_methods'; +import { generateData } from './generate_data'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const synthtrace = getService('synthtrace'); + const apmApiClient = getService('apmApi'); + const start = Date.now() - 24 * 60 * 60 * 1000; + const end = Date.now(); + + describe('Service group counts', () => { + let synthbeansServiceGroupId: string; + let opbeansServiceGroupId: string; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + const [, { body: synthbeansServiceGroup }, { body: opbeansServiceGroup }] = await Promise.all( + [ + generateData({ start, end, apmSynthtraceEsClient }), + createServiceGroupApi({ + apmApiClient, + groupName: 'synthbeans', + kuery: 'service.name: synth*', + }), + createServiceGroupApi({ + apmApiClient, + groupName: 'opbeans', + kuery: 'service.name: opbeans*', + }), + ] + ); + synthbeansServiceGroupId = synthbeansServiceGroup.id; + opbeansServiceGroupId = opbeansServiceGroup.id; + }); + + after(async () => { + await deleteAllServiceGroups(apmApiClient); + await apmSynthtraceEsClient.clean(); + }); + + it('returns the correct number of services', async () => { + const response = await getServiceGroupCounts(apmApiClient); + expect(response.status).to.be(200); + expect(Object.keys(response.body).length).to.be(2); + expect(response.body[synthbeansServiceGroupId]).to.have.property('services', 2); + expect(response.body[opbeansServiceGroupId]).to.have.property('services', 1); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/es_utils.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/es_utils.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/es_utils.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/es_utils.ts diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts new file mode 100644 index 000000000000..9324dee60d4e --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts @@ -0,0 +1,104 @@ +/* + * 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 { ValuesType } from 'utility-types'; +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 { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { createServiceGroupApi, deleteAllServiceGroups } from '../service_groups_api_methods'; +import { createServiceTransactionMetricsDocs } from './es_utils'; +import { generateData } from './generate_data'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const es = getService('es'); + const synthtrace = getService('synthtrace'); + + describe('Display overflow bucket in Service Groups', () => { + const indexName = 'metrics-apm.service_transaction.1m-default'; + const start = '2023-06-21T06:50:15.910Z'; + const end = '2023-06-21T06:59:15.910Z'; + const startTime = new Date(start).getTime() + 1000; + const OVERFLOW_SERVICE_NAME = '_other'; + let serviceGroupId: string; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + after(async () => { + await deleteAllServiceGroups(apmApiClient); + await apmSynthtraceEsClient.clean(); + }); + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + await generateData({ start, end, apmSynthtraceEsClient }); + + const docs = [ + createServiceTransactionMetricsDocs({ + time: startTime, + service: { + name: OVERFLOW_SERVICE_NAME, + }, + overflowCount: 13, + }), + ]; + + const bulkActions = docs.reduce( + (prev, doc) => { + return [...prev, { create: { _index: indexName } }, doc]; + }, + [] as Array< + | { + create: { + _index: string; + }; + } + | ValuesType + > + ); + + await es.bulk({ + body: bulkActions, + refresh: 'wait_for', + }); + + const serviceGroup = { + groupName: 'overflowGroup', + kuery: 'service.name: synth-go or service.name: synth-java', + }; + const createResponse = await createServiceGroupApi({ apmApiClient, ...serviceGroup }); + expect(createResponse.status).to.be(200); + serviceGroupId = createResponse.body.id; + }); + + it('get the overflow bucket even though its not added explicitly in the Service Group', async () => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services`, + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: '', + serviceGroup: serviceGroupId, + probability: 1, + documentType: ApmDocumentType.ServiceTransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + + const overflowBucket = response.body.items.find( + (service) => service.serviceName === OVERFLOW_SERVICE_NAME + ); + expect(overflowBucket?.serviceName).to.equal(OVERFLOW_SERVICE_NAME); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_groups_api_methods.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_groups_api_methods.ts new file mode 100644 index 000000000000..bd47b80e0bef --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_groups_api_methods.ts @@ -0,0 +1,66 @@ +/* + * 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 { ApmApiClient } from '../../../../services/apm_api'; + +export async function getServiceGroupsApi(apmApiClient: ApmApiClient) { + return apmApiClient.writeUser({ + endpoint: 'GET /internal/apm/service-groups', + }); +} + +export async function createServiceGroupApi({ + apmApiClient, + serviceGroupId, + groupName, + kuery, + description, + color, +}: { + apmApiClient: ApmApiClient; + serviceGroupId?: string; + groupName: string; + kuery: string; + description?: string; + color?: string; +}) { + const response = await apmApiClient.writeUser({ + endpoint: 'POST /internal/apm/service-group', + params: { + query: { + serviceGroupId, + }, + body: { + groupName, + kuery, + description, + color, + }, + }, + }); + return response; +} + +export async function getServiceGroupCounts(apmApiClient: ApmApiClient) { + return apmApiClient.readUser({ + endpoint: 'GET /internal/apm/service-group/counts', + }); +} + +export async function deleteAllServiceGroups(apmApiClient: ApmApiClient) { + return await getServiceGroupsApi(apmApiClient).then((response) => { + const promises = response.body.serviceGroups.map((item) => { + if (item.id) { + return apmApiClient.writeUser({ + endpoint: 'DELETE /internal/apm/service-group', + params: { query: { serviceGroupId: item.id } }, + }); + } + }); + return Promise.all(promises); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts index d39724b0570b..8b43114ba0ed 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts @@ -74,14 +74,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { await apmSynthtraceEsClient.clean(); }); - it('returns the correct number of services', async () => { - const response = await getServiceGroupCounts(apmApiClient); - expect(response.status).to.be(200); - expect(Object.keys(response.body).length).to.be(2); - expect(response.body[synthbeansServiceGroupId]).to.have.property('services', 2); - expect(response.body[opbeansServiceGroupId]).to.have.property('services', 1); - }); - describe('with alerts', () => { let ruleId: string; before(async () => { diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts deleted file mode 100644 index 205b43b57bdc..000000000000 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts +++ /dev/null @@ -1,105 +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 { ValuesType } from 'utility-types'; -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 { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { createServiceGroupApi, deleteAllServiceGroups } from '../service_groups_api_methods'; -import { createServiceTransactionMetricsDocs } from './es_utils'; -import { generateData } from './generate_data'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const es = getService('es'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - registry.when( - 'Display overflow bucket in Service Groups', - { config: 'basic', archives: [] }, - () => { - const indexName = 'metrics-apm.service_transaction.1m-default'; - const start = '2023-06-21T06:50:15.910Z'; - const end = '2023-06-21T06:59:15.910Z'; - const startTime = new Date(start).getTime() + 1000; - const OVERFLOW_SERVICE_NAME = '_other'; - let serviceGroupId: string; - - after(async () => { - await deleteAllServiceGroups(apmApiClient); - await apmSynthtraceEsClient.clean(); - }); - - before(async () => { - await generateData({ start, end, apmSynthtraceEsClient }); - - const docs = [ - createServiceTransactionMetricsDocs({ - time: startTime, - service: { - name: OVERFLOW_SERVICE_NAME, - }, - overflowCount: 13, - }), - ]; - - const bulkActions = docs.reduce( - (prev, doc) => { - return [...prev, { create: { _index: indexName } }, doc]; - }, - [] as Array< - | { - create: { - _index: string; - }; - } - | ValuesType - > - ); - - await es.bulk({ - body: bulkActions, - refresh: 'wait_for', - }); - - const serviceGroup = { - groupName: 'overflowGroup', - kuery: 'service.name: synth-go or service.name: synth-java', - }; - const createResponse = await createServiceGroupApi({ apmApiClient, ...serviceGroup }); - expect(createResponse.status).to.be(200); - serviceGroupId = createResponse.body.id; - }); - - it('get the overflow bucket even though its not added explicitly in the Service Group', async () => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services`, - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: '', - serviceGroup: serviceGroupId, - probability: 1, - documentType: ApmDocumentType.ServiceTransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - - const overflowBucket = response.body.items.find( - (service) => service.serviceName === OVERFLOW_SERVICE_NAME - ); - expect(overflowBucket?.serviceName).to.equal(OVERFLOW_SERVICE_NAME); - }); - } - ); -}