diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/rules/error_count.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/rules/error_count.cy.ts index ec01fc2df2da3..a9ba45b4ad319 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/rules/error_count.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/rules/error_count.cy.ts @@ -5,6 +5,9 @@ * 2.0. */ +import { synthtrace } from '../../../../synthtrace'; +import { generateData } from './generate_data'; + function deleteAllRules() { cy.log('Delete all rules'); cy.request({ @@ -38,6 +41,21 @@ describe('Rules', () => { deleteAllRules(); }); + before(() => { + const start = '2021-10-10T00:00:00.000Z'; + const end = '2021-10-10T00:01:00.000Z'; + synthtrace.index( + generateData({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + }) + ); + }); + + after(() => { + synthtrace.clean(); + }); + describe('Error count', () => { const ruleName = 'Error count threshold'; const comboBoxInputSelector = diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/rules/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/rules/generate_data.ts new file mode 100644 index 0000000000000..880c9d6b59442 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/rules/generate_data.ts @@ -0,0 +1,50 @@ +/* + * 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'; + +export function generateData({ from, to }: { from: number; to: number }) { + const range = timerange(from, to); + + const opbeansJava = apm + .service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }) + .instance('opbeans-java-prod-1') + .podId('opbeans-java-prod-1-pod'); + + const opbeansNode = apm + .service({ + name: 'opbeans-node', + environment: 'production', + agentName: 'nodejs', + }) + .instance('opbeans-node-prod-1'); + + return range + .interval('2m') + .rate(1) + .generator((timestamp, index) => [ + opbeansJava + .transaction({ transactionName: 'GET /apple 🍎 ' }) + .timestamp(timestamp) + .duration(1000) + .success() + .errors( + opbeansJava + .error({ message: `Error ${index}`, type: `exception ${index}` }) + .timestamp(timestamp) + ), + opbeansNode + .transaction({ transactionName: 'GET /banana 🍌' }) + .timestamp(timestamp) + .duration(500) + .success(), + ]); +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts b/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts index 9736a695e81c7..8af466b872629 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts @@ -73,7 +73,7 @@ export async function cypressTestRunner({ getService }: FtrProviderContext) { return res; } -function getCypressCliArgs() { +function getCypressCliArgs(): Record { if (!process.env.CYPRESS_CLI_ARGS) { return {}; } @@ -82,5 +82,11 @@ function getCypressCliArgs() { process.env.CYPRESS_CLI_ARGS ) as Record; - return cypressCliArgs; + const spec = + typeof cypressCliArgs.spec === 'string' && + !cypressCliArgs.spec.includes('**') + ? `**/${cypressCliArgs.spec}*` + : cypressCliArgs.spec; + + return { ...cypressCliArgs, spec }; } diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx index 2cd79535afac5..9d547f2288304 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx @@ -40,7 +40,7 @@ const stories: Meta<{}> = { }, notifications: { toasts: { add: () => {}, addWarning: () => {} } }, uiSettings: { get: () => [] }, - dataViews: { get: async () => {} }, + dataViews: { create: async () => {} }, } as unknown as CoreStart; const KibanaReactContext = createKibanaReactContext(coreMock); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index ff8442016adc5..4eb7d8140fdf3 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -48,7 +48,7 @@ export function ApmMainTemplate({ const location = useLocation(); const { services } = useKibana(); - const { http, docLinks, observability } = services; + const { http, docLinks, observability, application } = services; const basePath = http?.basePath.get(); const ObservabilityPageTemplate = observability.navigation.PageTemplate; @@ -57,6 +57,19 @@ export function ApmMainTemplate({ return callApmApi('GET /internal/apm/has_data'); }, []); + // create static data view on inital load + useFetcher( + (callApmApi) => { + const canCreateDataView = + application?.capabilities.savedObjectsManagement.edit; + + if (canCreateDataView) { + return callApmApi('POST /internal/apm/data_view/static'); + } + }, + [application?.capabilities.savedObjectsManagement.edit] + ); + const shouldBypassNoDataScreen = bypassNoDataScreenPaths.some((path) => location.pathname.includes(path) ); diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.test.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.test.tsx index 33a3646bf7529..1fb1e15a26ee6 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.test.tsx @@ -15,6 +15,7 @@ import { ApmServiceContextProvider } from '../../context/apm_service/apm_service import { UrlParamsProvider } from '../../context/url_params_context/url_params_context'; import type { ApmUrlParams } from '../../context/url_params_context/types'; import * as useFetcherHook from '../../hooks/use_fetcher'; +import * as useApmDataViewHook from '../../hooks/use_apm_data_view'; import * as useServiceTransactionTypesHook from '../../context/apm_service/use_service_transaction_types_fetcher'; import { renderWithTheme } from '../../utils/test_helpers'; import { fromQuery } from './links/url_helpers'; @@ -45,6 +46,11 @@ function setup({ .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypesFetcher') .mockReturnValue(serviceTransactionTypes); + // mock transaction types + jest + .spyOn(useApmDataViewHook, 'useApmDataView') + .mockReturnValue({ dataView: undefined }); + jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any); return renderWithTheme( diff --git a/x-pack/plugins/apm/public/hooks/use_apm_data_view.ts b/x-pack/plugins/apm/public/hooks/use_apm_data_view.ts index 9d4ef214ff5ef..63d7232128d86 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_data_view.ts +++ b/x-pack/plugins/apm/public/hooks/use_apm_data_view.ts @@ -7,20 +7,10 @@ import { DataView } from '@kbn/data-views-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; import { useEffect, useState } from 'react'; -import { APM_STATIC_DATA_VIEW_ID } from '../../common/data_view_constants'; -import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; import { ApmPluginStartDeps } from '../plugin'; import { callApmApi } from '../services/rest/create_call_apm_api'; -async function createStaticApmDataView() { - const res = await callApmApi('POST /internal/apm/data_view/static', { - signal: null, - }); - return res.dataView; -} - async function getApmDataViewTitle() { const res = await callApmApi('GET /internal/apm/data_view/title', { signal: null, @@ -30,37 +20,16 @@ async function getApmDataViewTitle() { export function useApmDataView() { const { services } = useKibana(); - const { core } = useApmPluginContext(); const [dataView, setDataView] = useState(); - const canCreateDataView = - core.application.capabilities.savedObjectsManagement.edit; - useEffect(() => { async function fetchDataView() { - try { - // load static data view - return await services.dataViews.get(APM_STATIC_DATA_VIEW_ID); - } catch (e) { - // re-throw if an unhandled error occurred - const notFound = e instanceof SavedObjectNotFound; - if (!notFound) { - throw e; - } - - // create static data view if user has permissions - if (canCreateDataView) { - return createStaticApmDataView(); - } else { - // or create dynamic data view if user does not have permissions to create a static - const title = await getApmDataViewTitle(); - return services.dataViews.create({ title }); - } - } + const title = await getApmDataViewTitle(); + return services.dataViews.create({ title }); } - fetchDataView().then((dv) => setDataView(dv)); - }, [canCreateDataView, services.dataViews]); + fetchDataView().then(setDataView); + }, [services.dataViews]); return { dataView }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 9d6a716c8b3ed..dc6e379367408 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -39,7 +39,7 @@ export async function setupRequest({ plugins, request, config, -}: APMRouteHandlerResources) { +}: APMRouteHandlerResources): Promise { return withApmSpan('setup_request', async () => { const { query } = params; const coreContext = await context.core; diff --git a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts index c7e22eba054b9..65ecb93bcb76e 100644 --- a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts +++ b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts @@ -8,8 +8,9 @@ import { createStaticDataView } from './create_static_data_view'; import { Setup } from '../../lib/helpers/setup_request'; import * as HistoricalAgentData from '../historical_data/has_historical_agent_data'; -import { APMConfig } from '../..'; import { DataViewsService } from '@kbn/data-views-plugin/common'; +import { APMRouteHandlerResources, APMCore } from '../typings'; +import { APMConfig } from '../..'; function getMockedDataViewService(existingDataViewTitle: string) { return { @@ -20,7 +21,7 @@ function getMockedDataViewService(existingDataViewTitle: string) { } as unknown as DataViewsService; } -const setup = { +const setupMock = { indices: { transaction: 'apm-*-transaction-*', span: 'apm-*-span-*', @@ -29,12 +30,28 @@ const setup = { } as APMConfig['indices'], } as unknown as Setup; +const coreMock = { + start: () => { + return { + savedObjects: { + getScopedClient: () => { + return { + updateObjectsSpaces: () => {}, + }; + }, + }, + }; + }, +} as unknown as APMCore; + describe('createStaticDataView', () => { it(`should not create data view if 'xpack.apm.autocreateApmIndexPattern=false'`, async () => { const dataViewService = getMockedDataViewService('apm-*'); await createStaticDataView({ - setup, - config: { autoCreateApmDataView: false } as APMConfig, + setup: setupMock, + resources: { + config: { autoCreateApmDataView: false }, + } as APMRouteHandlerResources, dataViewService, }); expect(dataViewService.createAndSave).not.toHaveBeenCalled(); @@ -49,8 +66,10 @@ describe('createStaticDataView', () => { const dataViewService = getMockedDataViewService('apm-*'); await createStaticDataView({ - setup, - config: { autoCreateApmDataView: true } as APMConfig, + setup: setupMock, + resources: { + config: { autoCreateApmDataView: false }, + } as APMRouteHandlerResources, dataViewService, }); expect(dataViewService.createAndSave).not.toHaveBeenCalled(); @@ -65,8 +84,11 @@ describe('createStaticDataView', () => { const dataViewService = getMockedDataViewService('apm-*'); await createStaticDataView({ - setup, - config: { autoCreateApmDataView: true } as APMConfig, + setup: setupMock, + resources: { + core: coreMock, + config: { autoCreateApmDataView: true }, + } as APMRouteHandlerResources, dataViewService, }); @@ -84,8 +106,11 @@ describe('createStaticDataView', () => { 'apm-*-transaction-*,apm-*-span-*,apm-*-error-*,apm-*-metrics-*'; await createStaticDataView({ - setup, - config: { autoCreateApmDataView: true } as APMConfig, + setup: setupMock, + resources: { + core: coreMock, + config: { autoCreateApmDataView: true }, + } as APMRouteHandlerResources, dataViewService, }); @@ -110,8 +135,11 @@ describe('createStaticDataView', () => { ); await createStaticDataView({ - setup, - config: { autoCreateApmDataView: true } as APMConfig, + setup: setupMock, + resources: { + core: coreMock, + config: { autoCreateApmDataView: true }, + } as APMRouteHandlerResources, dataViewService, }); diff --git a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts index 6142791b45cc9..c2310acadcff0 100644 --- a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts +++ b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts @@ -7,6 +7,7 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { DataView, DataViewsService } from '@kbn/data-views-plugin/common'; +import { i18n } from '@kbn/i18n'; import { TRACE_ID, TRANSACTION_ID, @@ -15,29 +16,49 @@ import { APM_STATIC_DATA_VIEW_ID } from '../../../common/data_view_constants'; import { hasHistoricalAgentData } from '../historical_data/has_historical_agent_data'; import { withApmSpan } from '../../utils/with_apm_span'; import { getApmDataViewTitle } from './get_apm_data_view_title'; + +import { APMRouteHandlerResources } from '../typings'; import { Setup } from '../../lib/helpers/setup_request'; -import { APMConfig } from '../..'; + +export type CreateDataViewResponse = Promise< + | { created: boolean; dataView: DataView } + | { created: boolean; reason?: string } +>; export async function createStaticDataView({ dataViewService, - config, + resources, setup, }: { dataViewService: DataViewsService; - config: APMConfig; + resources: APMRouteHandlerResources; setup: Setup; -}): Promise { +}): CreateDataViewResponse { + const { config } = resources; + return withApmSpan('create_static_data_view', async () => { // don't auto-create APM data view if it's been disabled via the config if (!config.autoCreateApmDataView) { - return; + return { + created: false, + reason: i18n.translate('xpack.apm.dataView.autoCreateDisabled', { + defaultMessage: + 'Auto-creation of data views has been disabled via "autoCreateApmDataView" config option', + }), + }; } // Discover and other apps will throw errors if an data view exists without having matching indices. // The following ensures the data view is only created if APM data is found const hasData = await hasHistoricalAgentData(setup); + if (!hasData) { - return; + return { + created: false, + reason: i18n.translate('xpack.apm.dataView.noApmData', { + defaultMessage: 'No APM data', + }), + }; } const apmDataViewTitle = getApmDataViewTitle(setup.indices); @@ -47,50 +68,42 @@ export async function createStaticDataView({ }); if (!shouldCreateOrUpdate) { - return; + return { + created: false, + reason: i18n.translate( + 'xpack.apm.dataView.alreadyExistsInActiveSpace', + { defaultMessage: 'Dataview already exists in the active space' } + ), + }; } - try { - return await withApmSpan('create_data_view', async () => { - const dataView = await dataViewService.createAndSave( - { - allowNoIndex: true, - id: APM_STATIC_DATA_VIEW_ID, - name: 'APM', - title: apmDataViewTitle, - timeFieldName: '@timestamp', - - // link to APM from Discover - fieldFormats: { - [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}}', - }, - }, - }, - }, - true - ); - - return dataView; - }); - } catch (e) { - // if the data view (saved object) already exists a conflict error (code: 409) will be thrown - // that error should be silenced - if (SavedObjectsErrorHelpers.isConflictError(e)) { - return; + return await withApmSpan('create_data_view', async () => { + try { + const dataView = await createAndSaveStaticDataView({ + dataViewService, + apmDataViewTitle, + }); + + await addDataViewToAllSpaces(resources); + + return { created: true, dataView }; + } catch (e) { + // if the data view (saved object) already exists a conflict error (code: 409) will be thrown + if (SavedObjectsErrorHelpers.isConflictError(e)) { + return { + created: false, + reason: i18n.translate( + 'xpack.apm.dataView.alreadyExistsInAnotherSpace', + { + defaultMessage: + 'Dataview already exists in another space but is not made available in this space', + } + ), + }; + } + throw e; } - throw e; - } + }); }); } @@ -114,3 +127,53 @@ async function getShouldCreateOrUpdate({ throw e; } } + +async function addDataViewToAllSpaces(resources: APMRouteHandlerResources) { + const { request, core } = resources; + const startServices = await core.start(); + const scopedClient = startServices.savedObjects.getScopedClient(request); + + // make data view available across all spaces + return scopedClient.updateObjectsSpaces( + [{ id: APM_STATIC_DATA_VIEW_ID, type: 'index-pattern' }], + ['*'], + [] + ); +} + +function createAndSaveStaticDataView({ + dataViewService, + apmDataViewTitle, +}: { + dataViewService: DataViewsService; + apmDataViewTitle: string; +}) { + return dataViewService.createAndSave( + { + allowNoIndex: true, + id: APM_STATIC_DATA_VIEW_ID, + name: 'APM', + title: apmDataViewTitle, + timeFieldName: '@timestamp', + + // link to APM from Discover + fieldFormats: { + [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}}', + }, + }, + }, + }, + true + ); +} diff --git a/x-pack/plugins/apm/server/routes/data_view/route.ts b/x-pack/plugins/apm/server/routes/data_view/route.ts index a8c660c293d4c..6f55f67fc9b4f 100644 --- a/x-pack/plugins/apm/server/routes/data_view/route.ts +++ b/x-pack/plugins/apm/server/routes/data_view/route.ts @@ -5,21 +5,23 @@ * 2.0. */ -import { DataView } from '@kbn/data-views-plugin/common'; -import { createStaticDataView } from './create_static_data_view'; -import { setupRequest } from '../../lib/helpers/setup_request'; +import { + CreateDataViewResponse, + createStaticDataView, +} from './create_static_data_view'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { getApmDataViewTitle } from './get_apm_data_view_title'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; +import { setupRequest } from '../../lib/helpers/setup_request'; const staticDataViewRoute = createApmServerRoute({ endpoint: 'POST /internal/apm/data_view/static', options: { tags: ['access:apm'] }, - handler: async (resources): Promise<{ dataView: DataView | undefined }> => { + handler: async (resources): CreateDataViewResponse => { + const { context, plugins, request } = resources; const setup = await setupRequest(resources); - const { context, plugins, request, config } = resources; - const coreContext = await context.core; + const dataViewStart = await plugins.dataViews.start(); const dataViewService = await dataViewStart.dataViewsServiceFactory( coreContext.savedObjects.client, @@ -28,13 +30,13 @@ const staticDataViewRoute = createApmServerRoute({ true ); - const dataView = await createStaticDataView({ + const res = await createStaticDataView({ dataViewService, - config, + resources, setup, }); - return { dataView }; + return res; }, }); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 2e246f753d75b..ff75af8f83342 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -47,6 +47,11 @@ export type TelemetryUsageCounter = ReturnType< UsageCollectionSetup['createUsageCounter'] >; +export interface APMCore { + setup: CoreSetup; + start: () => Promise; +} + export interface APMRouteHandlerResources { request: KibanaRequest; context: ApmPluginRequestHandlerContext; @@ -59,10 +64,7 @@ export interface APMRouteHandlerResources { }; config: APMConfig; logger: Logger; - core: { - setup: CoreSetup; - start: () => Promise; - }; + core: APMCore; plugins: { [key in keyof APMPluginSetupDependencies]: { setup: Required[key]; diff --git a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts index 86beb73d51ea3..c492f7af04a6d 100644 --- a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts +++ b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts @@ -41,10 +41,19 @@ export function createApmApiClient(st: supertest.SuperTest) { }; } +type ApiErrorResponse = Omit & { + body: { + statusCode: number; + error: string; + message: string; + attributes: object; + }; +}; + export type ApmApiSupertest = ReturnType; export class ApmApiError extends Error { - res: request.Response; + res: ApiErrorResponse; constructor(res: request.Response, endpoint: string) { super( 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 index 11bb01d3ca7bb..0ed65724bfd4c 100644 --- 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 @@ -8,30 +8,37 @@ import { apm, ApmSynthtraceEsClient, timerange } from '@kbn/apm-synthtrace'; import expect from '@kbn/expect'; import { APM_STATIC_DATA_VIEW_ID } from '@kbn/apm-plugin/common/data_view_constants'; +import { DataView } from '@kbn/data-views-plugin/common'; +import request from 'superagent'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { SupertestReturnType } from '../../common/apm_api_supertest'; +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('synthtraceEsClient'); - const dataViewPattern = 'traces-apm*,apm-*,logs-apm*,apm-*,metrics-apm*,apm-*'; - function createDataViewViaApmApi() { + function createDataViewWithWriteUser() { return apmApiClient.writeUser({ endpoint: 'POST /internal/apm/data_view/static' }); } + function createDataViewWithReadUser() { + return apmApiClient.readUser({ endpoint: 'POST /internal/apm/data_view/static' }); + } + function deleteDataView() { return supertest - .delete(`/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}`) - .set('kbn-xsrf', 'foo') - .expect(200); + .delete(`/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}?force=true`) + .set('kbn-xsrf', 'foo'); } - function getDataView() { - return supertest.get(`/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}`); + function getDataView({ space }: { space: string }) { + const spacePrefix = space !== 'default' ? `/s/${space}` : ''; + return supertest.get( + `${spacePrefix}/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}` + ); } function getDataViewSuggestions(field: string) { @@ -43,90 +50,150 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('no mappings exist', { config: 'basic', archives: [] }, () => { let response: SupertestReturnType<'POST /internal/apm/data_view/static'>; - describe('when no data is generated', () => { - before(async () => { - response = await createDataViewViaApmApi(); - }); + before(async () => { + response = await createDataViewWithWriteUser(); + }); - it('does not create data view', async () => { - expect(response.status).to.be(200); - expect(response.body.dataView).to.be(undefined); + 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 () => { - await getDataView().expect(404); - }); + it('cannot fetch data view', async () => { + const res = await getDataView({ space: 'default' }); + expect(res.status).to.be(404); + expect(res.body.message).to.eql( + 'Saved object [index-pattern/apm_static_index_pattern_id] not found' + ); }); }); - registry.when('mappings exists', { config: 'basic', archives: [] }, () => { - describe('when data is generated', () => { + registry.when('mappings and APM data exists', { config: 'basic', archives: [] }, () => { + before(async () => { + await generateApmData(synthtrace); + }); + + after(async () => { + await synthtrace.clean(); + }); + + afterEach(async () => { + await deleteDataView(); + }); + + describe('when creating data view with write user', () => { let response: SupertestReturnType<'POST /internal/apm/data_view/static'>; before(async () => { - await generateApmData(synthtrace); - response = await createDataViewViaApmApi(); - }); - - after(async () => { - await deleteDataView(); - await synthtrace.clean(); + response = await createDataViewWithWriteUser(); }); it('successfully creates the apm data view', async () => { expect(response.status).to.be(200); - expect(response.body.dataView!.id).to.be('apm_static_index_pattern_id'); - expect(response.body.dataView!.name).to.be('APM'); - expect(response.body.dataView!.title).to.be( - 'traces-apm*,apm-*,logs-apm*,apm-*,metrics-apm*,apm-*' - ); + // @ts-expect-error + const dataView = response.body.dataView as DataView; + + expect(dataView.id).to.be('apm_static_index_pattern_id'); + expect(dataView.name).to.be('APM'); + expect(dataView.title).to.be('traces-apm*,apm-*,logs-apm*,apm-*,metrics-apm*,apm-*'); }); + }); - describe('when fetching the data view', async () => { - let resBody: any; + describe('when fetching the data view', async () => { + let dataViewResponse: request.Response; - before(async () => { - const res = await getDataView().expect(200); - resBody = res.body; - }); + before(async () => { + await createDataViewWithWriteUser(); + dataViewResponse = await getDataView({ space: 'default' }); + }); - it('has correct id', () => { - expect(resBody.id).to.be('apm_static_index_pattern_id'); - }); + it('return 200', () => { + expect(dataViewResponse.status).to.be(200); + }); - it('has correct title', () => { - expect(resBody.attributes.title).to.be(dataViewPattern); - }); + it('has correct id', () => { + expect(dataViewResponse.body.id).to.be('apm_static_index_pattern_id'); + }); - it('has correct attributes', () => { - expect(resBody.attributes.fieldFormatMap).to.be( - JSON.stringify({ - 'trace.id': { - id: 'url', - params: { - urlTemplate: 'apm/link-to/trace/{{value}}', - labelTemplate: '{{value}}', - }, + 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.id': { + id: 'url', + params: { + urlTemplate: 'apm/link-to/transaction/{{value}}', + labelTemplate: '{{value}}', }, - }) - ); - }); + }, + }) + ); + }); + + // 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(); + } 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(); + const res = await createDataViewWithWriteUser(); - // 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); + expect(res.status).to.be(200); + expect(res.body).to.eql({ + created: false, + reason: 'Dataview already exists in the active space', }); }); }); + + describe('when creating data view in "default" space', async () => { + it('can be retrieved from the "default space"', async () => { + await createDataViewWithWriteUser(); + const res = await getDataView({ space: 'default' }); + expect(res.body.id).to.eql('apm_static_index_pattern_id'); + expect(res.body.namespaces).to.eql(['*', 'default']); + }); + + it('can be retrieved from the "foo" space', async () => { + await createDataViewWithWriteUser(); + const res = await getDataView({ space: 'foo' }); + expect(res.body.id).to.eql('apm_static_index_pattern_id'); + expect(res.body.namespaces).to.eql(['*', 'default']); + }); + }); }); } @@ -137,7 +204,7 @@ function generateApmData(synthtrace: ApmSynthtraceEsClient) { ); const instance = apm - .service({ name: 'multiple-env-service', environment: 'production', agentName: 'go' }) + .service({ name: 'my-service', environment: 'production', agentName: 'go' }) .instance('my-instance'); return synthtrace.index([ diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 9f882797b0a15..53c685a07e8b3 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -16,7 +16,7 @@ function getGlobPattern() { return '**/*.spec.ts'; } - return envGrepFiles.includes('.spec.ts') ? envGrepFiles : `**/*${envGrepFiles}*.spec.ts`; + return envGrepFiles.includes('**') ? envGrepFiles : `**/*${envGrepFiles}*`; } export default function apmApiIntegrationTests({ getService, loadTestFile }: FtrProviderContext) {