Prefer DataView client over SavedObjects client when possible (#136694)

* Prefer DataView client over SavedObjects client when possible

* Remove unused mock

* Fix unit test

* Fall back to dynamic data view

* Post-rebase fix
This commit is contained in:
Søren Louv-Jansen 2022-07-22 19:46:05 +02:00 committed by GitHub
parent b12d58cd55
commit c3649b822c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 231 additions and 347 deletions

View file

@ -18,10 +18,6 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
jest.mock('../services/rest/data_view', () => ({
createStaticDataView: () => Promise.resolve(undefined),
}));
describe('renderApp (APM)', () => {
let mockConsole: jest.SpyInstance;
beforeAll(() => {

View file

@ -18,7 +18,6 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { ConfigSchema } from '..';
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
import { createCallApmApi } from '../services/rest/create_call_apm_api';
import { createStaticDataView } from '../services/rest/data_view';
import { setHelpExtension } from '../set_help_extension';
import { setReadonlyBadge } from '../update_badge';
import { ApmAppRoot } from '../components/routing/app_root';
@ -61,12 +60,6 @@ export const renderApp = ({
setReadonlyBadge(coreStart);
createCallApmApi(coreStart);
// Automatically creates static data view and stores as saved object
createStaticDataView().catch((e) => {
// eslint-disable-next-line no-console
console.log('Error creating static data view', e);
});
// add .kbnAppWrappers class to root element
element.classList.add(APP_WRAPPER_CLASS);

View file

@ -36,11 +36,11 @@ const stories: Meta<{}> = {
default:
return {};
}
return {};
},
},
notifications: { toasts: { add: () => {}, addWarning: () => {} } },
uiSettings: { get: () => [] },
dataViews: { get: async () => {} },
} as unknown as CoreStart;
const KibanaReactContext = createKibanaReactContext(coreMock);

View file

@ -19,7 +19,7 @@ import {
TraceSearchQuery,
TraceSearchType,
} from '../../../../../common/trace_explorer';
import { useStaticDataView } from '../../../../hooks/use_static_data_view';
import { useApmDataView } from '../../../../hooks/use_apm_data_view';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { EQLCodeEditorSuggestionType } from '../../../shared/eql_code_editor/constants';
import { LazilyLoadedEQLCodeEditor } from '../../../shared/eql_code_editor/lazily_loaded_code_editor';
@ -57,7 +57,7 @@ export function TraceSearchBox({
loading,
}: Props) {
const { unifiedSearch } = useApmPluginContext();
const { value: dataView } = useStaticDataView();
const { dataView } = useApmDataView();
return (
<EuiFlexGroup direction="column">

View file

@ -16,7 +16,7 @@ import type { DataView } from '@kbn/data-views-plugin/public';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useDynamicDataViewFetcher } from '../../../hooks/use_dynamic_data_view';
import { useApmDataView } from '../../../hooks/use_apm_data_view';
import { fromQuery, toQuery } from '../links/url_helpers';
import { getBoolFilter } from './get_bool_filter';
// @ts-expect-error
@ -71,8 +71,7 @@ export function KueryBar(props: {
};
const example = examples[processorEvent || 'defaults'];
const { dataView } = useDynamicDataViewFetcher();
const { dataView } = useApmDataView();
const placeholder =
props.placeholder ??
@ -106,7 +105,7 @@ export function KueryBar(props: {
const suggestions = (
(await unifiedSearch.autocomplete.getQuerySuggestions({
language: 'kuery',
indexPatterns: [dataView as DataView],
indexPatterns: [dataView],
boolFilter:
props.boolFilter ??
getBoolFilter({

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { getByTestId, fireEvent, getByText } from '@testing-library/react';
import { getByTestId, fireEvent, getByText, act } from '@testing-library/react';
import { createMemoryHistory, MemoryHistory } from 'history';
import React from 'react';
import { Router } from 'react-router-dom';
@ -37,6 +37,7 @@ function setup({
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiCounter: () => {} },
dataViews: { get: async () => {} },
} as Partial<CoreStart>);
// mock transaction types
@ -91,7 +92,7 @@ describe('when transactionType is selected and multiple transaction types are gi
expect(dropdown).toHaveValue('secondType');
});
it('should update the URL when a transaction type is selected', () => {
it('should update the URL when a transaction type is selected', async () => {
const { container } = setup({
history,
serviceTransactionTypes: ['firstType', 'secondType'],
@ -112,7 +113,9 @@ describe('when transactionType is selected and multiple transaction types are gi
expect(getByText(dropdown, 'secondType')).toBeInTheDocument();
// change dropdown value
fireEvent.change(dropdown, { target: { value: 'firstType' } });
await act(async () => {
fireEvent.change(dropdown, { target: { value: 'firstType' } });
});
// assert that value was changed
expect(dropdown).toHaveValue('firstType');

View file

@ -28,6 +28,7 @@ const mockCore = merge({}, coreStart, {
capabilities: {
apm: {},
ml: {},
savedObjectsManagement: { edit: true },
},
},
uiSettings: {

View file

@ -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 { 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,
});
return res.apmDataViewTitle;
}
export function useApmDataView() {
const { services } = useKibana<ApmPluginStartDeps>();
const { core } = useApmPluginContext();
const [dataView, setDataView] = useState<DataView | undefined>();
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 });
}
}
}
fetchDataView().then((dv) => setDataView(dv));
}, [canCreateDataView, services.dataViews]);
return { dataView };
}

View file

@ -1,21 +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 { useFetcher } from './use_fetcher';
export function useDynamicDataViewFetcher() {
const { data, status } = useFetcher((callApmApi) => {
return callApmApi('GET /internal/apm/data_view/dynamic', {
isCachable: true,
});
}, []);
return {
dataView: data?.dynamicDataView,
status,
};
}

View file

@ -1,16 +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 useAsync from 'react-use/lib/useAsync';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
import { APM_STATIC_DATA_VIEW_ID } from '../../common/data_view_constants';
export function useStaticDataView() {
const { dataViews } = useApmPluginContext();
return useAsync(() => dataViews.get(APM_STATIC_DATA_VIEW_ID));
}

View file

@ -1,14 +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 { callApmApi } from './create_call_apm_api';
export const createStaticDataView = async () => {
return await callApmApi('POST /internal/apm/data_view/static', {
signal: null,
});
};

View file

@ -25,12 +25,6 @@ jest.mock('../../routes/settings/apm_indices/get_apm_indices', () => ({
} as Awaited<ReturnType<typeof getApmIndices>>),
}));
jest.mock('../../routes/data_view/get_dynamic_data_view', () => ({
getDynamicDataView: async () => {
return;
},
}));
function getMockResources() {
const esClientMock = elasticsearchServiceMock.createScopedClusterClient();
// @ts-expect-error incomplete definition

View file

@ -51,7 +51,9 @@ export async function setupRequest({
config,
}),
withApmSpan('get_ui_settings', () =>
coreContext.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN)
coreContext.uiSettings.client.get<boolean>(
UI_SETTINGS.SEARCH_INCLUDE_FROZEN
)
),
]);

View file

@ -8,18 +8,16 @@
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 { InternalSavedObjectsClient } from '../../lib/helpers/get_internal_saved_objects_client';
import { APMConfig } from '../..';
import { DataViewsService } from '@kbn/data-views-plugin/common';
function getMockSavedObjectsClient(existingDataViewTitle: string) {
function getMockedDataViewService(existingDataViewTitle: string) {
return {
get: jest.fn(() => ({
attributes: {
title: existingDataViewTitle,
},
title: existingDataViewTitle,
})),
create: jest.fn(),
} as unknown as InternalSavedObjectsClient;
createAndSave: jest.fn(),
} as unknown as DataViewsService;
}
const setup = {
@ -33,14 +31,13 @@ const setup = {
describe('createStaticDataView', () => {
it(`should not create data view if 'xpack.apm.autocreateApmIndexPattern=false'`, async () => {
const savedObjectsClient = getMockSavedObjectsClient('apm-*');
const dataViewService = getMockedDataViewService('apm-*');
await createStaticDataView({
setup,
config: { autoCreateApmDataView: false } as APMConfig,
savedObjectsClient,
spaceId: 'default',
dataViewService,
});
expect(savedObjectsClient.create).not.toHaveBeenCalled();
expect(dataViewService.createAndSave).not.toHaveBeenCalled();
});
it(`should not create data view if no APM data is found`, async () => {
@ -49,15 +46,14 @@ describe('createStaticDataView', () => {
.spyOn(HistoricalAgentData, 'hasHistoricalAgentData')
.mockResolvedValue(false);
const savedObjectsClient = getMockSavedObjectsClient('apm-*');
const dataViewService = getMockedDataViewService('apm-*');
await createStaticDataView({
setup,
config: { autoCreateApmDataView: true } as APMConfig,
savedObjectsClient,
spaceId: 'default',
dataViewService,
});
expect(savedObjectsClient.create).not.toHaveBeenCalled();
expect(dataViewService.createAndSave).not.toHaveBeenCalled();
});
it(`should create data view`, async () => {
@ -66,16 +62,15 @@ describe('createStaticDataView', () => {
.spyOn(HistoricalAgentData, 'hasHistoricalAgentData')
.mockResolvedValue(true);
const savedObjectsClient = getMockSavedObjectsClient('apm-*');
const dataViewService = getMockedDataViewService('apm-*');
await createStaticDataView({
setup,
config: { autoCreateApmDataView: true } as APMConfig,
savedObjectsClient,
spaceId: 'default',
dataViewService,
});
expect(savedObjectsClient.create).toHaveBeenCalled();
expect(dataViewService.createAndSave).toHaveBeenCalled();
});
it(`should overwrite the data view if the new data view title does not match the old data view title`, async () => {
@ -84,25 +79,24 @@ describe('createStaticDataView', () => {
.spyOn(HistoricalAgentData, 'hasHistoricalAgentData')
.mockResolvedValue(true);
const savedObjectsClient = getMockSavedObjectsClient('apm-*');
const dataViewService = getMockedDataViewService('apm-*');
const expectedDataViewTitle =
'apm-*-transaction-*,apm-*-span-*,apm-*-error-*,apm-*-metrics-*';
await createStaticDataView({
setup,
config: { autoCreateApmDataView: true } as APMConfig,
savedObjectsClient,
spaceId: 'default',
dataViewService,
});
expect(savedObjectsClient.get).toHaveBeenCalled();
expect(savedObjectsClient.create).toHaveBeenCalled();
expect(dataViewService.get).toHaveBeenCalled();
expect(dataViewService.createAndSave).toHaveBeenCalled();
// @ts-ignore
expect(savedObjectsClient.create.mock.calls[0][1].title).toBe(
expect(dataViewService.createAndSave.mock.calls[0][0].title).toBe(
expectedDataViewTitle
);
// @ts-ignore
expect(savedObjectsClient.create.mock.calls[0][2].overwrite).toBe(true);
expect(dataViewService.createAndSave.mock.calls[0][1]).toBe(true);
});
it(`should not overwrite an data view if the new data view title matches the old data view title`, async () => {
@ -111,20 +105,17 @@ describe('createStaticDataView', () => {
.spyOn(HistoricalAgentData, 'hasHistoricalAgentData')
.mockResolvedValue(true);
const savedObjectsClient = getMockSavedObjectsClient(
const dataViewService = getMockedDataViewService(
'apm-*-transaction-*,apm-*-span-*,apm-*-error-*,apm-*-metrics-*'
);
await createStaticDataView({
setup,
config: { autoCreateApmDataView: true } as APMConfig,
savedObjectsClient,
spaceId: 'default',
dataViewService,
});
expect(savedObjectsClient.get).toHaveBeenCalled();
expect(savedObjectsClient.create).toHaveBeenCalled();
// @ts-ignore
expect(savedObjectsClient.create.mock.calls[0][2].overwrite).toBe(false);
expect(dataViewService.get).toHaveBeenCalled();
expect(dataViewService.createAndSave).not.toHaveBeenCalled();
});
});

View file

@ -6,95 +6,109 @@
*/
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { DataView, DataViewsService } from '@kbn/data-views-plugin/common';
import {
TRACE_ID,
TRANSACTION_ID,
} from '../../../common/elasticsearch_fieldnames';
import { APM_STATIC_DATA_VIEW_ID } from '../../../common/data_view_constants';
import { hasHistoricalAgentData } from '../historical_data/has_historical_agent_data';
import { Setup } from '../../lib/helpers/setup_request';
import { APMRouteHandlerResources } from '../typings';
import { InternalSavedObjectsClient } from '../../lib/helpers/get_internal_saved_objects_client';
import { withApmSpan } from '../../utils/with_apm_span';
import { getApmDataViewTitle } from './get_apm_data_view_title';
import { getApmDataViewAttributes } from './get_apm_data_view_attributes';
interface ApmDataViewAttributes {
title: string;
}
import { Setup } from '../../lib/helpers/setup_request';
import { APMConfig } from '../..';
export async function createStaticDataView({
setup,
dataViewService,
config,
savedObjectsClient,
spaceId,
setup,
}: {
dataViewService: DataViewsService;
config: APMConfig;
setup: Setup;
config: APMRouteHandlerResources['config'];
savedObjectsClient: InternalSavedObjectsClient;
spaceId?: string;
}): Promise<boolean> {
}): Promise<DataView | undefined> {
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 false;
return;
}
// 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 false;
return;
}
const apmDataViewTitle = getApmDataViewTitle(setup.indices);
const forceOverwrite = await getForceOverwrite({
const shouldCreateOrUpdate = await getShouldCreateOrUpdate({
apmDataViewTitle,
savedObjectsClient,
dataViewService,
});
try {
await withApmSpan('create_index_pattern_saved_object', () =>
savedObjectsClient.create(
'index-pattern',
getApmDataViewAttributes(apmDataViewTitle),
{
id: APM_STATIC_DATA_VIEW_ID,
overwrite: forceOverwrite,
namespace: spaceId,
}
)
);
if (!shouldCreateOrUpdate) {
return;
}
return true;
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 false;
return;
}
throw e;
}
});
}
// force an overwrite of the data view if the data view has been changed
async function getForceOverwrite({
savedObjectsClient,
// only create data view if it doesn't exist or was changed
async function getShouldCreateOrUpdate({
dataViewService,
apmDataViewTitle,
}: {
savedObjectsClient: InternalSavedObjectsClient;
dataViewService: DataViewsService;
apmDataViewTitle: string;
}) {
try {
const existingDataView =
await savedObjectsClient.get<ApmDataViewAttributes>(
'index-pattern',
APM_STATIC_DATA_VIEW_ID
);
// if the existing data view does not matches the new one, force an update
return existingDataView.attributes.title !== apmDataViewTitle;
const existingDataView = await dataViewService.get(APM_STATIC_DATA_VIEW_ID);
return existingDataView.title !== apmDataViewTitle;
} catch (e) {
// ignore exception if the data view (saved object) is not found
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
return false;
return true;
}
throw e;

View file

@ -1,41 +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 {
TRACE_ID,
TRANSACTION_ID,
} from '../../../common/elasticsearch_fieldnames';
export function getApmDataViewAttributes(title: string) {
return {
// required fields (even if empty)
title,
fieldAttrs: '{}',
fields: '[]',
runtimeFieldMap: '{}',
timeFieldName: '@timestamp',
typeMeta: '{}',
// link to APM from Discover
fieldFormatMap: 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}}',
},
},
}),
};
}

View file

@ -1,66 +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 { IndexPatternsFetcher, FieldDescriptor } from '@kbn/data-plugin/server';
import { APMRouteHandlerResources } from '../typings';
import { withApmSpan } from '../../utils/with_apm_span';
import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
import { getApmDataViewTitle } from './get_apm_data_view_title';
export interface DataViewTitleAndFields {
title: string;
timeFieldName: string;
fields: FieldDescriptor[];
}
export const getDynamicDataView = ({
config,
context,
logger,
}: Pick<APMRouteHandlerResources, 'logger' | 'config' | 'context'>) => {
return withApmSpan('get_dynamic_data_view', async () => {
const coreContext = await context.core;
const apmIndicies = await getApmIndices({
savedObjectsClient: coreContext.savedObjects.client,
config,
});
const dataViewTitle = getApmDataViewTitle(apmIndicies);
const DataViewsFetcher = new IndexPatternsFetcher(
coreContext.elasticsearch.client.asCurrentUser
);
// Since `getDynamicDataView` is called in setup_request (and thus by every endpoint)
// and since `getFieldsForWildcard` will throw if the specified indices don't exist,
// we have to catch errors here to avoid all endpoints returning 500 for users without APM data
// (would be a bad first time experience)
try {
const fields = await DataViewsFetcher.getFieldsForWildcard({
pattern: dataViewTitle,
});
const dataView: DataViewTitleAndFields = {
fields,
timeFieldName: '@timestamp',
title: dataViewTitle,
};
return dataView;
} catch (e) {
const notExists = e.output?.statusCode === 404;
if (notExists) {
logger.error(
`Could not get dynamic data view because indices "${dataViewTitle}" don't exist`
);
return;
}
// re-throw
throw e;
}
});
};

View file

@ -5,69 +5,59 @@
* 2.0.
*/
import { ISavedObjectsRepository } from '@kbn/core/server';
import { DataView } from '@kbn/data-views-plugin/common';
import { createStaticDataView } from './create_static_data_view';
import { setupRequest } from '../../lib/helpers/setup_request';
import { getDynamicDataView } from './get_dynamic_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';
const staticDataViewRoute = createApmServerRoute({
endpoint: 'POST /internal/apm/data_view/static',
options: { tags: ['access:apm'] },
handler: async (resources): Promise<{ created: boolean }> => {
const {
handler: async (resources): Promise<{ dataView: DataView | undefined }> => {
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,
coreContext.elasticsearch.client.asCurrentUser,
request,
core,
plugins: { spaces },
true
);
const dataView = await createStaticDataView({
dataViewService,
config,
} = resources;
const setupPromise = setupRequest(resources);
const clientPromise = core
.start()
.then(
(coreStart): ISavedObjectsRepository =>
coreStart.savedObjects.createInternalRepository()
);
const setup = await setupPromise;
const savedObjectsClient = await clientPromise;
const spaceId = spaces?.setup.spacesService.getSpaceId(request);
const didCreateDataView = await createStaticDataView({
setup,
config,
savedObjectsClient,
spaceId,
});
return { created: didCreateDataView };
return { dataView };
},
});
const dynamicDataViewRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/data_view/dynamic',
const dataViewTitleRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/data_view/title',
options: { tags: ['access:apm'] },
handler: async ({
context,
config,
logger,
}): Promise<{
dynamicDataView:
| import('./get_dynamic_data_view').DataViewTitleAndFields
| undefined;
}> => {
const dynamicDataView = await getDynamicDataView({
context,
}): Promise<{ apmDataViewTitle: string }> => {
const coreContext = await context.core;
const apmIndicies = await getApmIndices({
savedObjectsClient: coreContext.savedObjects.client,
config,
logger,
});
return { dynamicDataView };
const apmDataViewTitle = getApmDataViewTitle(apmIndicies);
return { apmDataViewTitle };
},
});
export const dataViewRouteRepository = {
...staticDataViewRoute,
...dynamicDataViewRoute,
...dataViewTitleRoute,
};

View file

@ -13,9 +13,6 @@ import {
} from '@kbn/home-plugin/server';
import { CloudSetup } from '@kbn/cloud-plugin/server';
import { APMConfig } from '..';
import { APM_STATIC_DATA_VIEW_ID } from '../../common/data_view_constants';
import { getApmDataViewAttributes } from '../routes/data_view/get_apm_data_view_attributes';
import { getApmDataViewTitle } from '../routes/data_view/get_apm_data_view_title';
import { ApmIndicesConfig } from '../routes/settings/apm_indices/get_apm_indices';
import { createElasticCloudInstructions } from './envs/elastic_cloud';
import { onPremInstructions } from './envs/on_prem';
@ -39,15 +36,6 @@ export const tutorialProvider =
isFleetPluginEnabled: boolean;
}) =>
() => {
const dataViewTitle = getApmDataViewTitle(apmIndices);
const savedObjects = [
{
id: APM_STATIC_DATA_VIEW_ID,
attributes: getApmDataViewAttributes(dataViewTitle),
type: 'index-pattern',
},
];
const artifacts: ArtifactsSchema = {
dashboards: [
{
@ -109,13 +97,5 @@ It allows you to monitor the performance of thousands of applications in real ti
cloudSetup: cloud,
}),
previewImagePath: '/plugins/apm/assets/apm.png',
savedObjects,
savedObjectsInstallMsg: i18n.translate(
'xpack.apm.tutorial.specProvider.savedObjectsInstallMsg',
{
defaultMessage:
'An APM data view is required for some features in the APM UI.',
}
),
} as TutorialSchema;
};

View file

@ -50,6 +50,8 @@ import {
FleetStartContract as FleetPluginStart,
} from '@kbn/fleet-plugin/server';
import { InfraPluginStart, InfraPluginSetup } from '@kbn/infra-plugin/server';
import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import { APMConfig } from '.';
import { ApmIndicesConfig } from './routes/settings/apm_indices/get_apm_indices';
import { APMEventClient } from './lib/helpers/create_es_client/create_apm_event_client';
@ -73,6 +75,7 @@ export interface APMPluginSetupDependencies {
observability: ObservabilityPluginSetup;
ruleRegistry: RuleRegistryPluginSetupContract;
infra: InfraPluginSetup;
dataViews: {};
// optional dependencies
actions?: ActionsPlugin['setup'];
@ -95,6 +98,7 @@ export interface APMPluginStartDependencies {
observability: undefined;
ruleRegistry: RuleRegistryPluginStartContract;
infra: InfraPluginStart;
dataViews: DataViewsServerPluginStart;
// optional dependencies
actions?: ActionsPlugin['start'];

View file

@ -8565,7 +8565,6 @@
"xpack.apm.tutorial.specProvider.artifacts.dashboards.linkLabel": "Tableau de bord APM",
"xpack.apm.tutorial.specProvider.longDescription": "Le monitoring des performances applicatives (APM) collecte les indicateurs et les erreurs de performance approfondies depuis votre application. Cela vous permet de monitorer les performances de milliers d'applications en temps réel. [Learn more]({learnMoreLink}).",
"xpack.apm.tutorial.specProvider.name": "APM",
"xpack.apm.tutorial.specProvider.savedObjectsInstallMsg": "Une vue de données APM est requise pour certaines fonctionnalités de l'interface utilisateur APM.",
"xpack.apm.tutorial.startServer.textPre": "Le serveur traite et conserve les indicateurs de performances de l'application dans Elasticsearch.",
"xpack.apm.tutorial.startServer.title": "Lancer le serveur APM",
"xpack.apm.tutorial.windowsServerInstructions.textPost": "Remarque : si l'exécution du script est désactivée dans votre système, vous devez définir la politique d'exécution de la session en cours de sorte que l'exécution du script soit autorisée. Par exemple : {command}.",

View file

@ -8557,7 +8557,6 @@
"xpack.apm.tutorial.specProvider.artifacts.dashboards.linkLabel": "APM ダッシュボード",
"xpack.apm.tutorial.specProvider.longDescription": "アプリケーションパフォーマンスモニタリングAPMは、アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。何千ものアプリケーションのパフォーマンスをリアルタイムで監視できます。[詳細]{learnMoreLink})。",
"xpack.apm.tutorial.specProvider.name": "APM",
"xpack.apm.tutorial.specProvider.savedObjectsInstallMsg": "APM UIの機能にはAPMデータビューが必要なものがあります。",
"xpack.apm.tutorial.startServer.textPre": "サーバーは、Elasticsearch アプリケーションのパフォーマンスメトリックを処理し保存します。",
"xpack.apm.tutorial.startServer.title": "APM Server の起動",
"xpack.apm.tutorial.windowsServerInstructions.textPost": "注:システムでスクリプトの実行が無効な場合、スクリプトを実行するために現在のセッションの実行ポリシーの設定が必要となります。例:{command}。",

View file

@ -8571,7 +8571,6 @@
"xpack.apm.tutorial.specProvider.artifacts.dashboards.linkLabel": "APM 仪表板",
"xpack.apm.tutorial.specProvider.longDescription": "应用程序性能监测 (APM) 从您的应用程序内收集深入全面的性能指标和错误。其允许您实时监测数以千计的应用程序的性能。[了解详情]({learnMoreLink})。",
"xpack.apm.tutorial.specProvider.name": "APM",
"xpack.apm.tutorial.specProvider.savedObjectsInstallMsg": "APM UI 中的某些功能需要 APM 数据视图。",
"xpack.apm.tutorial.startServer.textPre": "服务器在 Elasticsearch 中处理并存储应用程序性能指标。",
"xpack.apm.tutorial.startServer.title": "启动 APM Server",
"xpack.apm.tutorial.windowsServerInstructions.textPost": "注意:如果您的系统禁用了脚本执行,则需要为当前会话设置执行策略,以允许脚本运行。示例:{command}。",

View file

@ -11,7 +11,7 @@ import {
DataView,
DataViewsPublicPluginStart,
} from '@kbn/data-views-plugin/public';
import { useDynamicDataViewFetcher } from '../../../../hooks/use_dynamic_data_view';
import { useDynamicDataViewTitle } from '../../../../hooks/use_dynamic_data_view';
import { useFetcher } from '../../../../hooks/use_fetcher';
interface SharedData {
@ -51,15 +51,15 @@ export function CsmSharedContextProvider({
services: { dataViews },
} = useKibana<{ dataViews: DataViewsPublicPluginStart }>();
const { dataView: uxDataView } = useDynamicDataViewFetcher();
const { dataViewTitle } = useDynamicDataViewTitle();
const { data } = useFetcher<Promise<DataView | undefined>>(async () => {
if (uxDataView?.title) {
if (dataViewTitle) {
return dataViews.create({
title: uxDataView?.title,
title: dataViewTitle,
});
}
}, [uxDataView?.title, dataViews]);
}, [dataViewTitle, dataViews]);
useEffect(() => {
setDataView(data);

View file

@ -38,12 +38,16 @@ async function getCoreWebVitalsResponse({
serviceName,
dataStartPlugin,
}: WithDataPlugin<FetchDataParams>) {
const dataView = await callApmApi('GET /internal/apm/data_view/dynamic', {
signal: null,
});
const dataViewResponse = await callApmApi(
'GET /internal/apm/data_view/title',
{
signal: null,
}
);
return await esQuery<ReturnType<typeof coreWebVitalsQuery>>(dataStartPlugin, {
params: {
index: dataView.dynamicDataView?.title,
index: dataViewResponse.apmDataViewTitle,
...coreWebVitalsQuery(absoluteTime.start, absoluteTime.end, undefined, {
serviceName: serviceName ? [serviceName] : undefined,
}),
@ -78,14 +82,18 @@ export const fetchUxOverviewDate = async (
export async function hasRumData(
params: WithDataPlugin<HasDataParams>
): Promise<UXHasDataResponse> {
const dataView = await callApmApi('GET /internal/apm/data_view/dynamic', {
signal: null,
});
const dataViewResponse = await callApmApi(
'GET /internal/apm/data_view/title',
{
signal: null,
}
);
const esQueryResponse = await esQuery<ReturnType<typeof hasRumDataQuery>>(
params.dataStartPlugin,
{
params: {
index: dataView.dynamicDataView?.title,
index: dataViewResponse.apmDataViewTitle,
...hasRumDataQuery({
start: params?.absoluteTime?.start,
end: params?.absoluteTime?.end,
@ -94,7 +102,7 @@ export async function hasRumData(
}
);
return formatHasRumResult(esQueryResponse, dataView.dynamicDataView?.title);
return formatHasRumResult(esQueryResponse, dataViewResponse.apmDataViewTitle);
}
async function esQuery<T>(

View file

@ -7,15 +7,15 @@
import { useFetcher } from './use_fetcher';
export function useDynamicDataViewFetcher() {
export function useDynamicDataViewTitle() {
const { data, status } = useFetcher((callApmApi) => {
return callApmApi('GET /internal/apm/data_view/dynamic', {
return callApmApi('GET /internal/apm/data_view/title', {
isCachable: true,
});
}, []);
return {
dataView: data?.dynamicDataView,
dataViewTitle: data?.apmDataViewTitle,
status,
};
}

View file

@ -20,11 +20,10 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const dataViewPattern = 'traces-apm*,apm-*,logs-apm*,apm-*,metrics-apm*,apm-*';
function createDataViewViaApmApi() {
return apmApiClient.readUser({ endpoint: 'POST /internal/apm/data_view/static' });
return apmApiClient.writeUser({ endpoint: 'POST /internal/apm/data_view/static' });
}
function deleteDataView() {
// return supertest.delete('/api/saved_objects/<type>/<id>').set('kbn-xsrf', 'foo').expect(200)
return supertest
.delete(`/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}`)
.set('kbn-xsrf', 'foo')
@ -51,7 +50,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('does not create data view', async () => {
expect(response.status).to.be(200);
expect(response.body.created).to.be(false);
expect(response.body.dataView).to.be(undefined);
});
it('cannot fetch data view', async () => {
@ -79,7 +78,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('successfully creates the apm data view', async () => {
expect(response.status).to.be(200);
expect(response.body.created).to.be(true);
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-*'
);
});
describe('when fetching the data view', async () => {