[APM] Make UI indices space aware (support for spaces) (#126176)

* new apm indices saved object

* adding some comments

* fixing typo

* addressing PR comments

* fixing tests

* addressing PR comments

* fixing tests

* fixing test

* showing callout with space name

* addressing PR changes
This commit is contained in:
Cauê Marcondes 2022-02-24 13:04:45 -05:00 committed by GitHub
parent 43a2622b97
commit 4e238ecbe5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 397 additions and 68 deletions

View file

@ -8,9 +8,9 @@
// the types have to match the names of the saved object mappings
// in /x-pack/plugins/apm/mappings.json
// APM indices
export const APM_INDICES_SAVED_OBJECT_TYPE = 'apm-indices';
export const APM_INDICES_SAVED_OBJECT_ID = 'apm-indices';
// APM index settings
export const APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE = 'apm-indices';
export const APM_INDEX_SETTINGS_SAVED_OBJECT_ID = 'apm-indices';
// APM telemetry
export const APM_TELEMETRY_SAVED_OBJECT_TYPE = 'apm-telemetry';

View file

@ -1,37 +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 { render } from '@testing-library/react';
import React from 'react';
import { ApmIndices } from '.';
import * as hooks from '../../../../hooks/use_fetcher';
import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
describe('ApmIndices', () => {
it('should not get stuck in infinite loop', () => {
const spy = jest.spyOn(hooks, 'useFetcher').mockReturnValue({
data: undefined,
status: hooks.FETCH_STATUS.LOADING,
refetch: jest.fn(),
});
const { getByText } = render(
<MockApmPluginContextWrapper>
<ApmIndices />
</MockApmPluginContextWrapper>
);
expect(getByText('Indices')).toMatchInlineSnapshot(`
<h2
class="euiTitle euiTitle--small"
>
Indices
</h2>
`);
expect(spy).toHaveBeenCalledTimes(2);
});
});

View file

@ -8,6 +8,7 @@
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
@ -19,9 +20,12 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useEffect, useState } from 'react';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { ApmPluginStartDeps } from '../../../../plugin';
import { clearCache } from '../../../../services/rest/call_api';
import {
APIReturnType,
@ -93,6 +97,8 @@ const INITIAL_STATE: ApiResponse = { apmIndexSettings: [] };
export function ApmIndices() {
const { core } = useApmPluginContext();
const { services } = useKibana<ApmPluginStartDeps>();
const { notifications, application } = core;
const canSave = application.capabilities.apm.save;
@ -108,6 +114,10 @@ export function ApmIndices() {
[canSave]
);
const { data: space } = useFetcher(() => {
return services.spaces?.getActiveSpace();
}, [services.spaces]);
useEffect(() => {
setApmIndices(
data.apmIndexSettings.reduce(
@ -191,6 +201,31 @@ export function ApmIndices() {
<EuiSpacer size="m" />
{space?.name && (
<>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiCallOut
color="primary"
iconType="spacesApp"
title={
<EuiText size="s">
<FormattedMessage
id="xpack.apm.settings.apmIndices.spaceDescription"
defaultMessage="The index settings apply to the {spaceName} space."
values={{
spaceName: <strong>{space?.name}</strong>,
}}
/>
</EuiText>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
</>
)}
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiForm>

View file

@ -53,6 +53,7 @@ import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/
import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension';
import { featureCatalogueEntry } from './feature_catalogue_entry';
import type { SecurityPluginStart } from '../../security/public';
import { SpacesPluginStart } from '../../spaces/public';
export type ApmPluginSetup = ReturnType<ApmPlugin['setup']>;
@ -82,6 +83,7 @@ export interface ApmPluginStartDeps {
observability: ObservabilityPublicStart;
fleet?: FleetStart;
security?: SecurityPluginStart;
spaces?: SpacesPluginStart;
}
const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', {

View file

@ -48,6 +48,7 @@ import {
TRANSACTION_TYPE,
} from '../common/elasticsearch_fieldnames';
import { tutorialProvider } from './tutorial';
import { migrateLegacyAPMIndicesToSpaceAware } from './saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware';
export class APMPlugin
implements
@ -247,6 +248,11 @@ export class APMPlugin
config: this.currentConfig,
logger: this.logger,
});
migrateLegacyAPMIndicesToSpaceAware({
coreStart: core,
logger: this.logger,
});
}
public stop() {}

View file

@ -7,13 +7,14 @@
import { SavedObjectsClient } from 'src/core/server';
import {
APM_INDICES_SAVED_OBJECT_TYPE,
APM_INDICES_SAVED_OBJECT_ID,
APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
APM_INDEX_SETTINGS_SAVED_OBJECT_ID,
} from '../../../../common/apm_saved_object_constants';
import { APMConfig } from '../../..';
import { APMRouteHandlerResources } from '../../typings';
import { withApmSpan } from '../../../utils/with_apm_span';
import { ApmIndicesConfig } from '../../../../../observability/common/typings';
import { APMIndices } from '../../../saved_objects/apm_indices';
export type { ApmIndicesConfig };
@ -22,13 +23,15 @@ type ISavedObjectsClient = Pick<SavedObjectsClient, 'get'>;
async function getApmIndicesSavedObject(
savedObjectsClient: ISavedObjectsClient
) {
const apmIndices = await withApmSpan('get_apm_indices_saved_object', () =>
savedObjectsClient.get<Partial<ApmIndicesConfig>>(
APM_INDICES_SAVED_OBJECT_TYPE,
APM_INDICES_SAVED_OBJECT_ID
)
const apmIndicesSavedObject = await withApmSpan(
'get_apm_indices_saved_object',
() =>
savedObjectsClient.get<Partial<APMIndices>>(
APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
APM_INDEX_SETTINGS_SAVED_OBJECT_ID
)
);
return apmIndices.attributes;
return apmIndicesSavedObject.attributes.apmIndices;
}
export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig {
@ -90,6 +93,6 @@ export async function getApmIndexSettings({
return apmIndices.map((configurationName) => ({
configurationName,
defaultValue: apmIndicesConfig[configurationName], // value defined in kibana[.dev].yml
savedValue: apmIndicesSavedObject[configurationName], // value saved via Saved Objects service
savedValue: apmIndicesSavedObject?.[configurationName], // value saved via Saved Objects service
}));
}

View file

@ -26,7 +26,10 @@ describe('saveApmIndices', () => {
await saveApmIndices(savedObjectsClient, apmIndices);
expect(savedObjectsClient.create).toHaveBeenCalledWith(
expect.any(String),
{ settingA: 'aa', settingF: 'ff', settingG: 'gg' },
{
apmIndices: { settingA: 'aa', settingF: 'ff', settingG: 'gg' },
isSpaceAware: true,
},
expect.any(Object)
);
});

View file

@ -7,9 +7,10 @@
import { SavedObjectsClientContract } from '../../../../../../../src/core/server';
import {
APM_INDICES_SAVED_OBJECT_TYPE,
APM_INDICES_SAVED_OBJECT_ID,
APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
APM_INDEX_SETTINGS_SAVED_OBJECT_ID,
} from '../../../../common/apm_saved_object_constants';
import { APMIndices } from '../../../saved_objects/apm_indices';
import { withApmSpan } from '../../../utils/with_apm_span';
import { ApmIndicesConfig } from './get_apm_indices';
@ -18,13 +19,10 @@ export function saveApmIndices(
apmIndices: Partial<ApmIndicesConfig>
) {
return withApmSpan('save_apm_indices', () =>
savedObjectsClient.create(
APM_INDICES_SAVED_OBJECT_TYPE,
removeEmpty(apmIndices),
{
id: APM_INDICES_SAVED_OBJECT_ID,
overwrite: true,
}
savedObjectsClient.create<APMIndices>(
APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
{ apmIndices: removeEmpty(apmIndices), isSpaceAware: true },
{ id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, overwrite: true }
)
);
}

View file

@ -10,20 +10,43 @@ import { i18n } from '@kbn/i18n';
import { updateApmOssIndexPaths } from './migrations/update_apm_oss_index_paths';
import { ApmIndicesConfigName } from '..';
const properties: { [Property in ApmIndicesConfigName]: { type: 'keyword' } } =
{
sourcemap: { type: 'keyword' },
error: { type: 'keyword' },
onboarding: { type: 'keyword' },
span: { type: 'keyword' },
transaction: { type: 'keyword' },
metric: { type: 'keyword' },
export interface APMIndices {
apmIndices?: {
sourcemap?: string;
error?: string;
onboarding?: string;
span?: string;
transaction?: string;
metric?: string;
};
isSpaceAware?: boolean;
}
const properties: {
apmIndices: {
properties: {
[Property in ApmIndicesConfigName]: { type: 'keyword' };
};
};
isSpaceAware: { type: 'boolean' };
} = {
apmIndices: {
properties: {
sourcemap: { type: 'keyword' },
error: { type: 'keyword' },
onboarding: { type: 'keyword' },
span: { type: 'keyword' },
transaction: { type: 'keyword' },
metric: { type: 'keyword' },
},
},
isSpaceAware: { type: 'boolean' },
};
export const apmIndices: SavedObjectsType = {
name: 'apm-indices',
hidden: false,
namespaceType: 'agnostic',
namespaceType: 'single',
mappings: { properties },
management: {
importableAndExportable: true,

View file

@ -0,0 +1,206 @@
/*
* 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 type { CoreStart, Logger } from 'src/core/server';
import {
APM_INDEX_SETTINGS_SAVED_OBJECT_ID,
APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
} from '../../../common/apm_saved_object_constants';
import { migrateLegacyAPMIndicesToSpaceAware } from './migrate_legacy_apm_indices_to_space_aware';
const loggerMock = {
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
} as unknown as Logger;
describe('migrateLegacyAPMIndicesToSpaceAware', () => {
describe('when legacy APM indices is not found', () => {
const mockBulkCreate = jest.fn();
const mockCreate = jest.fn();
const mockFind = jest.fn();
const core = {
savedObjects: {
createInternalRepository: jest.fn().mockReturnValue({
get: () => {
throw new Error('BOOM');
},
find: mockFind,
bulkCreate: mockBulkCreate,
create: mockCreate,
}),
},
} as unknown as CoreStart;
it('does not save any new saved object', () => {
migrateLegacyAPMIndicesToSpaceAware({
coreStart: core,
logger: loggerMock,
});
expect(mockFind).not.toHaveBeenCalled();
expect(mockBulkCreate).not.toHaveBeenCalled();
expect(mockCreate).not.toHaveBeenCalled();
});
});
describe('when only default space is available', () => {
const mockBulkCreate = jest.fn();
const mockCreate = jest.fn();
const mockSpaceFind = jest.fn().mockReturnValue({
page: 1,
per_page: 10000,
total: 3,
saved_objects: [
{
type: 'space',
id: 'default',
attributes: {
name: 'Default',
},
references: [],
migrationVersion: {
space: '6.6.0',
},
coreMigrationVersion: '8.2.0',
updated_at: '2022-02-22T14:13:28.839Z',
version: 'WzI4OSwxXQ==',
score: 0,
},
],
});
const core = {
savedObjects: {
createInternalRepository: jest.fn().mockReturnValue({
get: jest.fn().mockReturnValue({
id: 'apm-indices',
type: 'apm-indices',
namespaces: [],
updated_at: '2022-02-22T14:17:10.584Z',
version: 'WzE1OSwxXQ==',
attributes: {
transaction: 'default-apm-*',
span: 'default-apm-*',
error: 'default-apm-*',
metric: 'default-apm-*',
sourcemap: 'default-apm-*',
onboarding: 'default-apm-*',
},
references: [],
migrationVersion: {
'apm-indices': '7.16.0',
},
coreMigrationVersion: '8.2.0',
}),
find: mockSpaceFind,
bulkCreate: mockBulkCreate,
create: mockCreate,
}),
},
} as unknown as CoreStart;
it('creates new default saved object with space awareness and delete legacy', async () => {
await migrateLegacyAPMIndicesToSpaceAware({
coreStart: core,
logger: loggerMock,
});
expect(mockCreate).toBeCalledWith(
APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
{
apmIndices: {
transaction: 'default-apm-*',
span: 'default-apm-*',
error: 'default-apm-*',
metric: 'default-apm-*',
sourcemap: 'default-apm-*',
onboarding: 'default-apm-*',
},
isSpaceAware: true,
},
{
id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID,
overwrite: true,
}
);
});
});
describe('when multiple spaces are found', () => {
const mockBulkCreate = jest.fn();
const mockCreate = jest.fn();
const savedObjects = [
{ id: 'default', name: 'Default' },
{ id: 'space-a', name: 'Space A' },
{ id: 'space-b', name: 'Space B' },
];
const mockSpaceFind = jest.fn().mockReturnValue({
page: 1,
per_page: 10000,
total: 3,
saved_objects: savedObjects.map(({ id, name }) => {
return {
type: 'space',
id,
attributes: { name },
references: [],
migrationVersion: { space: '6.6.0' },
coreMigrationVersion: '8.2.0',
updated_at: '2022-02-22T14:13:28.839Z',
version: 'WzI4OSwxXQ==',
score: 0,
};
}),
});
const attributes = {
transaction: 'space-apm-*',
span: 'space-apm-*',
error: 'space-apm-*',
metric: 'space-apm-*',
sourcemap: 'space-apm-*',
onboarding: 'space-apm-*',
};
const core = {
savedObjects: {
createInternalRepository: jest.fn().mockReturnValue({
get: jest.fn().mockReturnValue({
id: 'apm-indices',
type: 'apm-indices',
namespaces: [],
updated_at: '2022-02-22T14:17:10.584Z',
version: 'WzE1OSwxXQ==',
attributes,
references: [],
migrationVersion: {
'apm-indices': '7.16.0',
},
coreMigrationVersion: '8.2.0',
}),
find: mockSpaceFind,
bulkCreate: mockBulkCreate,
create: mockCreate,
}),
},
} as unknown as CoreStart;
it('creates multiple saved objects with space awareness and delete legacies', async () => {
await migrateLegacyAPMIndicesToSpaceAware({
coreStart: core,
logger: loggerMock,
});
expect(mockCreate).toBeCalled();
expect(mockBulkCreate).toBeCalledWith(
savedObjects
.filter(({ id }) => id !== 'default')
.map(({ id }) => {
return {
type: APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID,
initialNamespaces: [id],
attributes: { apmIndices: attributes, isSpaceAware: true },
};
})
);
});
});
});

View file

@ -0,0 +1,90 @@
/*
* 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 type {
CoreStart,
Logger,
ISavedObjectsRepository,
} from 'src/core/server';
import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server';
import {
APM_INDEX_SETTINGS_SAVED_OBJECT_ID,
APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
} from '../../../common/apm_saved_object_constants';
import { ApmIndicesConfig } from '../../routes/settings/apm_indices/get_apm_indices';
import { APMIndices } from '../apm_indices';
async function fetchLegacyAPMIndices(repository: ISavedObjectsRepository) {
try {
const apmIndices = await repository.get<
Partial<ApmIndicesConfig & { isSpaceAware: boolean }>
>(APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, APM_INDEX_SETTINGS_SAVED_OBJECT_ID);
if (apmIndices.attributes.isSpaceAware) {
// This has already been migrated to become space-aware
return null;
}
return apmIndices;
} catch (err) {
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
// This can happen if APM is not being used
return null;
}
throw err;
}
}
export async function migrateLegacyAPMIndicesToSpaceAware({
coreStart,
logger,
}: {
coreStart: CoreStart;
logger: Logger;
}) {
const repository = coreStart.savedObjects.createInternalRepository(['space']);
try {
// Fetch legacy APM indices
const legacyAPMIndices = await fetchLegacyAPMIndices(repository);
if (legacyAPMIndices === null) {
return;
}
// Fetch spaces available
const spaces = await repository.find({
type: 'space',
page: 1,
perPage: 10_000, // max number of spaces as of 8.2
fields: ['name'], // to avoid fetching *all* fields
});
const savedObjectAttributes: APMIndices = {
apmIndices: legacyAPMIndices.attributes,
isSpaceAware: true,
};
// Calls create first to update the default space setting isSpaceAware to true
await repository.create<
Partial<ApmIndicesConfig & { isSpaceAware: boolean }>
>(APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, savedObjectAttributes, {
id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID,
overwrite: true,
});
// Create new APM indices space aware for all spaces available
await repository.bulkCreate<Partial<APMIndices>>(
spaces.saved_objects
// Skip default space since it was already updated
.filter(({ id: spaceId }) => spaceId !== 'default')
.map(({ id: spaceId }) => ({
id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID,
type: APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
initialNamespaces: [spaceId],
attributes: savedObjectAttributes,
}))
);
} catch (e) {
logger.error('Failed to migrate legacy APM indices object: ' + e.message);
}
}