mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
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:
parent
b12d58cd55
commit
c3649b822c
27 changed files with 231 additions and 347 deletions
|
@ -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(() => {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -28,6 +28,7 @@ const mockCore = merge({}, coreStart, {
|
|||
capabilities: {
|
||||
apm: {},
|
||||
ml: {},
|
||||
savedObjectsManagement: { edit: true },
|
||||
},
|
||||
},
|
||||
uiSettings: {
|
||||
|
|
66
x-pack/plugins/apm/public/hooks/use_apm_data_view.ts
Normal file
66
x-pack/plugins/apm/public/hooks/use_apm_data_view.ts
Normal 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 };
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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}.",
|
||||
|
|
|
@ -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}。",
|
||||
|
|
|
@ -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}。",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue