diff --git a/packages/kbn-custom-integrations/src/components/create/form.tsx b/packages/kbn-custom-integrations/src/components/create/form.tsx
index 2273db68a479a..a05fa97b78fdf 100644
--- a/packages/kbn-custom-integrations/src/components/create/form.tsx
+++ b/packages/kbn-custom-integrations/src/components/create/form.tsx
@@ -28,6 +28,11 @@ import {
} from '../../state_machines/create/types';
import { Dataset, IntegrationError } from '../../types';
import { hasFailedSelector } from '../../state_machines/create/selectors';
+import {
+ datasetNameWillBePrefixed,
+ getDatasetNamePrefix,
+ prefixDatasetName,
+} from '../../state_machines/create/pipelines/fields';
// NOTE: Hardcoded for now. We will likely extend the functionality here to allow the selection of the type.
// And also to allow adding multiple datasets.
@@ -50,6 +55,7 @@ export const ConnectedCreateCustomIntegrationForm = ({
testSubjects?: CreateTestSubjects;
}) => {
const [state, send] = useActor(machineRef);
+
const updateIntegrationName = useCallback(
(integrationName: string) => {
send({ type: 'UPDATE_FIELDS', fields: { integrationName } });
@@ -181,17 +187,32 @@ export const CreateCustomIntegrationForm = ({
}
- helpText={i18n.translate('customIntegrationsPackage.create.dataset.helper', {
- defaultMessage:
- "All lowercase, max 100 chars, special characters will be replaced with '_'.",
- })}
+ helpText={[
+ i18n.translate('customIntegrationsPackage.create.dataset.helper', {
+ defaultMessage:
+ "All lowercase, max 100 chars, special characters will be replaced with '_'.",
+ }),
+ datasetNameWillBePrefixed(datasetName, integrationName)
+ ? i18n.translate(
+ 'customIntegrationsPackage.create.dataset.name.tooltipPrefixMessage',
+ {
+ defaultMessage:
+ 'This name will be prefixed with {prefixValue}, e.g. {prefixedDatasetName}',
+ values: {
+ prefixValue: getDatasetNamePrefix(integrationName),
+ prefixedDatasetName: prefixDatasetName(datasetName, integrationName),
+ },
+ }
+ )
+ : '',
+ ].join(' ')}
isInvalid={hasErrors(errors?.fields?.datasets?.[0]?.name) && touchedFields.datasets}
error={errorsList(errors?.fields?.datasets?.[0]?.name)}
>
diff --git a/packages/kbn-custom-integrations/src/components/create/utils.ts b/packages/kbn-custom-integrations/src/components/create/utils.ts
index 3af857be37236..688bea61128ed 100644
--- a/packages/kbn-custom-integrations/src/components/create/utils.ts
+++ b/packages/kbn-custom-integrations/src/components/create/utils.ts
@@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
-export const replaceSpecialChars = (filename: string) => {
+export const replaceSpecialChars = (value: string) => {
// Replace special characters with _
- const replacedSpecialCharacters = filename.replaceAll(/[^a-zA-Z0-9_]/g, '_');
+ const replacedSpecialCharacters = value.replaceAll(/[^a-zA-Z0-9_]/g, '_');
// Allow only one _ in a row
const noRepetitions = replacedSpecialCharacters.replaceAll(/[\_]{2,}/g, '_');
return noRepetitions;
diff --git a/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts b/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts
index 38ef7fb993906..2ca7e09ae8f6e 100644
--- a/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts
+++ b/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts
@@ -27,11 +27,20 @@ export type CreateCustomIntegrationNotificationEvent =
};
export const CreateIntegrationNotificationEventSelectors = {
- integrationCreated: (context: CreateCustomIntegrationContext) =>
- ({
- type: 'INTEGRATION_CREATED',
- fields: context.fields,
- } as CreateCustomIntegrationNotificationEvent),
+ integrationCreated: (
+ context: CreateCustomIntegrationContext,
+ event: CreateCustomIntegrationEvent
+ ) => {
+ return 'data' in event && 'integrationName' in event.data && 'datasets' in event.data
+ ? ({
+ type: 'INTEGRATION_CREATED',
+ fields: {
+ integrationName: event.data.integrationName,
+ datasets: event.data.datasets,
+ },
+ } as CreateCustomIntegrationNotificationEvent)
+ : null;
+ },
integrationCleanup: (
context: CreateCustomIntegrationContext,
event: CreateCustomIntegrationEvent
diff --git a/packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts b/packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts
new file mode 100644
index 0000000000000..3a7fef9ce98a7
--- /dev/null
+++ b/packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts
@@ -0,0 +1,134 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { pipe } from 'fp-ts/lib/pipeable';
+import { replaceSpecialChars } from '../../../components/create/utils';
+import { CreateCustomIntegrationContext, UpdateFieldsEvent, WithTouchedFields } from '../types';
+
+type ValuesTuple = [CreateCustomIntegrationContext, UpdateFieldsEvent];
+
+// Pipeline for updating the fields and touchedFields properties within context
+export const executeFieldsPipeline = (
+ context: CreateCustomIntegrationContext,
+ event: UpdateFieldsEvent
+) => {
+ return pipe(
+ [context, event] as ValuesTuple,
+ updateFields(context),
+ updateTouchedFields(context),
+ maybeMatchDatasetNameToIntegrationName(context),
+ replaceSpecialCharacters(context)
+ );
+};
+
+const updateFields =
+ (originalContext: CreateCustomIntegrationContext) =>
+ (values: ValuesTuple): ValuesTuple => {
+ const [context, event] = values;
+
+ const mergedContext = {
+ ...context,
+ fields: {
+ ...context.fields,
+ ...event.fields,
+ },
+ };
+ return [mergedContext, event];
+ };
+
+const updateTouchedFields =
+ (originalContext: CreateCustomIntegrationContext) =>
+ (values: ValuesTuple): ValuesTuple => {
+ const [context, event] = values;
+
+ const mergedContext = {
+ ...context,
+ touchedFields: {
+ ...context.touchedFields,
+ ...Object.keys(event.fields).reduce(
+ (acc, field) => ({ ...acc, [field]: true }),
+ {} as WithTouchedFields['touchedFields']
+ ),
+ },
+ };
+ return [mergedContext, event];
+ };
+
+const maybeMatchDatasetNameToIntegrationName =
+ (originalContext: CreateCustomIntegrationContext) =>
+ (values: ValuesTuple): ValuesTuple => {
+ const [context, event] = values;
+ if (context.touchedFields.integrationName && !context.touchedFields.datasets) {
+ return [
+ {
+ ...context,
+ fields: {
+ ...context.fields,
+ datasets: context.fields.datasets.map((dataset, index) => ({
+ ...dataset,
+ name: index === 0 ? context.fields.integrationName : dataset.name,
+ })),
+ },
+ },
+ event,
+ ];
+ } else {
+ return [context, event];
+ }
+ };
+
+const replaceSpecialCharacters =
+ (originalContext: CreateCustomIntegrationContext) =>
+ (values: ValuesTuple): ValuesTuple => {
+ const [context, event] = values;
+
+ const mergedContext = {
+ ...context,
+ fields: {
+ ...context.fields,
+ integrationName: replaceSpecialChars(context.fields.integrationName),
+ datasets: context.fields.datasets.map((dataset) => ({
+ ...dataset,
+ name: replaceSpecialChars(dataset.name),
+ })),
+ },
+ };
+
+ return [mergedContext, event];
+ };
+
+export const getDatasetNamePrefix = (integrationName: string) => `${integrationName}.`;
+export const datasetNameIsPrefixed = (datasetName: string, integrationName: string) =>
+ datasetName.startsWith(getDatasetNamePrefix(integrationName));
+export const datasetNameWillBePrefixed = (datasetName: string, integrationName: string) =>
+ datasetName !== integrationName;
+export const prefixDatasetName = (datasetName: string, integrationName: string) =>
+ `${getDatasetNamePrefix(integrationName)}${datasetName}`;
+
+// The content after the integration name prefix.
+export const getDatasetNameWithoutPrefix = (datasetName: string, integrationName: string) =>
+ datasetNameIsPrefixed(datasetName, integrationName)
+ ? datasetName.split(getDatasetNamePrefix(integrationName))[1]
+ : datasetName;
+
+// The machine holds unprefixed names internally to dramatically reduce complexity and improve performance for input changes in the UI.
+// Prefixed names are used at the outermost edges e.g. when providing initial state and submitting to the API.
+export const normalizeDatasetNames = (fields: UpdateFieldsEvent['fields']) => {
+ const value = {
+ ...fields,
+ ...(fields.datasets !== undefined && fields.integrationName !== undefined
+ ? {
+ datasets: fields.datasets.map((dataset) => ({
+ ...dataset,
+ name: getDatasetNameWithoutPrefix(dataset.name, fields.integrationName!),
+ })),
+ }
+ : {}),
+ };
+ return value;
+};
diff --git a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts
index ad93d3eb8cb05..2ae0e51bad3d8 100644
--- a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts
+++ b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts
@@ -28,10 +28,12 @@ import {
DefaultCreateCustomIntegrationContext,
WithErrors,
WithPreviouslyCreatedIntegration,
- WithTouchedFields,
- WithFields,
} from './types';
-import { replaceSpecialChars } from '../../components/create/utils';
+import {
+ datasetNameWillBePrefixed,
+ executeFieldsPipeline,
+ prefixDatasetName,
+} from './pipelines/fields';
export const createPureCreateCustomIntegrationStateMachine = (
initialContext: DefaultCreateCustomIntegrationContext = DEFAULT_CONTEXT
@@ -211,32 +213,12 @@ export const createPureCreateCustomIntegrationStateMachine = (
: {};
}),
storeFields: actions.assign((context, event) => {
- return event.type === 'UPDATE_FIELDS'
- ? ({
- fields: {
- ...context.fields,
- ...event.fields,
- integrationName:
- event.fields.integrationName !== undefined
- ? replaceSpecialChars(event.fields.integrationName)
- : context.fields.integrationName,
- datasets:
- event.fields.datasets !== undefined
- ? event.fields.datasets.map((dataset) => ({
- ...dataset,
- name: replaceSpecialChars(dataset.name),
- }))
- : context.fields.datasets,
- },
- touchedFields: {
- ...context.touchedFields,
- ...Object.keys(event.fields).reduce(
- (acc, field) => ({ ...acc, [field]: true }),
- {} as WithTouchedFields['touchedFields']
- ),
- },
- } as WithFields & WithTouchedFields)
- : {};
+ if (event.type === 'UPDATE_FIELDS') {
+ const [contextResult] = executeFieldsPipeline(context, event);
+ return contextResult;
+ } else {
+ return {};
+ }
}),
resetValues: actions.assign((context, event) => {
return {
@@ -315,7 +297,15 @@ export const createCreateCustomIntegrationStateMachine = ({
}),
}),
save: (context) => {
- return integrationsClient.createCustomIntegration(context.fields);
+ return integrationsClient.createCustomIntegration({
+ ...context.fields,
+ datasets: context.fields.datasets.map((dataset) => ({
+ ...dataset,
+ name: datasetNameWillBePrefixed(dataset.name, context.fields.integrationName)
+ ? prefixDatasetName(dataset.name, context.fields.integrationName)
+ : dataset.name,
+ })),
+ });
},
cleanup: (context) => {
return integrationsClient.deleteCustomIntegration({
diff --git a/packages/kbn-custom-integrations/src/state_machines/create/types.ts b/packages/kbn-custom-integrations/src/state_machines/create/types.ts
index e7cfad728d78e..ec0e61f2309d9 100644
--- a/packages/kbn-custom-integrations/src/state_machines/create/types.ts
+++ b/packages/kbn-custom-integrations/src/state_machines/create/types.ts
@@ -19,7 +19,9 @@ export interface WithTouchedFields {
touchedFields: Record;
}
-export type CreateInitialState = WithOptions & WithFields & WithPreviouslyCreatedIntegration;
+export type CreateInitialState = Partial &
+ Partial &
+ WithPreviouslyCreatedIntegration;
export interface WithOptions {
options: {
@@ -91,11 +93,13 @@ export type CreateCustomIntegrationTypestate =
export type CreateCustomIntegrationContext = CreateCustomIntegrationTypestate['context'];
+export interface UpdateFieldsEvent {
+ type: 'UPDATE_FIELDS';
+ fields: Partial;
+}
+
export type CreateCustomIntegrationEvent =
- | {
- type: 'UPDATE_FIELDS';
- fields: Partial;
- }
+ | UpdateFieldsEvent
| {
type: 'INITIALIZE';
}
diff --git a/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts b/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts
index 4bd71313bf43a..3baa44d76a7e6 100644
--- a/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts
+++ b/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts
@@ -20,6 +20,8 @@ import {
import { createCreateCustomIntegrationStateMachine } from '../create/state_machine';
import { IIntegrationsClient } from '../services/integrations_client';
import { CustomIntegrationsNotificationChannel } from './notifications';
+import { executeFieldsPipeline, normalizeDatasetNames } from '../create/pipelines/fields';
+import { CreateInitialState } from '../create/types';
export const createPureCustomIntegrationsStateMachine = (
initialContext: DefaultCustomIntegrationsContext = DEFAULT_CONTEXT
@@ -95,22 +97,32 @@ export const createCustomIntegrationsStateMachine = ({
return createPureCustomIntegrationsStateMachine(initialContext).withConfig({
services: {
createCustomIntegration: (context) => {
+ const getInitialContextForCreate = (initialCreateState: CreateInitialState) => {
+ const baseAndOptions = {
+ ...DEFAULT_CREATE_CONTEXT,
+ ...(initialCreateState ? initialCreateState : {}),
+ options: {
+ ...DEFAULT_CREATE_CONTEXT.options,
+ ...(initialCreateState?.options ? initialCreateState.options : {}),
+ },
+ };
+ const fields = initialCreateState.fields
+ ? executeFieldsPipeline(baseAndOptions, {
+ type: 'UPDATE_FIELDS',
+ fields: normalizeDatasetNames(initialCreateState.fields),
+ })[0]
+ : {};
+ return {
+ ...baseAndOptions,
+ ...fields,
+ };
+ };
+
return createCreateCustomIntegrationStateMachine({
integrationsClient,
initialContext:
- initialState.mode === 'create'
- ? {
- ...DEFAULT_CREATE_CONTEXT,
- ...(initialState?.context ? initialState?.context : {}),
- options: {
- ...DEFAULT_CREATE_CONTEXT.options,
- ...(initialState?.context?.options ? initialState.context.options : {}),
- },
- fields: {
- ...DEFAULT_CREATE_CONTEXT.fields,
- ...(initialState?.context?.fields ? initialState.context.fields : {}),
- },
- }
+ initialState.mode === 'create' && initialState.context
+ ? getInitialContextForCreate(initialState.context)
: DEFAULT_CREATE_CONTEXT,
});
},
diff --git a/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts b/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts
index c5606a7e2e154..047bd4a7644d0 100644
--- a/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts
+++ b/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts
@@ -19,6 +19,8 @@ import {
IntegrationNotInstalledError,
NamingCollisionError,
UnknownError,
+ IntegrationName,
+ Dataset,
} from '../../types';
const GENERIC_CREATE_ERROR_MESSAGE = i18n.translate(
@@ -70,6 +72,7 @@ export class IntegrationsClient implements IIntegrationsClient {
return {
integrationName: params.integrationName,
+ datasets: params.datasets,
installedAssets: data.items,
};
} catch (error) {
@@ -128,7 +131,8 @@ export const createCustomIntegrationResponseRT = rt.exact(
);
export interface CreateCustomIntegrationValue {
- integrationName: string;
+ integrationName: IntegrationName;
+ datasets: Dataset[];
installedAssets: AssetList;
}
diff --git a/packages/kbn-custom-integrations/src/types.ts b/packages/kbn-custom-integrations/src/types.ts
index 062601239137a..84ecda1eb4df3 100644
--- a/packages/kbn-custom-integrations/src/types.ts
+++ b/packages/kbn-custom-integrations/src/types.ts
@@ -9,25 +9,26 @@
import * as rt from 'io-ts';
export const integrationNameRT = rt.string;
+export type IntegrationName = rt.TypeOf;
-const datasetTypes = rt.keyof({
+const datasetTypesRT = rt.keyof({
logs: null,
metrics: null,
});
-const dataset = rt.exact(
+export const datasetRT = rt.exact(
rt.type({
name: rt.string,
- type: datasetTypes,
+ type: datasetTypesRT,
})
);
-export type Dataset = rt.TypeOf;
+export type Dataset = rt.TypeOf;
export const customIntegrationOptionsRT = rt.exact(
rt.type({
integrationName: integrationNameRT,
- datasets: rt.array(dataset),
+ datasets: rt.array(datasetRT),
})
);
diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts
index c507d9e6732a7..ffc0742706063 100644
--- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts
+++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts
@@ -85,6 +85,7 @@ import type {
} from '../../types';
import { getDataStreams } from '../../services/epm/data_streams';
import { NamingCollisionError } from '../../services/epm/packages/custom_integrations/validation/check_naming_collision';
+import { DatasetNamePrefixError } from '../../services/epm/packages/custom_integrations/validation/check_dataset_name_format';
const CACHE_CONTROL_10_MINUTES_HEADER: HttpResponseOptions['headers'] = {
'cache-control': 'max-age=600',
@@ -452,6 +453,13 @@ export const createCustomIntegrationHandler: FleetRequestHandler<
message: error.message,
},
});
+ } else if (error instanceof DatasetNamePrefixError) {
+ return response.customError({
+ statusCode: 422,
+ body: {
+ message: error.message,
+ },
+ });
}
return await defaultFleetErrorHandler({ error, response });
}
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts
index b984b8cddbbd3..c3512443c8828 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts
@@ -15,7 +15,7 @@ export const generateDatastreamEntries = (
const { name, type } = dataset;
return {
type,
- dataset: `${packageName}.${name}`,
+ dataset: `${name}`,
title: `Data stream for the ${packageName} custom integration, and ${name} dataset.`,
package: packageName,
path: name,
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts
new file mode 100644
index 0000000000000..daa90f892034e
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { CustomPackageDatasetConfiguration } from '../../install';
+
+// Dataset name must either match integration name exactly OR be prefixed with integration name and a dot.
+export const checkDatasetsNameFormat = (
+ datasets: CustomPackageDatasetConfiguration[],
+ integrationName: string
+) => {
+ const invalidNames = datasets
+ .filter((dataset) => {
+ const { name } = dataset;
+ return name !== integrationName && !name.startsWith(`${integrationName}.`);
+ })
+ .map((dataset) => dataset.name);
+
+ if (invalidNames.length > 0) {
+ throw new DatasetNamePrefixError(
+ `Dataset names '${invalidNames.join(
+ ', '
+ )}' must either match integration name '${integrationName}' exactly or be prefixed with integration name and a dot (e.g. '${integrationName}.').`
+ );
+ }
+};
+
+export class DatasetNamePrefixError extends Error {
+ constructor(message?: string) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ this.name = 'DatasetNamePrefixError';
+ }
+}
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
index 4bb77d03996c7..da31b26d275e9 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
@@ -103,6 +103,7 @@ import { createAssets } from './custom_integrations';
import { cacheAssets } from './custom_integrations/assets/cache';
import { generateDatastreamEntries } from './custom_integrations/assets/dataset/utils';
import { checkForNamingCollision } from './custom_integrations/validation/check_naming_collision';
+import { checkDatasetsNameFormat } from './custom_integrations/validation/check_dataset_name_format';
export async function isPackageInstalled(options: {
savedObjectsClient: SavedObjectsClientContract;
@@ -784,6 +785,7 @@ export async function installCustomPackage(
// Validate that we can create this package, validations will throw if they don't pass
await checkForNamingCollision(savedObjectsClient, pkgName);
+ checkDatasetsNameFormat(datasets, pkgName);
// Compose a packageInfo
const packageInfo = {
diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts
index 0843423f10b1b..19e84284b671b 100644
--- a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts
+++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts
@@ -623,7 +623,7 @@ describe('[Logs onboarding] Custom logs - install elastic agent', () => {
cy.getByTestSubj('obltOnboardingExploreLogs').should('exist').click();
cy.url().should('include', '/app/observability-log-explorer');
- cy.get('button').contains('[mylogs] mylogs').should('exist');
+ cy.get('button').contains('[Mylogs] mylogs').should('exist');
});
});
});
diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts
index f4a30e896c8db..e989089f491eb 100644
--- a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts
+++ b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts
@@ -107,9 +107,9 @@ Cypress.Commands.add('installCustomIntegration', (integrationName: string) => {
force: true,
integrationName,
datasets: [
- { name: 'access', type: 'logs' },
- { name: 'error', type: 'metrics' },
- { name: 'warning', type: 'logs' },
+ { name: `${integrationName}.access`, type: 'logs' },
+ { name: `${integrationName}.error`, type: 'metrics' },
+ { name: `${integrationName}.warning`, type: 'logs' },
],
},
headers: {
diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx
index b5edcf75b1214..3bfe683d48c83 100644
--- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx
+++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx
@@ -92,10 +92,14 @@ export function ConfigureLogs() {
resetOnCreation: false,
errorOnFailedCleanup: false,
},
- fields: {
- integrationName,
- datasets: [{ name: datasetName, type: 'logs' as const }],
- },
+ ...(integrationName !== undefined && datasetName !== undefined
+ ? {
+ fields: {
+ integrationName,
+ datasets: [{ name: datasetName, type: 'logs' as const }],
+ },
+ }
+ : {}),
previouslyCreatedIntegration: lastCreatedIntegrationOptions,
},
}}
diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx
index 81d7faf448a48..afe575c08018c 100644
--- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx
+++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx
@@ -17,9 +17,9 @@ import { InstallElasticAgent } from './install_elastic_agent';
import { SelectLogs } from './select_logs';
interface WizardState {
- integrationName: string;
+ integrationName?: string;
lastCreatedIntegrationOptions?: CustomIntegrationOptions;
- datasetName: string;
+ datasetName?: string;
serviceName: string;
logFilePaths: string[];
namespace: string;
@@ -40,8 +40,8 @@ interface WizardState {
}
const initialState: WizardState = {
- integrationName: '',
- datasetName: '',
+ integrationName: undefined,
+ datasetName: undefined,
serviceName: '',
logFilePaths: [''],
namespace: 'default',
diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts b/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts
index 88d71edb74d47..63dfd7690e887 100644
--- a/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts
+++ b/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts
@@ -39,9 +39,9 @@ export default function (providerContext: FtrProviderContext) {
force: true,
integrationName: INTEGRATION_NAME,
datasets: [
- { name: 'access', type: 'logs' },
- { name: 'error', type: 'metrics' },
- { name: 'warning', type: 'logs' },
+ { name: `${INTEGRATION_NAME}.access`, type: 'logs' },
+ { name: `${INTEGRATION_NAME}.error`, type: 'metrics' },
+ { name: `${INTEGRATION_NAME}.warning`, type: 'logs' },
],
})
.expect(200);
@@ -108,9 +108,9 @@ export default function (providerContext: FtrProviderContext) {
force: true,
integrationName: INTEGRATION_NAME,
datasets: [
- { name: 'access', type: 'logs' },
- { name: 'error', type: 'metrics' },
- { name: 'warning', type: 'logs' },
+ { name: `${INTEGRATION_NAME}.access`, type: 'logs' },
+ { name: `${INTEGRATION_NAME}.error`, type: 'metrics' },
+ { name: `${INTEGRATION_NAME}.warning`, type: 'logs' },
],
})
.expect(200);
@@ -123,9 +123,9 @@ export default function (providerContext: FtrProviderContext) {
force: true,
integrationName: INTEGRATION_NAME,
datasets: [
- { name: 'access', type: 'logs' },
- { name: 'error', type: 'metrics' },
- { name: 'warning', type: 'logs' },
+ { name: `${INTEGRATION_NAME}.access`, type: 'logs' },
+ { name: `${INTEGRATION_NAME}.error`, type: 'metrics' },
+ { name: `${INTEGRATION_NAME}.warning`, type: 'logs' },
],
})
.expect(409);
@@ -145,7 +145,7 @@ export default function (providerContext: FtrProviderContext) {
.send({
force: true,
integrationName: pkgName,
- datasets: [{ name: 'error', type: 'logs' }],
+ datasets: [{ name: `${INTEGRATION_NAME}.error`, type: 'logs' }],
})
.expect(409);
@@ -153,5 +153,48 @@ export default function (providerContext: FtrProviderContext) {
`Failed to create the integration as an integration with the name ${pkgName} already exists in the package registry or as a bundled package.`
);
});
+
+ it('Throws an error when dataset names are not prefixed correctly', async () => {
+ const response = await supertest
+ .post(`/api/fleet/epm/custom_integrations`)
+ .set('kbn-xsrf', 'xxxx')
+ .type('application/json')
+ .send({
+ force: true,
+ integrationName: INTEGRATION_NAME,
+ datasets: [{ name: 'error', type: 'logs' }],
+ })
+ .expect(422);
+
+ expect(response.body.message).to.be(
+ `Dataset names 'error' must either match integration name '${INTEGRATION_NAME}' exactly or be prefixed with integration name and a dot (e.g. '${INTEGRATION_NAME}.').`
+ );
+
+ await uninstallPackage();
+
+ await supertest
+ .post(`/api/fleet/epm/custom_integrations`)
+ .set('kbn-xsrf', 'xxxx')
+ .type('application/json')
+ .send({
+ force: true,
+ integrationName: INTEGRATION_NAME,
+ datasets: [{ name: INTEGRATION_NAME, type: 'logs' }],
+ })
+ .expect(200);
+
+ await uninstallPackage();
+
+ await supertest
+ .post(`/api/fleet/epm/custom_integrations`)
+ .set('kbn-xsrf', 'xxxx')
+ .type('application/json')
+ .send({
+ force: true,
+ integrationName: INTEGRATION_NAME,
+ datasets: [{ name: `${INTEGRATION_NAME}.error`, type: 'logs' }],
+ })
+ .expect(200);
+ });
});
}