mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Make data view space aware (#170857)
Closes https://github.com/elastic/kibana/issues/169924 Related SDH: https://github.com/elastic/sdh-apm/issues/1080 (_internal_) The APM data view is automatically created when opening the APM UI app via a request to `POST /internal/apm/data_view/static`. This creates a data view which is made available to all spaces. The problem with this approach is that users can change index settings (apm index patterns) per space, which means that there should be a data view per space as well. This PR ensures that a data view is created per space and that any links to the data view uses the spaace specific dataview --------- Co-authored-by: Caue Marcondes <caue.marcondes@elastic.co> Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
This commit is contained in:
parent
2a8a68c1f8
commit
9523a78e7a
28 changed files with 302 additions and 182 deletions
|
@ -5,5 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// value of const needs to be backwards compatible
|
||||
export const APM_STATIC_DATA_VIEW_ID = 'apm_static_index_pattern_id';
|
||||
export const DO_NOT_USE_LEGACY_APM_STATIC_DATA_VIEW_ID =
|
||||
'apm_static_index_pattern_id';
|
||||
|
||||
const APM_STATIC_DATA_VIEW_ID_PREFIX = 'apm_static_data_view_id';
|
||||
|
||||
export function getDataViewId(spaceId: string) {
|
||||
return `${APM_STATIC_DATA_VIEW_ID_PREFIX}_${spaceId}`;
|
||||
}
|
||||
|
|
|
@ -13,20 +13,23 @@ import {
|
|||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { orderBy } from 'lodash';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { asBigNumber, asInteger } from '../../../../common/utils/formatters';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../../../common/data_view_constants';
|
||||
import type { ApmEvent } from '../../../../server/routes/diagnostics/bundle/get_apm_events';
|
||||
import { useDiagnosticsContext } from './context/use_diagnostics';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useDataViewId } from '../../../hooks/use_data_view_id';
|
||||
import { ApmPluginStartDeps } from '../../../plugin';
|
||||
import { SearchBar } from '../../shared/search_bar/search_bar';
|
||||
import { useDiagnosticsContext } from './context/use_diagnostics';
|
||||
|
||||
export function DiagnosticsApmDocuments() {
|
||||
const { diagnosticsBundle, isImported } = useDiagnosticsContext();
|
||||
const { discover } = useKibana<ApmPluginStartDeps>().services;
|
||||
const dataViewId = useDataViewId();
|
||||
|
||||
const [sortField, setSortField] = useState<keyof ApmEvent>('name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const {
|
||||
|
@ -103,7 +106,7 @@ export function DiagnosticsApmDocuments() {
|
|||
language: 'kuery',
|
||||
query: item.kuery,
|
||||
},
|
||||
dataViewId: APM_STATIC_DATA_VIEW_ID,
|
||||
dataViewId,
|
||||
timeRange:
|
||||
rangeTo && rangeFrom
|
||||
? {
|
||||
|
@ -123,13 +126,37 @@ export function DiagnosticsApmDocuments() {
|
|||
{isImported && diagnosticsBundle ? (
|
||||
<>
|
||||
<EuiBadge>
|
||||
From: {new Date(diagnosticsBundle.params.start).toISOString()}
|
||||
{i18n.translate(
|
||||
'xpack.apm.diagnosticsApmDocuments.from:BadgeLabel',
|
||||
{
|
||||
defaultMessage: 'From: {date}',
|
||||
values: {
|
||||
date: new Date(diagnosticsBundle.params.start).toISOString(),
|
||||
},
|
||||
}
|
||||
)}
|
||||
</EuiBadge>
|
||||
<EuiBadge>
|
||||
To: {new Date(diagnosticsBundle.params.end).toISOString()}
|
||||
{i18n.translate('xpack.apm.diagnosticsApmDocuments.to:BadgeLabel', {
|
||||
defaultMessage: 'To: {date}',
|
||||
values: {
|
||||
date: new Date(diagnosticsBundle.params.end).toISOString(),
|
||||
},
|
||||
})}
|
||||
</EuiBadge>
|
||||
<EuiBadge>
|
||||
Filter: {diagnosticsBundle?.params.kuery ?? <em>Empty</em>}
|
||||
{i18n.translate(
|
||||
'xpack.apm.diagnosticsApmDocuments.filter:BadgeLabel',
|
||||
{ defaultMessage: 'Filter:' }
|
||||
)}
|
||||
{diagnosticsBundle?.params.kuery ?? (
|
||||
<em>
|
||||
{i18n.translate(
|
||||
'xpack.apm.diagnosticsApmDocuments.em.emptyLabel',
|
||||
{ defaultMessage: 'Empty' }
|
||||
)}
|
||||
</em>
|
||||
)}
|
||||
</EuiBadge>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
|
@ -183,7 +210,13 @@ function IntervalDocCount({
|
|||
<EuiText
|
||||
css={{ fontStyle: 'italic', fontSize: '80%', display: 'inline' }}
|
||||
>
|
||||
({asBigNumber(interval.eventDocCount)} events)
|
||||
{i18n.translate('xpack.apm.intervalDocCount.TextLabel', {
|
||||
defaultMessage:
|
||||
'({docCount} {docCount, plural, one {event} other {events}})',
|
||||
values: {
|
||||
docCount: asBigNumber(interval.eventDocCount),
|
||||
},
|
||||
})}
|
||||
</EuiText>
|
||||
</div>
|
||||
</EuiToolTip>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import type { DashboardPanelMap } from '@kbn/dashboard-plugin/common';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../../../../common/data_view_constants';
|
||||
import {
|
||||
AGENT_NAME_DASHBOARD_FILE_MAPPING,
|
||||
loadDashboardFile,
|
||||
|
@ -29,7 +28,8 @@ function getDashboardFile({ agentName }: MetricsDashboardProps) {
|
|||
}
|
||||
|
||||
export async function getDashboardPanelMap(
|
||||
props: MetricsDashboardProps
|
||||
props: MetricsDashboardProps,
|
||||
dataViewId: string
|
||||
): Promise<DashboardPanelMap | undefined> {
|
||||
const dashboardFile = getDashboardFile(props);
|
||||
const panelsRawObj = !!dashboardFile
|
||||
|
@ -42,7 +42,7 @@ export async function getDashboardPanelMap(
|
|||
|
||||
const panelsStr: string = (
|
||||
panelsRawObj.attributes.panelsJSON as string
|
||||
).replaceAll('APM_STATIC_DATA_VIEW_ID', APM_STATIC_DATA_VIEW_ID);
|
||||
).replaceAll('APM_STATIC_DATA_VIEW_ID', dataViewId);
|
||||
|
||||
const panelsRawObjects = JSON.parse(panelsStr) as any[];
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
|
||||
import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { NotificationsStart } from '@kbn/core/public';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../../../../common/data_view_constants';
|
||||
import { useDataViewId } from '../../../../hooks/use_data_view_id';
|
||||
import {
|
||||
ENVIRONMENT_ALL,
|
||||
ENVIRONMENT_NOT_DEFINED,
|
||||
|
@ -28,11 +28,11 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug
|
|||
import { useApmDataView } from '../../../../hooks/use_apm_data_view';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
|
||||
import { getDashboardPanelMap, MetricsDashboardProps } from './helper';
|
||||
|
||||
export function JsonMetricsDashboard(dashboardProps: MetricsDashboardProps) {
|
||||
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
||||
const dataViewId = useDataViewId();
|
||||
|
||||
const {
|
||||
query: { environment, kuery, rangeFrom, rangeTo },
|
||||
|
@ -65,7 +65,7 @@ export function JsonMetricsDashboard(dashboardProps: MetricsDashboardProps) {
|
|||
return (
|
||||
<DashboardRenderer
|
||||
getCreationOptions={() =>
|
||||
getCreationOptions(dashboardProps, notifications)
|
||||
getCreationOptions(dashboardProps, notifications, dataViewId)
|
||||
}
|
||||
ref={setDashboard}
|
||||
/>
|
||||
|
@ -74,20 +74,21 @@ export function JsonMetricsDashboard(dashboardProps: MetricsDashboardProps) {
|
|||
|
||||
async function getCreationOptions(
|
||||
dashboardProps: MetricsDashboardProps,
|
||||
notifications: NotificationsStart
|
||||
notifications: NotificationsStart,
|
||||
dataViewId: string
|
||||
): Promise<DashboardCreationOptions> {
|
||||
try {
|
||||
const builder = controlGroupInputBuilder;
|
||||
const controlGroupInput = getDefaultControlGroupInput();
|
||||
|
||||
await builder.addDataControlFromField(controlGroupInput, {
|
||||
dataViewId: APM_STATIC_DATA_VIEW_ID,
|
||||
dataViewId,
|
||||
title: 'Node name',
|
||||
fieldName: 'service.node.name',
|
||||
width: 'medium',
|
||||
grow: true,
|
||||
});
|
||||
const panels = await getDashboardPanelMap(dashboardProps);
|
||||
const panels = await getDashboardPanelMap(dashboardProps, dataViewId);
|
||||
|
||||
if (!panels) {
|
||||
throw new Error('Failed parsing dashboard panels.');
|
||||
|
|
|
@ -23,6 +23,7 @@ import { css } from '@emotion/react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { useDataViewId } from '../../../../../hooks/use_data_view_id';
|
||||
import { ApmPluginStartDeps } from '../../../../../plugin';
|
||||
import { getLayerList } from './map_layers/get_layer_list';
|
||||
import { MapTypes } from '../../../../../../common/mobile/constants';
|
||||
|
@ -40,6 +41,7 @@ function EmbeddedMapComponent({
|
|||
filters: Filter[];
|
||||
}) {
|
||||
const [error, setError] = useState<boolean>();
|
||||
const dataViewId = useDataViewId();
|
||||
|
||||
const [embeddable, setEmbeddable] = useState<
|
||||
MapEmbeddable | ErrorEmbeddable | undefined
|
||||
|
@ -128,7 +130,7 @@ function EmbeddedMapComponent({
|
|||
useEffect(() => {
|
||||
const setLayerList = async () => {
|
||||
if (embeddable && !isErrorEmbeddable(embeddable)) {
|
||||
const layerList = await getLayerList({ selectedMap, maps });
|
||||
const layerList = await getLayerList({ selectedMap, maps, dataViewId });
|
||||
await Promise.all([
|
||||
embeddable.setLayerList(layerList),
|
||||
embeddable.reload(),
|
||||
|
@ -137,7 +139,7 @@ function EmbeddedMapComponent({
|
|||
};
|
||||
|
||||
setLayerList();
|
||||
}, [embeddable, selectedMap, maps]);
|
||||
}, [embeddable, selectedMap, maps, dataViewId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddable) {
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
SPAN_SUBTYPE,
|
||||
SPAN_TYPE,
|
||||
} from '../../../../../../../common/es_fields/apm';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../../../../../../common/data_view_constants';
|
||||
import { getLayerStyle, PalleteColors } from './get_map_layer_style';
|
||||
import {
|
||||
MobileSpanSubtype,
|
||||
|
@ -48,7 +47,10 @@ const label = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export async function getHttpRequestsLayerList(maps?: MapsStartApi) {
|
||||
export async function getHttpRequestsLayerList(
|
||||
maps: MapsStartApi | undefined,
|
||||
dataViewId: string
|
||||
) {
|
||||
const whereQuery = {
|
||||
language: 'kuery',
|
||||
query: `${PROCESSOR_EVENT}:${ProcessorEvent.span} and ${SPAN_SUBTYPE}:${MobileSpanSubtype.Http} and ${SPAN_TYPE}:${MobileSpanType.External}`,
|
||||
|
@ -72,7 +74,7 @@ export async function getHttpRequestsLayerList(maps?: MapsStartApi) {
|
|||
},
|
||||
],
|
||||
whereQuery,
|
||||
indexPatternId: APM_STATIC_DATA_VIEW_ID,
|
||||
indexPatternId: dataViewId,
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
applyForceRefresh: true,
|
||||
|
@ -114,7 +116,7 @@ export async function getHttpRequestsLayerList(maps?: MapsStartApi) {
|
|||
},
|
||||
],
|
||||
whereQuery,
|
||||
indexPatternId: APM_STATIC_DATA_VIEW_ID,
|
||||
indexPatternId: dataViewId,
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
applyForceRefresh: true,
|
||||
|
|
|
@ -10,19 +10,21 @@ import { getHttpRequestsLayerList } from './get_http_requests_map_layer_list';
|
|||
import { getSessionMapLayerList } from './get_session_map_layer_list';
|
||||
import { MapTypes } from '../../../../../../../common/mobile/constants';
|
||||
|
||||
export async function getLayerList({
|
||||
export function getLayerList({
|
||||
selectedMap,
|
||||
maps,
|
||||
dataViewId,
|
||||
}: {
|
||||
selectedMap: MapTypes;
|
||||
maps?: MapsStartApi;
|
||||
maps: MapsStartApi | undefined;
|
||||
dataViewId: string;
|
||||
}): Promise<LayerDescriptor[]> {
|
||||
switch (selectedMap) {
|
||||
case MapTypes.Http:
|
||||
return await getHttpRequestsLayerList(maps);
|
||||
return getHttpRequestsLayerList(maps, dataViewId);
|
||||
case MapTypes.Session:
|
||||
return await getSessionMapLayerList(maps);
|
||||
return getSessionMapLayerList(maps, dataViewId);
|
||||
default:
|
||||
return await getHttpRequestsLayerList(maps);
|
||||
return getHttpRequestsLayerList(maps, dataViewId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
CLIENT_GEO_REGION_ISO_CODE,
|
||||
SESSION_ID,
|
||||
} from '../../../../../../../common/es_fields/apm';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../../../../../../common/data_view_constants';
|
||||
import { getLayerStyle, PalleteColors } from './get_map_layer_style';
|
||||
|
||||
interface VectorLayerDescriptor extends BaseVectorLayerDescriptor {
|
||||
|
@ -40,7 +39,10 @@ const label = i18n.translate(
|
|||
defaultMessage: 'Sessions',
|
||||
}
|
||||
);
|
||||
export async function getSessionMapLayerList(maps?: MapsStartApi) {
|
||||
export async function getSessionMapLayerList(
|
||||
maps: MapsStartApi | undefined,
|
||||
dataViewId: string
|
||||
) {
|
||||
const basemapLayerDescriptor =
|
||||
await maps?.createLayerDescriptors?.createBasemapLayerDescriptor();
|
||||
|
||||
|
@ -59,7 +61,7 @@ export async function getSessionMapLayerList(maps?: MapsStartApi) {
|
|||
label,
|
||||
},
|
||||
],
|
||||
indexPatternId: APM_STATIC_DATA_VIEW_ID,
|
||||
indexPatternId: dataViewId,
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
applyForceRefresh: true,
|
||||
|
@ -101,7 +103,7 @@ export async function getSessionMapLayerList(maps?: MapsStartApi) {
|
|||
label,
|
||||
},
|
||||
],
|
||||
indexPatternId: APM_STATIC_DATA_VIEW_ID,
|
||||
indexPatternId: dataViewId,
|
||||
applyGlobalQuery: true,
|
||||
applyGlobalTime: true,
|
||||
applyForceRefresh: true,
|
||||
|
|
|
@ -12,7 +12,7 @@ import React from 'react';
|
|||
import { useLocation } from 'react-router-dom';
|
||||
import rison from '@kbn/rison';
|
||||
import url from 'url';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../../../../common/data_view_constants';
|
||||
import { useDataViewId } from '../../../../hooks/use_data_view_id';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { getTimepickerRisonData } from '../rison_helpers';
|
||||
|
||||
|
@ -37,16 +37,18 @@ export const getDiscoverHref = ({
|
|||
basePath,
|
||||
location,
|
||||
query,
|
||||
dataViewId,
|
||||
}: {
|
||||
basePath: IBasePath;
|
||||
location: Location;
|
||||
query: Props['query'];
|
||||
dataViewId: string;
|
||||
}) => {
|
||||
const risonQuery = {
|
||||
_g: getTimepickerRisonData(location.search),
|
||||
_a: {
|
||||
...query._a,
|
||||
index: APM_STATIC_DATA_VIEW_ID,
|
||||
index: dataViewId,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -62,11 +64,13 @@ export const getDiscoverHref = ({
|
|||
export function DiscoverLink({ query = {}, ...rest }: Props) {
|
||||
const { core } = useApmPluginContext();
|
||||
const location = useLocation();
|
||||
const dataViewId = useDataViewId();
|
||||
|
||||
const href = getDiscoverHref({
|
||||
basePath: core.http.basePath,
|
||||
query,
|
||||
location,
|
||||
dataViewId,
|
||||
});
|
||||
|
||||
return <EuiLink data-test-subj="apmDiscoverLinkLink" {...rest} href={href} />;
|
||||
|
|
|
@ -35,7 +35,7 @@ describe('DiscoverLinks', () => {
|
|||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"'))"`
|
||||
`"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_data_view_id_default,interval:auto,query:(language:kuery,query:'processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"'))"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -55,7 +55,7 @@ describe('DiscoverLinks', () => {
|
|||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'span.id:\\"test-span-id\\"'))"`
|
||||
`"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_data_view_id_default,interval:auto,query:(language:kuery,query:'span.id:\\"test-span-id\\"'))"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -77,7 +77,7 @@ describe('DiscoverLinks', () => {
|
|||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\"'),sort:('@timestamp':desc))"`
|
||||
`"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_data_view_id_default,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\"'),sort:('@timestamp':desc))"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -100,7 +100,7 @@ describe('DiscoverLinks', () => {
|
|||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\" AND some:kuery-string'),sort:('@timestamp':desc))"`
|
||||
`"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_data_view_id_default,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\" AND some:kuery-string'),sort:('@timestamp':desc))"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -77,6 +77,7 @@ describe('Transaction action menu', () => {
|
|||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
dataViewId: 'apm_static_data_view_id_default',
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
|
@ -113,7 +114,7 @@ describe('Transaction action menu', () => {
|
|||
{
|
||||
key: 'sampleDocument',
|
||||
label: 'View transaction in Discover',
|
||||
href: 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))',
|
||||
href: 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_data_view_id_default,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))',
|
||||
condition: true,
|
||||
},
|
||||
],
|
||||
|
@ -145,6 +146,7 @@ describe('Transaction action menu', () => {
|
|||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
dataViewId: 'apm_static_data_view_id_default',
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
|
@ -200,7 +202,7 @@ describe('Transaction action menu', () => {
|
|||
{
|
||||
key: 'sampleDocument',
|
||||
label: 'View transaction in Discover',
|
||||
href: 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))',
|
||||
href: 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_data_view_id_default,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))',
|
||||
condition: true,
|
||||
},
|
||||
],
|
||||
|
@ -232,6 +234,7 @@ describe('Transaction action menu', () => {
|
|||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
dataViewId: 'apm_static_data_view_id_default',
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
|
@ -286,7 +289,7 @@ describe('Transaction action menu', () => {
|
|||
{
|
||||
key: 'sampleDocument',
|
||||
label: 'View transaction in Discover',
|
||||
href: 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))',
|
||||
href: 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_data_view_id_default,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))',
|
||||
condition: true,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -55,6 +55,7 @@ export const getSections = ({
|
|||
allDatasetsLocator,
|
||||
logsLocator,
|
||||
nodeLogsLocator,
|
||||
dataViewId,
|
||||
}: {
|
||||
transaction?: Transaction;
|
||||
basePath: IBasePath;
|
||||
|
@ -68,6 +69,7 @@ export const getSections = ({
|
|||
allDatasetsLocator: LocatorPublic<AllDatasetsLocatorParams>;
|
||||
logsLocator: LocatorPublic<LogsLocatorParams>;
|
||||
nodeLogsLocator: LocatorPublic<NodeLogsLocatorParams>;
|
||||
dataViewId: string;
|
||||
}) => {
|
||||
if (!transaction) return [];
|
||||
|
||||
|
@ -271,6 +273,7 @@ export const getSections = ({
|
|||
basePath,
|
||||
query: getDiscoverQuery(transaction),
|
||||
location,
|
||||
dataViewId,
|
||||
}),
|
||||
condition: true,
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
NodeLogsLocatorParams,
|
||||
} from '@kbn/logs-shared-plugin/common';
|
||||
import type { ProfilingLocators } from '@kbn/observability-shared-plugin/public';
|
||||
import { useDataViewId } from '../../../hooks/use_data_view_id';
|
||||
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
||||
import { ApmFeatureFlagName } from '../../../../common/apm_feature_flags';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
|
||||
|
@ -138,6 +139,7 @@ function ActionMenuSections({
|
|||
const { core, uiActions, share } = useApmPluginContext();
|
||||
const location = useLocation();
|
||||
const apmRouter = useApmRouter();
|
||||
const dataViewId = useDataViewId();
|
||||
|
||||
const allDatasetsLocator = share.url.locators.get<AllDatasetsLocatorParams>(
|
||||
ALL_DATASETS_LOCATOR_ID
|
||||
|
@ -173,6 +175,7 @@ function ActionMenuSections({
|
|||
allDatasetsLocator,
|
||||
logsLocator,
|
||||
nodeLogsLocator,
|
||||
dataViewId,
|
||||
});
|
||||
|
||||
const externalMenuItems = useAsync(() => {
|
||||
|
|
|
@ -12,11 +12,11 @@ import { useEffect, useState } from 'react';
|
|||
import { ApmPluginStartDeps } from '../plugin';
|
||||
import { callApmApi } from '../services/rest/create_call_apm_api';
|
||||
|
||||
async function getApmDataViewTitle() {
|
||||
const res = await callApmApi('GET /internal/apm/data_view/title', {
|
||||
async function getApmDataViewIndexPattern() {
|
||||
const res = await callApmApi('GET /internal/apm/data_view/index_pattern', {
|
||||
signal: null,
|
||||
});
|
||||
return res.apmDataViewTitle;
|
||||
return res.apmDataViewIndexPattern;
|
||||
}
|
||||
|
||||
export function useApmDataView() {
|
||||
|
@ -25,11 +25,11 @@ export function useApmDataView() {
|
|||
|
||||
useEffect(() => {
|
||||
async function fetchDataView() {
|
||||
const title = await getApmDataViewTitle();
|
||||
const indexPattern = await getApmDataViewIndexPattern();
|
||||
try {
|
||||
const displayError = false;
|
||||
return await services.dataViews.create(
|
||||
{ title },
|
||||
{ title: indexPattern },
|
||||
undefined,
|
||||
displayError
|
||||
);
|
||||
|
|
29
x-pack/plugins/apm/public/hooks/use_data_view_id.tsx
Normal file
29
x-pack/plugins/apm/public/hooks/use_data_view_id.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getDataViewId } from '../../common/data_view_constants';
|
||||
import { ApmPluginStartDeps } from '../plugin';
|
||||
|
||||
export function useDataViewId() {
|
||||
const [dataViewId, setDataViewId] = useState<string>(
|
||||
getDataViewId('default')
|
||||
);
|
||||
const { spaces } = useKibana<ApmPluginStartDeps>().services;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSpaceId = async () => {
|
||||
const space = await spaces?.getActiveSpace();
|
||||
setDataViewId(getDataViewId(space?.id ?? 'default'));
|
||||
};
|
||||
|
||||
fetchSpaceId();
|
||||
}, [spaces]);
|
||||
|
||||
return dataViewId;
|
||||
}
|
|
@ -11,7 +11,6 @@ import {
|
|||
Logger,
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
SavedObjectsClient,
|
||||
} from '@kbn/core/server';
|
||||
import { isEmpty, mapValues } from 'lodash';
|
||||
import { Dataset } from '@kbn/rule-registry-plugin/server';
|
||||
|
@ -50,7 +49,6 @@ import { scheduleSourceMapMigration } from './routes/source_maps/schedule_source
|
|||
import { createApmSourceMapIndexTemplate } from './routes/source_maps/create_apm_source_map_index_template';
|
||||
import { addApiKeysToEveryPackagePolicyIfMissing } from './routes/fleet/api_keys/add_api_keys_to_policies_if_missing';
|
||||
import { apmTutorialCustomIntegration } from '../common/tutorial/tutorials';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../common/data_view_constants';
|
||||
|
||||
export class APMPlugin
|
||||
implements
|
||||
|
@ -123,26 +121,6 @@ export class APMPlugin
|
|||
],
|
||||
});
|
||||
|
||||
// ensure that the APM data view is globally available
|
||||
getCoreStart()
|
||||
.then(async (coreStart) => {
|
||||
const soClient = new SavedObjectsClient(
|
||||
coreStart.savedObjects.createInternalRepository()
|
||||
);
|
||||
|
||||
await soClient.updateObjectsSpaces(
|
||||
[{ id: APM_STATIC_DATA_VIEW_ID, type: 'index-pattern' }],
|
||||
['*'],
|
||||
[]
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger?.error(
|
||||
'Failed to make APM data view available globally',
|
||||
e
|
||||
);
|
||||
});
|
||||
|
||||
const resourcePlugins = mapValues(plugins, (value, key) => {
|
||||
return {
|
||||
setup: value,
|
||||
|
|
|
@ -5,20 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createStaticDataView } from './create_static_data_view';
|
||||
import * as HistoricalAgentData from '../historical_data/has_historical_agent_data';
|
||||
import { DataViewsService } from '@kbn/data-views-plugin/common';
|
||||
import { APMCore } from '../typings';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import type { APMIndices } from '@kbn/apm-data-access-plugin/server';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { DataViewsService } from '@kbn/data-views-plugin/common';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { APMRouteHandlerResources } from '../apm_routes/register_apm_server_routes';
|
||||
import * as HistoricalAgentData from '../historical_data/has_historical_agent_data';
|
||||
import { APMCore } from '../typings';
|
||||
import { createStaticDataView } from './create_static_data_view';
|
||||
|
||||
function getMockedDataViewService(existingDataViewTitle: string) {
|
||||
return {
|
||||
get: jest.fn(() => ({
|
||||
title: existingDataViewTitle,
|
||||
getIndexPattern: () => existingDataViewTitle,
|
||||
})),
|
||||
createAndSave: jest.fn(),
|
||||
delete: () => {},
|
||||
} as unknown as DataViewsService;
|
||||
}
|
||||
|
||||
|
@ -36,6 +38,10 @@ const coreMock = {
|
|||
},
|
||||
} as unknown as APMCore;
|
||||
|
||||
const logger = {
|
||||
info: jest.fn,
|
||||
} as unknown as Logger;
|
||||
|
||||
const apmEventClientMock = {
|
||||
search: jest.fn(),
|
||||
indices: {
|
||||
|
@ -55,6 +61,8 @@ describe('createStaticDataView', () => {
|
|||
config: { autoCreateApmDataView: false },
|
||||
} as APMRouteHandlerResources,
|
||||
dataViewService,
|
||||
spaceId: 'default',
|
||||
logger,
|
||||
});
|
||||
expect(dataViewService.createAndSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -73,6 +81,8 @@ describe('createStaticDataView', () => {
|
|||
config: { autoCreateApmDataView: false },
|
||||
} as APMRouteHandlerResources,
|
||||
dataViewService,
|
||||
spaceId: 'default',
|
||||
logger,
|
||||
});
|
||||
expect(dataViewService.createAndSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -92,6 +102,8 @@ describe('createStaticDataView', () => {
|
|||
config: { autoCreateApmDataView: true },
|
||||
} as APMRouteHandlerResources,
|
||||
dataViewService,
|
||||
spaceId: 'default',
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(dataViewService.createAndSave).toHaveBeenCalled();
|
||||
|
@ -114,6 +126,8 @@ describe('createStaticDataView', () => {
|
|||
config: { autoCreateApmDataView: true },
|
||||
} as APMRouteHandlerResources,
|
||||
dataViewService,
|
||||
spaceId: 'default',
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(dataViewService.get).toHaveBeenCalled();
|
||||
|
@ -143,6 +157,8 @@ describe('createStaticDataView', () => {
|
|||
config: { autoCreateApmDataView: true },
|
||||
} as APMRouteHandlerResources,
|
||||
dataViewService,
|
||||
spaceId: 'default',
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(dataViewService.get).toHaveBeenCalled();
|
||||
|
|
|
@ -5,18 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { Logger, SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { DataView, DataViewsService } from '@kbn/data-views-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
DO_NOT_USE_LEGACY_APM_STATIC_DATA_VIEW_ID,
|
||||
getDataViewId,
|
||||
} from '../../../common/data_view_constants';
|
||||
import {
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID,
|
||||
TRANSACTION_DURATION,
|
||||
} from '../../../common/es_fields/apm';
|
||||
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 { getApmDataViewIndexPattern } from './get_apm_data_view_index_pattern';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { APMRouteHandlerResources } from '../apm_routes/register_apm_server_routes';
|
||||
|
||||
|
@ -29,12 +32,18 @@ export async function createStaticDataView({
|
|||
dataViewService,
|
||||
resources,
|
||||
apmEventClient,
|
||||
spaceId,
|
||||
logger,
|
||||
}: {
|
||||
dataViewService: DataViewsService;
|
||||
resources: APMRouteHandlerResources;
|
||||
apmEventClient: APMEventClient;
|
||||
spaceId: string;
|
||||
logger: Logger;
|
||||
}): CreateDataViewResponse {
|
||||
const { config } = resources;
|
||||
const dataViewId = getDataViewId(spaceId);
|
||||
logger.info(`create static data view ${dataViewId}`);
|
||||
|
||||
return withApmSpan('create_static_data_view', async () => {
|
||||
// don't auto-create APM data view if it's been disabled via the config
|
||||
|
@ -61,10 +70,13 @@ export async function createStaticDataView({
|
|||
};
|
||||
}
|
||||
|
||||
const apmDataViewTitle = getApmDataViewTitle(apmEventClient.indices);
|
||||
const apmDataViewIndexPattern = getApmDataViewIndexPattern(
|
||||
apmEventClient.indices
|
||||
);
|
||||
const shouldCreateOrUpdate = await getShouldCreateOrUpdate({
|
||||
apmDataViewTitle,
|
||||
apmDataViewIndexPattern,
|
||||
dataViewService,
|
||||
dataViewId,
|
||||
});
|
||||
|
||||
if (!shouldCreateOrUpdate) {
|
||||
|
@ -72,52 +84,48 @@ export async function createStaticDataView({
|
|||
created: false,
|
||||
reason: i18n.translate(
|
||||
'xpack.apm.dataView.alreadyExistsInActiveSpace',
|
||||
{ defaultMessage: 'Dataview already exists in the active space' }
|
||||
{
|
||||
defaultMessage:
|
||||
'Dataview already exists in the active space and does not need to be updated',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return await withApmSpan('create_data_view', async () => {
|
||||
try {
|
||||
const dataView = await createAndSaveStaticDataView({
|
||||
dataViewService,
|
||||
apmDataViewTitle,
|
||||
});
|
||||
// delete legacy global data view
|
||||
|
||||
await addDataViewToAllSpaces(resources);
|
||||
const dataView = await createAndSaveStaticDataView({
|
||||
dataViewService,
|
||||
apmDataViewIndexPattern,
|
||||
dataViewId,
|
||||
});
|
||||
|
||||
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',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
try {
|
||||
await dataViewService.delete(DO_NOT_USE_LEGACY_APM_STATIC_DATA_VIEW_ID);
|
||||
} catch (e) {
|
||||
// swallow error if caused by the data view (saved object) not existing
|
||||
if (!SavedObjectsErrorHelpers.isNotFoundError(e)) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { created: true, dataView };
|
||||
});
|
||||
}
|
||||
|
||||
// only create data view if it doesn't exist or was changed
|
||||
async function getShouldCreateOrUpdate({
|
||||
dataViewService,
|
||||
apmDataViewTitle,
|
||||
apmDataViewIndexPattern,
|
||||
dataViewId,
|
||||
}: {
|
||||
dataViewService: DataViewsService;
|
||||
apmDataViewTitle: string;
|
||||
apmDataViewIndexPattern: string;
|
||||
dataViewId: string;
|
||||
}) {
|
||||
try {
|
||||
const existingDataView = await dataViewService.get(APM_STATIC_DATA_VIEW_ID);
|
||||
return existingDataView.title !== apmDataViewTitle;
|
||||
const existingDataView = await dataViewService.get(dataViewId);
|
||||
return existingDataView.getIndexPattern() !== apmDataViewIndexPattern;
|
||||
} catch (e) {
|
||||
// ignore exception if the data view (saved object) is not found
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
|
||||
|
@ -128,32 +136,21 @@ async function getShouldCreateOrUpdate({
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
apmDataViewIndexPattern,
|
||||
dataViewId,
|
||||
}: {
|
||||
dataViewService: DataViewsService;
|
||||
apmDataViewTitle: string;
|
||||
apmDataViewIndexPattern: string;
|
||||
dataViewId: string;
|
||||
}) {
|
||||
return dataViewService.createAndSave(
|
||||
{
|
||||
allowNoIndex: true,
|
||||
id: APM_STATIC_DATA_VIEW_ID,
|
||||
id: dataViewId,
|
||||
name: 'APM',
|
||||
title: apmDataViewTitle,
|
||||
title: apmDataViewIndexPattern,
|
||||
timeFieldName: '@timestamp',
|
||||
|
||||
// link to APM from Discover
|
||||
|
|
|
@ -6,23 +6,23 @@
|
|||
*/
|
||||
|
||||
import type { APMIndices } from '@kbn/apm-data-access-plugin/server';
|
||||
import { getApmDataViewTitle } from './get_apm_data_view_title';
|
||||
import { getApmDataViewIndexPattern } from './get_apm_data_view_index_pattern';
|
||||
|
||||
describe('getApmDataViewTitle', () => {
|
||||
it('returns a data view title by combining existing indicies', () => {
|
||||
const title = getApmDataViewTitle({
|
||||
describe('getApmDataViewIndexPattern', () => {
|
||||
it('returns a data view index pattern by combining existing indices', () => {
|
||||
const indexPattern = getApmDataViewIndexPattern({
|
||||
transaction: 'apm-*-transaction-*',
|
||||
span: 'apm-*-span-*',
|
||||
error: 'apm-*-error-*',
|
||||
metric: 'apm-*-metrics-*',
|
||||
} as APMIndices);
|
||||
expect(title).toBe(
|
||||
expect(indexPattern).toBe(
|
||||
'apm-*-transaction-*,apm-*-span-*,apm-*-error-*,apm-*-metrics-*'
|
||||
);
|
||||
});
|
||||
|
||||
it('removes duplicates', () => {
|
||||
const title = getApmDataViewTitle({
|
||||
const title = getApmDataViewIndexPattern({
|
||||
transaction: 'apm-*',
|
||||
span: 'apm-*',
|
||||
error: 'apm-*',
|
|
@ -8,7 +8,7 @@
|
|||
import { uniq } from 'lodash';
|
||||
import type { APMIndices } from '@kbn/apm-data-access-plugin/server';
|
||||
|
||||
export function getApmDataViewTitle(apmIndices: APMIndices) {
|
||||
export function getApmDataViewIndexPattern(apmIndices: APMIndices) {
|
||||
return uniq([
|
||||
apmIndices.transaction,
|
||||
apmIndices.span,
|
|
@ -5,22 +5,32 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
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 { getApmDataViewIndexPattern } from './get_apm_data_view_index_pattern';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
|
||||
const staticDataViewRoute = createApmServerRoute({
|
||||
endpoint: 'POST /internal/apm/data_view/static',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources): CreateDataViewResponse => {
|
||||
const { context, plugins, request } = resources;
|
||||
const { context, plugins, request, logger } = resources;
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const coreContext = await context.core;
|
||||
|
||||
// get name of selected (name)space
|
||||
const spacesStart = await plugins.spaces?.start();
|
||||
const spaceId =
|
||||
spacesStart?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
|
||||
|
||||
if (!spaceId) {
|
||||
throw new Error('No spaceId found');
|
||||
}
|
||||
|
||||
const dataViewStart = await plugins.dataViews.start();
|
||||
const dataViewService = await dataViewStart.dataViewsServiceFactory(
|
||||
coreContext.savedObjects.client,
|
||||
|
@ -33,6 +43,8 @@ const staticDataViewRoute = createApmServerRoute({
|
|||
dataViewService,
|
||||
resources,
|
||||
apmEventClient,
|
||||
spaceId,
|
||||
logger,
|
||||
});
|
||||
|
||||
return res;
|
||||
|
@ -40,13 +52,15 @@ const staticDataViewRoute = createApmServerRoute({
|
|||
});
|
||||
|
||||
const dataViewTitleRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/data_view/title',
|
||||
endpoint: 'GET /internal/apm/data_view/index_pattern',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ getApmIndices }): Promise<{ apmDataViewTitle: string }> => {
|
||||
handler: async ({
|
||||
getApmIndices,
|
||||
}): Promise<{ apmDataViewIndexPattern: string }> => {
|
||||
const apmIndicies = await getApmIndices();
|
||||
const apmDataViewTitle = getApmDataViewTitle(apmIndicies);
|
||||
const apmDataViewIndexPattern = getApmDataViewIndexPattern(apmIndicies);
|
||||
|
||||
return { apmDataViewTitle };
|
||||
return { apmDataViewIndexPattern };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -8684,7 +8684,6 @@
|
|||
"xpack.apm.dashboard.addDashboard.useContextFilterLabel.tooltip": "L'activation de cette option applique des filtres au tableau de bord en fonction du service et de l'environnement choisis.",
|
||||
"xpack.apm.data_view.creation_failed": "Une erreur s'est produite lors de la création de la vue de données",
|
||||
"xpack.apm.dataView.alreadyExistsInActiveSpace": "La vue de données existe déjà dans l'espace actif",
|
||||
"xpack.apm.dataView.alreadyExistsInAnotherSpace": "La vue de données existe déjà dans un autre espace mais elle n'est pas disponible dans cet espace",
|
||||
"xpack.apm.dataView.autoCreateDisabled": "La création automatique des vues de données a été désactivée via l'option de configuration \"autoCreateApmDataView\"",
|
||||
"xpack.apm.dataView.noApmData": "Aucune donnée APM",
|
||||
"xpack.apm.dependecyOperationDetailView.header.backLinkLabel": "Toutes les opérations",
|
||||
|
|
|
@ -8699,7 +8699,6 @@
|
|||
"xpack.apm.dashboard.addDashboard.useContextFilterLabel.tooltip": "このオプションを有効にすると、選択したサービスと環境に基づいてダッシュボードにフィルターが適用されます。",
|
||||
"xpack.apm.data_view.creation_failed": "データビューの作成中にエラーが発生しました",
|
||||
"xpack.apm.dataView.alreadyExistsInActiveSpace": "アクティブなスペースにはすでにデータビューが存在します",
|
||||
"xpack.apm.dataView.alreadyExistsInAnotherSpace": "データビューはすでに別のスペースに存在しますが、このスペースでは使用できません",
|
||||
"xpack.apm.dataView.autoCreateDisabled": "データビューの自動作成は、「autoCreateApmDataView」構成オプションによって無効化されています",
|
||||
"xpack.apm.dataView.noApmData": "APMデータがありません",
|
||||
"xpack.apm.dependecyOperationDetailView.header.backLinkLabel": "すべての演算",
|
||||
|
|
|
@ -8698,7 +8698,6 @@
|
|||
"xpack.apm.dashboard.addDashboard.useContextFilterLabel.tooltip": "启用此选项会根据您选择的服务和环境,将筛选应用于仪表板。",
|
||||
"xpack.apm.data_view.creation_failed": "创建数据视图时出错",
|
||||
"xpack.apm.dataView.alreadyExistsInActiveSpace": "活动工作区中已存在数据视图",
|
||||
"xpack.apm.dataView.alreadyExistsInAnotherSpace": "数据视图已在另一工作区中存在,但在此工作区中不可用",
|
||||
"xpack.apm.dataView.autoCreateDisabled": "已通过“autoCreateApmDataView”配置选项禁止自动创建数据视图",
|
||||
"xpack.apm.dataView.noApmData": "无 APM 数据",
|
||||
"xpack.apm.dependecyOperationDetailView.header.backLinkLabel": "所有操作",
|
||||
|
|
|
@ -39,7 +39,7 @@ async function getCoreWebVitalsResponse({
|
|||
dataStartPlugin,
|
||||
}: WithDataPlugin<FetchDataParams>) {
|
||||
const dataViewResponse = await callApmApi(
|
||||
'GET /internal/apm/data_view/title',
|
||||
'GET /internal/apm/data_view/index_pattern',
|
||||
{
|
||||
signal: null,
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ async function getCoreWebVitalsResponse({
|
|||
|
||||
return await esQuery<ReturnType<typeof coreWebVitalsQuery>>(dataStartPlugin, {
|
||||
params: {
|
||||
index: dataViewResponse.apmDataViewTitle,
|
||||
index: dataViewResponse.apmDataViewIndexPattern,
|
||||
...coreWebVitalsQuery(absoluteTime.start, absoluteTime.end, undefined, {
|
||||
serviceName: serviceName ? [serviceName] : undefined,
|
||||
}),
|
||||
|
@ -83,7 +83,7 @@ export async function hasRumData(
|
|||
params: WithDataPlugin<HasDataParams>
|
||||
): Promise<UXHasDataResponse> {
|
||||
const dataViewResponse = await callApmApi(
|
||||
'GET /internal/apm/data_view/title',
|
||||
'GET /internal/apm/data_view/index_pattern',
|
||||
{
|
||||
signal: null,
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ export async function hasRumData(
|
|||
params.dataStartPlugin,
|
||||
{
|
||||
params: {
|
||||
index: dataViewResponse.apmDataViewTitle,
|
||||
index: dataViewResponse.apmDataViewIndexPattern,
|
||||
...hasRumDataQuery({
|
||||
start: params?.absoluteTime?.start,
|
||||
end: params?.absoluteTime?.end,
|
||||
|
@ -102,7 +102,10 @@ export async function hasRumData(
|
|||
}
|
||||
);
|
||||
|
||||
return formatHasRumResult(esQueryResponse, dataViewResponse.apmDataViewTitle);
|
||||
return formatHasRumResult(
|
||||
esQueryResponse,
|
||||
dataViewResponse.apmDataViewIndexPattern
|
||||
);
|
||||
}
|
||||
|
||||
async function esQuery<T>(
|
||||
|
|
|
@ -9,13 +9,13 @@ import { useFetcher } from './use_fetcher';
|
|||
|
||||
export function useDynamicDataViewTitle() {
|
||||
const { data, status } = useFetcher((callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/data_view/title', {
|
||||
return callApmApi('GET /internal/apm/data_view/index_pattern', {
|
||||
isCachable: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
dataViewTitle: data?.apmDataViewTitle,
|
||||
dataViewTitle: data?.apmDataViewIndexPattern,
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export function createApmApiClient(st: supertest.SuperTest<supertest.Test>) {
|
|||
options: {
|
||||
type?: 'form-data';
|
||||
endpoint: TEndpoint;
|
||||
spaceId?: string;
|
||||
} & APIClientRequestParamsOf<TEndpoint> & { params?: { query?: { _inspect?: boolean } } }
|
||||
): Promise<SupertestReturnType<TEndpoint>> => {
|
||||
const { endpoint, type } = options;
|
||||
|
@ -27,7 +28,8 @@ export function createApmApiClient(st: supertest.SuperTest<supertest.Test>) {
|
|||
const params = 'params' in options ? (options.params as Record<string, any>) : {};
|
||||
|
||||
const { method, pathname, version } = formatRequest(endpoint, params.path);
|
||||
const url = format({ pathname, query: params?.query });
|
||||
const pathnameWithSpaceId = options.spaceId ? `/s/${options.spaceId}${pathname}` : pathname;
|
||||
const url = format({ pathname: pathnameWithSpaceId, query: params?.query });
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'kbn-xsrf': 'foo',
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import expect from '@kbn/expect';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '@kbn/apm-plugin/common/data_view_constants';
|
||||
import { getDataViewId } from '@kbn/apm-plugin/common/data_view_constants';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
|
||||
import request from 'superagent';
|
||||
|
@ -20,28 +20,33 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
const apmApiClient = getService('apmApiClient');
|
||||
const supertest = getService('supertest');
|
||||
const synthtrace = getService('synthtraceEsClient');
|
||||
const logger = getService('log');
|
||||
const dataViewPattern = 'traces-apm*,apm-*,logs-apm*,apm-*,metrics-apm*,apm-*';
|
||||
|
||||
function createDataViewWithWriteUser() {
|
||||
function createDataViewWithWriteUser({ spaceId }: { spaceId: string }) {
|
||||
return apmApiClient.writeUser({
|
||||
endpoint: 'POST /internal/apm/data_view/static',
|
||||
spaceId,
|
||||
});
|
||||
}
|
||||
|
||||
function createDataViewWithReadUser() {
|
||||
return apmApiClient.readUser({ endpoint: 'POST /internal/apm/data_view/static' });
|
||||
function createDataViewWithReadUser({ spaceId }: { spaceId: string }) {
|
||||
return apmApiClient.readUser({
|
||||
endpoint: 'POST /internal/apm/data_view/static',
|
||||
spaceId,
|
||||
});
|
||||
}
|
||||
|
||||
function deleteDataView() {
|
||||
function deleteDataView(spaceId: string) {
|
||||
return supertest
|
||||
.delete(`/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}?force=true`)
|
||||
.delete(`/s/${spaceId}/api/saved_objects/index-pattern/${getDataViewId(spaceId)}?force=true`)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
}
|
||||
|
||||
function getDataView({ space }: { space: string }) {
|
||||
const spacePrefix = space !== 'default' ? `/s/${space}` : '';
|
||||
function getDataView({ spaceId }: { spaceId: string }) {
|
||||
const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : '';
|
||||
return supertest.get(
|
||||
`${spacePrefix}/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}`
|
||||
`${spacePrefix}/api/saved_objects/index-pattern/${getDataViewId(spaceId)}`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -56,7 +61,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
registry.when('no mappings exist', { config: 'basic', archives: [] }, () => {
|
||||
let response: SupertestReturnType<'POST /internal/apm/data_view/static'>;
|
||||
before(async () => {
|
||||
response = await createDataViewWithWriteUser();
|
||||
response = await createDataViewWithWriteUser({ spaceId: 'default' });
|
||||
});
|
||||
|
||||
it('does not create data view', async () => {
|
||||
|
@ -68,10 +73,10 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('cannot fetch data view', async () => {
|
||||
const res = await getDataView({ space: 'default' });
|
||||
const res = await getDataView({ spaceId: 'default' });
|
||||
expect(res.status).to.be(404);
|
||||
expect(res.body.message).to.eql(
|
||||
'Saved object [index-pattern/apm_static_index_pattern_id] not found'
|
||||
'Saved object [index-pattern/apm_static_data_view_id_default] not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -86,14 +91,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteDataView();
|
||||
try {
|
||||
await Promise.all([deleteDataView('default'), deleteDataView('foo')]);
|
||||
} catch (e) {
|
||||
logger.error(`Could not delete data views ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
describe('when creating data view with write user', () => {
|
||||
let response: SupertestReturnType<'POST /internal/apm/data_view/static'>;
|
||||
|
||||
before(async () => {
|
||||
response = await createDataViewWithWriteUser();
|
||||
response = await createDataViewWithWriteUser({ spaceId: 'default' });
|
||||
});
|
||||
|
||||
it('successfully creates the apm data view', async () => {
|
||||
|
@ -102,7 +111,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
// @ts-expect-error
|
||||
const dataView = response.body.dataView as DataView;
|
||||
|
||||
expect(dataView.id).to.be('apm_static_index_pattern_id');
|
||||
expect(dataView.id).to.be('apm_static_data_view_id_default');
|
||||
expect(dataView.name).to.be('APM');
|
||||
expect(dataView.title).to.be('traces-apm*,apm-*,logs-apm*,apm-*,metrics-apm*,apm-*');
|
||||
});
|
||||
|
@ -112,8 +121,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
let dataViewResponse: request.Response;
|
||||
|
||||
before(async () => {
|
||||
await createDataViewWithWriteUser();
|
||||
dataViewResponse = await getDataView({ space: 'default' });
|
||||
await createDataViewWithWriteUser({ spaceId: 'default' });
|
||||
dataViewResponse = await getDataView({ spaceId: 'default' });
|
||||
});
|
||||
|
||||
it('return 200', () => {
|
||||
|
@ -121,7 +130,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('has correct id', () => {
|
||||
expect(dataViewResponse.body.id).to.be('apm_static_index_pattern_id');
|
||||
expect(dataViewResponse.body.id).to.be('apm_static_data_view_id_default');
|
||||
});
|
||||
|
||||
it('has correct title', () => {
|
||||
|
@ -170,7 +179,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
describe('when creating data view via read user', () => {
|
||||
it('throws an error', async () => {
|
||||
try {
|
||||
await createDataViewWithReadUser();
|
||||
await createDataViewWithReadUser({ spaceId: 'default' });
|
||||
} catch (e) {
|
||||
const err = e as ApmApiError;
|
||||
const responseBody = err.res.body;
|
||||
|
@ -184,30 +193,44 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
describe('when creating data view twice', () => {
|
||||
it('returns 200 response with reason, if data view already exists', async () => {
|
||||
await createDataViewWithWriteUser();
|
||||
const res = await createDataViewWithWriteUser();
|
||||
await createDataViewWithWriteUser({ spaceId: 'default' });
|
||||
const res = await createDataViewWithWriteUser({ spaceId: 'default' });
|
||||
|
||||
expect(res.status).to.be(200);
|
||||
expect(res.body).to.eql({
|
||||
created: false,
|
||||
reason: 'Dataview already exists in the active space',
|
||||
reason: 'Dataview already exists in the active space and does not need to be updated',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 "default" space', async () => {
|
||||
await createDataViewWithWriteUser({ spaceId: 'default' });
|
||||
const res = await getDataView({ spaceId: 'default' });
|
||||
expect(res.body.id).to.eql('apm_static_data_view_id_default');
|
||||
expect(res.body.namespaces).to.eql(['default']);
|
||||
});
|
||||
|
||||
it('cannot be retrieved from the "foo" space', async () => {
|
||||
await createDataViewWithWriteUser({ spaceId: 'default' });
|
||||
const res = await getDataView({ spaceId: 'foo' });
|
||||
expect(res.body.statusCode).to.be(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when creating data view in "foo" space', async () => {
|
||||
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']);
|
||||
await createDataViewWithWriteUser({ spaceId: 'foo' });
|
||||
const res = await getDataView({ spaceId: 'foo' });
|
||||
expect(res.body.id).to.eql('apm_static_data_view_id_foo');
|
||||
expect(res.body.namespaces).to.eql(['foo']);
|
||||
});
|
||||
|
||||
it('cannot be retrieved from the "default" space', async () => {
|
||||
await createDataViewWithWriteUser({ spaceId: 'foo' });
|
||||
const res = await getDataView({ spaceId: 'default' });
|
||||
expect(res.body.statusCode).to.be(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue