From 8fe516a5399e17563f01173d98ab8ddd934d0c8c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 12 Nov 2024 23:23:13 +1100 Subject: [PATCH] [8.x] [APM] Migrate `/data_view` to deployment agnostic test (#199296) (#199753) # Backport This will backport the following commits from `main` to `8.x`: - [[APM] Migrate `/data_view` to deployment agnostic test (#199296)](https://github.com/elastic/kibana/pull/199296) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Sergi Romeu --- .../apis/observability/apm/data_view/index.ts | 14 + .../apm/data_view/static.spec.ts | 291 ++++++++++++++++++ .../apis/observability/apm/index.ts | 1 + .../tests/data_view/static.spec.ts | 268 ---------------- 4 files changed, 306 insertions(+), 268 deletions(-) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/static.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/data_view/static.spec.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts new file mode 100644 index 0000000000000..9412ddca7cbcb --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/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('data_view', () => { + loadTestFile(require.resolve('./static.spec.ts')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/static.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/static.spec.ts new file mode 100644 index 0000000000000..2dc71cd599bd1 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/static.spec.ts @@ -0,0 +1,291 @@ +/* + * 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'; +import expect from '@kbn/expect'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import request from 'superagent'; +import { getStaticDataViewId } from '@kbn/apm-data-view'; +import { SupertestWithRoleScope } from '../../../../services/role_scoped_supertest'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { + SupertestReturnType, + ApmApiError, +} from '../../../../../../apm_api_integration/common/apm_api_supertest'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const roleScopedSupertest = getService('roleScopedSupertest'); + const synthtraceService = getService('synthtrace'); + const logger = getService('log'); + const dataViewPattern = + 'traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-*'; + + function createDataViewWithWriteUser({ spaceId }: { spaceId: string }) { + return apmApiClient.writeUser({ + endpoint: 'POST /internal/apm/data_view/static', + spaceId, + }); + } + + function createDataViewWithReadUser({ spaceId }: { spaceId: string }) { + return apmApiClient.readUser({ + endpoint: 'POST /internal/apm/data_view/static', + spaceId, + }); + } + + describe('Data view static', () => { + let supertestEditorWithApiKey: SupertestWithRoleScope; + let supertestEditorWithCookieCredentials: SupertestWithRoleScope; + + before(async () => { + supertestEditorWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('editor', { + withInternalHeaders: true, + }); + + supertestEditorWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'editor', + { + useCookieHeader: true, + withInternalHeaders: true, + } + ); + }); + + after(async () => { + await supertestEditorWithApiKey.destroy(); + }); + + function deleteDataView(spaceId: string) { + return supertestEditorWithApiKey.delete( + `/s/${spaceId}/api/saved_objects/index-pattern/${getStaticDataViewId(spaceId)}?force=true` + ); + } + + function getDataView({ spaceId }: { spaceId: string }) { + const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : ''; + return supertestEditorWithApiKey.get( + `${spacePrefix}/api/saved_objects/index-pattern/${getStaticDataViewId(spaceId)}` + ); + } + + function getDataViewSuggestions(field: string) { + return supertestEditorWithCookieCredentials + .post(`/internal/kibana/suggestions/values/${dataViewPattern}`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .send({ query: '', field, method: 'terms_agg' }); + } + + describe('no mappings exist', () => { + let response: SupertestReturnType<'POST /internal/apm/data_view/static'>; + before(async () => { + response = await createDataViewWithWriteUser({ spaceId: 'default' }); + }); + + it('does not create data view', async () => { + expect(response.status).to.be(200); + expect(response.body).to.eql({ + created: false, + reason: 'No APM data', + }); + }); + + it('cannot fetch data view', async () => { + const res = await getDataView({ spaceId: 'default' }); + expect(res.status).to.be(404); + expect(res.body.message).to.eql( + 'Saved object [index-pattern/apm_static_data_view_id_default] not found' + ); + }); + }); + + describe('when mappings and APM data exists', () => { + let synthtrace: ApmSynthtraceEsClient; + + before(async () => { + synthtrace = await synthtraceService.createApmSynthtraceEsClient(); + await generateApmData(synthtrace); + }); + + after(async () => { + await synthtrace.clean(); + }); + + afterEach(async () => { + try { + await Promise.all([deleteDataView('default'), deleteDataView('foo')]); + } catch (e) { + logger.error(`Could not delete data views ${e.message}`); + } + }); + + describe('when creating data view with write user', () => { + let response: SupertestReturnType<'POST /internal/apm/data_view/static'>; + + before(async () => { + response = await createDataViewWithWriteUser({ spaceId: 'default' }); + }); + + it('successfully creates the apm data view', async () => { + expect(response.status).to.be(200); + + const dataView = (response.body as { dataView: DataView }).dataView; + + expect(dataView.id).to.be('apm_static_data_view_id_default'); + expect(dataView.name).to.be('APM'); + expect(dataView.title).to.be( + 'traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-*' + ); + }); + }); + + describe('when fetching the data view', () => { + let dataViewResponse: request.Response; + + before(async () => { + await createDataViewWithWriteUser({ spaceId: 'default' }); + dataViewResponse = await getDataView({ spaceId: 'default' }); + }); + + it('return 200', () => { + expect(dataViewResponse.status).to.be(200); + }); + + it('has correct id', () => { + expect(dataViewResponse.body.id).to.be('apm_static_data_view_id_default'); + }); + + it('has correct title', () => { + expect(dataViewResponse.body.attributes.title).to.be(dataViewPattern); + }); + + it('has correct attributes', () => { + expect(dataViewResponse.body.attributes.fieldFormatMap).to.be( + JSON.stringify({ + 'trace.id': { + id: 'url', + params: { + urlTemplate: 'apm/link-to/trace/{{value}}', + labelTemplate: '{{value}}', + }, + }, + 'transaction.id': { + id: 'url', + params: { + urlTemplate: 'apm/link-to/transaction/{{value}}', + labelTemplate: '{{value}}', + }, + }, + 'transaction.duration.us': { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'asMilliseconds', + showSuffix: true, + useShortSuffix: true, + outputPrecision: 2, + includeSpaceWithSuffix: true, + }, + }, + }) + ); + }); + + // this test ensures that the default APM Data View doesn't interfere with suggestions returned in the kuery bar (this has been a problem in the past) + it('can get suggestions for `trace.id`', async () => { + const suggestions = await getDataViewSuggestions('trace.id'); + expect(suggestions.body.length).to.be(10); + }); + }); + + describe('when creating data view via read user', () => { + it('throws an error', async () => { + try { + await createDataViewWithReadUser({ spaceId: 'default' }); + } catch (e) { + const err = e as ApmApiError; + const responseBody = err.res.body; + expect(err.res.status).to.eql(403); + expect(responseBody.statusCode).to.eql(403); + expect(responseBody.error).to.eql('Forbidden'); + expect(responseBody.message).to.eql('Unable to create index-pattern'); + } + }); + }); + + describe('when creating data view twice', () => { + it('returns 200 response with reason, if data view already exists', async () => { + await createDataViewWithWriteUser({ spaceId: 'default' }); + const res = await createDataViewWithWriteUser({ spaceId: 'default' }); + + expect(res.status).to.be(200); + expect(res.body).to.eql({ + created: false, + reason: 'Dataview already exists in the active space and does not need to be updated', + }); + }); + }); + + describe('when creating data view in "default" space', () => { + it('can be retrieved from the "default" space', async () => { + await createDataViewWithWriteUser({ spaceId: 'default' }); + const res = await getDataView({ spaceId: 'default' }); + expect(res.body.id).to.eql('apm_static_data_view_id_default'); + expect(res.body.namespaces).to.eql(['default']); + }); + + it('cannot be retrieved from the "foo" space', async () => { + await createDataViewWithWriteUser({ spaceId: 'default' }); + const res = await getDataView({ spaceId: 'foo' }); + expect(res.body.statusCode).to.be(404); + }); + }); + + describe('when creating data view in "foo" space', () => { + it('can be retrieved from the "foo" space', async () => { + await createDataViewWithWriteUser({ spaceId: 'foo' }); + const res = await getDataView({ spaceId: 'foo' }); + expect(res.body.id).to.eql('apm_static_data_view_id_foo'); + expect(res.body.namespaces).to.eql(['foo']); + }); + + it('cannot be retrieved from the "default" space', async () => { + await createDataViewWithWriteUser({ spaceId: 'foo' }); + const res = await getDataView({ spaceId: 'default' }); + expect(res.body.statusCode).to.be(404); + }); + }); + }); + }); +} + +function generateApmData(synthtrace: ApmSynthtraceEsClient) { + const range = timerange( + new Date('2021-10-01T00:00:00.000Z').getTime(), + new Date('2021-10-01T00:01:00.000Z').getTime() + ); + + const instance = apm + .service({ name: 'my-service', environment: 'production', agentName: 'go' }) + .instance('my-instance'); + + return synthtrace.index([ + range + .interval('1s') + .rate(1) + .generator((timestamp) => + instance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(30) + .success() + ), + ]); +} 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 233d7e07be29e..79a3f8cc9816b 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 @@ -14,6 +14,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./agent_explorer')); loadTestFile(require.resolve('./custom_dashboards')); loadTestFile(require.resolve('./dependencies')); + loadTestFile(require.resolve('./data_view')); loadTestFile(require.resolve('./correlations')); loadTestFile(require.resolve('./entities')); loadTestFile(require.resolve('./cold_start')); diff --git a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts b/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts deleted file mode 100644 index 2d6d1b230ef6e..0000000000000 --- a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts +++ /dev/null @@ -1,268 +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 type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import expect from '@kbn/expect'; -import { DataView } from '@kbn/data-views-plugin/common'; -import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; -import request from 'superagent'; -import { getStaticDataViewId } from '@kbn/apm-data-view'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { SupertestReturnType, ApmApiError } from '../../common/apm_api_supertest'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const supertest = getService('supertest'); - const synthtrace = getService('apmSynthtraceEsClient'); - const logger = getService('log'); - const dataViewPattern = - 'traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-*'; - - function createDataViewWithWriteUser({ spaceId }: { spaceId: string }) { - return apmApiClient.writeUser({ - endpoint: 'POST /internal/apm/data_view/static', - spaceId, - }); - } - - function createDataViewWithReadUser({ spaceId }: { spaceId: string }) { - return apmApiClient.readUser({ - endpoint: 'POST /internal/apm/data_view/static', - spaceId, - }); - } - - function deleteDataView(spaceId: string) { - return supertest - .delete( - `/s/${spaceId}/api/saved_objects/index-pattern/${getStaticDataViewId(spaceId)}?force=true` - ) - .set('kbn-xsrf', 'foo'); - } - - function getDataView({ spaceId }: { spaceId: string }) { - const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : ''; - return supertest.get( - `${spacePrefix}/api/saved_objects/index-pattern/${getStaticDataViewId(spaceId)}` - ); - } - - function getDataViewSuggestions(field: string) { - return supertest - .post(`/internal/kibana/suggestions/values/${dataViewPattern}`) - .set('kbn-xsrf', 'foo') - .set(ELASTIC_HTTP_VERSION_HEADER, '1') - .send({ query: '', field, method: 'terms_agg' }); - } - - registry.when('no mappings exist', { config: 'basic', archives: [] }, () => { - let response: SupertestReturnType<'POST /internal/apm/data_view/static'>; - before(async () => { - response = await createDataViewWithWriteUser({ spaceId: 'default' }); - }); - - it('does not create data view', async () => { - expect(response.status).to.be(200); - expect(response.body).to.eql({ - created: false, - reason: 'No APM data', - }); - }); - - it('cannot fetch data view', async () => { - const res = await getDataView({ spaceId: 'default' }); - expect(res.status).to.be(404); - expect(res.body.message).to.eql( - 'Saved object [index-pattern/apm_static_data_view_id_default] not found' - ); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177120 - registry.when('mappings and APM data exists', { config: 'basic', archives: [] }, () => { - // eslint-disable-next-line mocha/no-sibling-hooks - before(async () => { - await generateApmData(synthtrace); - }); - - after(async () => { - await synthtrace.clean(); - }); - - afterEach(async () => { - try { - await Promise.all([deleteDataView('default'), deleteDataView('foo')]); - } catch (e) { - logger.error(`Could not delete data views ${e.message}`); - } - }); - - describe('when creating data view with write user', () => { - let response: SupertestReturnType<'POST /internal/apm/data_view/static'>; - - before(async () => { - response = await createDataViewWithWriteUser({ spaceId: 'default' }); - }); - - it('successfully creates the apm data view', async () => { - expect(response.status).to.be(200); - - // @ts-expect-error - const dataView = response.body.dataView as DataView; - - expect(dataView.id).to.be('apm_static_data_view_id_default'); - expect(dataView.name).to.be('APM'); - expect(dataView.title).to.be( - 'traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-*' - ); - }); - }); - - describe('when fetching the data view', () => { - let dataViewResponse: request.Response; - - before(async () => { - await createDataViewWithWriteUser({ spaceId: 'default' }); - dataViewResponse = await getDataView({ spaceId: 'default' }); - }); - - it('return 200', () => { - expect(dataViewResponse.status).to.be(200); - }); - - it('has correct id', () => { - expect(dataViewResponse.body.id).to.be('apm_static_data_view_id_default'); - }); - - it('has correct title', () => { - expect(dataViewResponse.body.attributes.title).to.be(dataViewPattern); - }); - - it('has correct attributes', () => { - expect(dataViewResponse.body.attributes.fieldFormatMap).to.be( - JSON.stringify({ - 'trace.id': { - id: 'url', - params: { - urlTemplate: 'apm/link-to/trace/{{value}}', - labelTemplate: '{{value}}', - }, - }, - 'transaction.id': { - id: 'url', - params: { - urlTemplate: 'apm/link-to/transaction/{{value}}', - labelTemplate: '{{value}}', - }, - }, - 'transaction.duration.us': { - id: 'duration', - params: { - inputFormat: 'microseconds', - outputFormat: 'asMilliseconds', - showSuffix: true, - useShortSuffix: true, - outputPrecision: 2, - includeSpaceWithSuffix: true, - }, - }, - }) - ); - }); - - // this test ensures that the default APM Data View doesn't interfere with suggestions returned in the kuery bar (this has been a problem in the past) - it('can get suggestions for `trace.id`', async () => { - const suggestions = await getDataViewSuggestions('trace.id'); - expect(suggestions.body.length).to.be(10); - }); - }); - - describe('when creating data view via read user', () => { - it('throws an error', async () => { - try { - await createDataViewWithReadUser({ spaceId: 'default' }); - } catch (e) { - const err = e as ApmApiError; - const responseBody = err.res.body; - expect(err.res.status).to.eql(403); - expect(responseBody.statusCode).to.eql(403); - expect(responseBody.error).to.eql('Forbidden'); - expect(responseBody.message).to.eql('Unable to create index-pattern'); - } - }); - }); - - describe('when creating data view twice', () => { - it('returns 200 response with reason, if data view already exists', async () => { - await createDataViewWithWriteUser({ spaceId: 'default' }); - const res = await createDataViewWithWriteUser({ spaceId: 'default' }); - - expect(res.status).to.be(200); - expect(res.body).to.eql({ - created: false, - reason: 'Dataview already exists in the active space and does not need to be updated', - }); - }); - }); - - describe('when creating data view in "default" space', () => { - it('can be retrieved from the "default" space', async () => { - await createDataViewWithWriteUser({ spaceId: 'default' }); - const res = await getDataView({ spaceId: 'default' }); - expect(res.body.id).to.eql('apm_static_data_view_id_default'); - expect(res.body.namespaces).to.eql(['default']); - }); - - it('cannot be retrieved from the "foo" space', async () => { - await createDataViewWithWriteUser({ spaceId: 'default' }); - const res = await getDataView({ spaceId: 'foo' }); - expect(res.body.statusCode).to.be(404); - }); - }); - - describe('when creating data view in "foo" space', () => { - it('can be retrieved from the "foo" space', async () => { - await createDataViewWithWriteUser({ spaceId: 'foo' }); - const res = await getDataView({ spaceId: 'foo' }); - expect(res.body.id).to.eql('apm_static_data_view_id_foo'); - expect(res.body.namespaces).to.eql(['foo']); - }); - - it('cannot be retrieved from the "default" space', async () => { - await createDataViewWithWriteUser({ spaceId: 'foo' }); - const res = await getDataView({ spaceId: 'default' }); - expect(res.body.statusCode).to.be(404); - }); - }); - }); -} - -function generateApmData(synthtrace: ApmSynthtraceEsClient) { - const range = timerange( - new Date('2021-10-01T00:00:00.000Z').getTime(), - new Date('2021-10-01T00:01:00.000Z').getTime() - ); - - const instance = apm - .service({ name: 'my-service', environment: 'production', agentName: 'go' }) - .instance('my-instance'); - - return synthtrace.index([ - range - .interval('1s') - .rate(1) - .generator((timestamp) => - instance - .transaction({ transactionName: 'GET /api' }) - .timestamp(timestamp) - .duration(30) - .success() - ), - ]); -}