diff --git a/x-pack/plugins/apm/common/apm_saved_object_constants.ts b/x-pack/plugins/apm/common/apm_saved_object_constants.ts
index 7d9e571242afe..17c5a802a440a 100644
--- a/x-pack/plugins/apm/common/apm_saved_object_constants.ts
+++ b/x-pack/plugins/apm/common/apm_saved_object_constants.ts
@@ -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';
diff --git a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.test.tsx b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.test.tsx
deleted file mode 100644
index 1b19bb5860b2c..0000000000000
--- a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.test.tsx
+++ /dev/null
@@ -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(
-
-
-
- );
-
- expect(getByText('Indices')).toMatchInlineSnapshot(`
-
- Indices
-
- `);
-
- expect(spy).toHaveBeenCalledTimes(2);
- });
-});
diff --git a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx
index 1a1654e22eb19..383be90168a10 100644
--- a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx
@@ -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();
+
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() {
+ {space?.name && (
+ <>
+
+
+
+ {space?.name},
+ }}
+ />
+
+ }
+ />
+
+
+
+ >
+ )}
+
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index 952df64da840a..75c3c290512d8 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -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;
@@ -82,6 +83,7 @@ export interface ApmPluginStartDeps {
observability: ObservabilityPublicStart;
fleet?: FleetStart;
security?: SecurityPluginStart;
+ spaces?: SpacesPluginStart;
}
const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', {
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index e5ad2cb6c6c1f..34a09fe57a05b 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -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() {}
diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts
index 450ce3fa18dad..7d98502ee5d93 100644
--- a/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts
+++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts
@@ -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;
async function getApmIndicesSavedObject(
savedObjectsClient: ISavedObjectsClient
) {
- const apmIndices = await withApmSpan('get_apm_indices_saved_object', () =>
- savedObjectsClient.get>(
- APM_INDICES_SAVED_OBJECT_TYPE,
- APM_INDICES_SAVED_OBJECT_ID
- )
+ const apmIndicesSavedObject = await withApmSpan(
+ 'get_apm_indices_saved_object',
+ () =>
+ savedObjectsClient.get>(
+ 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
}));
}
diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts
index bce6857aa4ff2..0c91bccb42999 100644
--- a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts
+++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts
@@ -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)
);
});
diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts
index 14a5830d8246c..2bd4273910bbf 100644
--- a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts
+++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts
@@ -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
) {
return withApmSpan('save_apm_indices', () =>
- savedObjectsClient.create(
- APM_INDICES_SAVED_OBJECT_TYPE,
- removeEmpty(apmIndices),
- {
- id: APM_INDICES_SAVED_OBJECT_ID,
- overwrite: true,
- }
+ savedObjectsClient.create(
+ APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE,
+ { apmIndices: removeEmpty(apmIndices), isSpaceAware: true },
+ { id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, overwrite: true }
)
);
}
diff --git a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts
index 4aa6c4953056a..4a3b0d32e9667 100644
--- a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts
+++ b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts
@@ -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,
diff --git a/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.test.ts b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.test.ts
new file mode 100644
index 0000000000000..404e08a6b112b
--- /dev/null
+++ b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.test.ts
@@ -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 },
+ };
+ })
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.ts b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.ts
new file mode 100644
index 0000000000000..130070b80ff14
--- /dev/null
+++ b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.ts
@@ -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
+ >(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
+ >(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>(
+ 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);
+ }
+}