From 6ee112da494e601af79ce9ce1d321a91ea2a1a7c Mon Sep 17 00:00:00 2001
From: William Easton
Date: Sun, 25 Jan 2026 10:05:47 -0600
Subject: [PATCH 01/13] Add EDOT Cloud Forwarder onboarding to Add Data section
- Add CloudForwarder page component with custom header
- Create CloudForwarderPanel with Launch Stack button for AWS
- Add route for /cloudforwarder in observability_onboarding_flow
- Add CloudForwarder custom card to use_custom_cards
- Include CloudForwarder in cloud category featured cards
- Card visible only on Cloud deployments (like Firehose)
The CloudForwarder enables users to deploy the Elastic Distribution
of OpenTelemetry Cloud Forwarder to forward AWS CloudWatch logs
to Elastic using OpenTelemetry.
Co-authored-by: bill.easton
---
.../observability_onboarding_flow.tsx | 6 +
.../onboarding_flow_form.tsx | 7 +-
.../onboarding_flow_form/use_custom_cards.tsx | 36 ++
.../application/pages/cloudforwarder.tsx | 38 ++
.../public/application/pages/index.ts | 1 +
.../quickstart_flows/cloudforwarder/index.tsx | 435 ++++++++++++++++++
.../cloudforwarder/use_cloudforwarder_flow.ts | 45 ++
.../server/routes/cloudforwarder/route.ts | 58 +++
.../server/routes/index.ts | 2 +
9 files changed, 627 insertions(+), 1 deletion(-)
create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/use_cloudforwarder_flow.ts
create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx
index f12bee7bdc245..4ba7c8f819a81 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx
@@ -18,6 +18,7 @@ import {
OtelKubernetesPage,
FirehosePage,
OtelApmPage,
+ CloudForwarderPage,
} from './pages';
import type { ObservabilityOnboardingAppServices } from '..';
import { useFlowBreadcrumb } from './shared/use_flow_breadcrumbs';
@@ -66,6 +67,11 @@ export function ObservabilityOnboardingFlow() {
)}
+ {(isCloud || isDev) && (
+
+
+
+ )}
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx
index e575d13a475c5..42339485038cd 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx
@@ -183,7 +183,12 @@ export const OnboardingFlowForm: FunctionComponent = () => {
host: ['auto-detect-logs', 'otel-logs'],
kubernetes: ['kubernetes-quick-start', 'otel-kubernetes'],
application: ['apm-virtual', 'otel-virtual', 'synthetics-virtual'],
- cloud: ['azure-logs-virtual', 'aws-logs-virtual', 'gcp-logs-virtual'],
+ cloud: [
+ 'azure-logs-virtual',
+ 'aws-logs-virtual',
+ 'gcp-logs-virtual',
+ 'cloudforwarder-quick-start',
+ ],
};
const customCards = useCustomCards(createCollectionCardHandler);
const featuredCardsForCategory: IntegrationCardItem[] = customCards.filter((card) => {
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx
index b7df85adb58cb..bff8f1cd21fcd 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx
@@ -51,6 +51,10 @@ export function useCustomCards(
history,
`/otel-apm/${location.search}`
);
+ const { href: cloudforwarderUrl } = reactRouterNavigate(
+ history,
+ `/cloudforwarder/${location.search}`
+ );
const apmUrl = `${getUrlForApp?.('apm')}/${isServerless ? 'onboarding' : 'tutorial'}`;
const otelApmUrl = isManagedOtlpServiceAvailable ? otelApmQuickstartUrl : apmUrl;
@@ -86,6 +90,33 @@ export function useCustomCards(
isQuickstart: true,
};
+ const cloudforwarderQuickstartCard: IntegrationCardItem = {
+ id: 'cloudforwarder-quick-start',
+ name: 'cloudforwarder-quick-start',
+ type: 'virtual',
+ title: i18n.translate('xpack.observability_onboarding.packageList.cloudforwarderTitle', {
+ defaultMessage: 'EDOT Cloud Forwarder',
+ }),
+ description: i18n.translate(
+ 'xpack.observability_onboarding.packageList.cloudforwarderDescription',
+ {
+ defaultMessage: 'Forward AWS CloudWatch logs to Elastic using OpenTelemetry.',
+ }
+ ),
+ categories: ['observability'],
+ icons: [
+ {
+ type: 'svg',
+ src: http?.staticAssets.getPluginAssetHref('opentelemetry.svg') ?? '',
+ },
+ ],
+ url: cloudforwarderUrl,
+ version: '',
+ integration: '',
+ isQuickstart: true,
+ release: 'preview',
+ };
+
return [
{
id: 'auto-detect-logs',
@@ -464,6 +495,11 @@ export function useCustomCards(
* which is not available for on-prem customers.
*/
...(isCloud ? [firehoseQuickstartCard] : []),
+ /**
+ * The EDOT Cloud Forwarder card should only be visible on Cloud
+ * as it requires Elastic Cloud infrastructure.
+ */
+ ...(isCloud ? [cloudforwarderQuickstartCard] : []),
];
}
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
new file mode 100644
index 0000000000000..56f0f00d3df2f
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import React from 'react';
+import { CloudForwarderPanel } from '../quickstart_flows/cloudforwarder';
+import { PageTemplate } from './template';
+import { CustomHeader } from '../header';
+
+export const CloudForwarderPage = () => (
+
+ }
+ >
+
+
+);
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/index.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/index.ts
index 0d01b8742c955..4f0c1e4dcdb52 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/index.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/index.ts
@@ -12,3 +12,4 @@ export { LandingPage } from './landing';
export { OtelLogsPage } from './otel_logs';
export { FirehosePage } from './firehose';
export { OtelApmPage } from './otel_apm';
+export { CloudForwarderPage } from './cloudforwarder';
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
new file mode 100644
index 0000000000000..80399d1036f37
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
@@ -0,0 +1,435 @@
+/*
+ * 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 React, { useEffect, useState } from 'react';
+import type { EuiBasicTableColumn } from '@elastic/eui';
+import {
+ EuiBasicTable,
+ EuiButton,
+ EuiButtonGroup,
+ EuiButtonIcon,
+ EuiCallOut,
+ EuiLink,
+ EuiPanel,
+ EuiSkeletonRectangle,
+ EuiSkeletonText,
+ EuiSpacer,
+ EuiSteps,
+ EuiText,
+ copyToClipboard,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { usePerformanceContext } from '@kbn/ebt-tools';
+import type { ValuesType } from 'utility-types';
+import { FETCH_STATUS } from '../../../hooks/use_fetcher';
+import { FeedbackButtons } from '../shared/feedback_buttons';
+import { useFlowBreadcrumb } from '../../shared/use_flow_breadcrumbs';
+import { useCloudForwarderFlow } from './use_cloudforwarder_flow';
+import { EmptyPrompt } from '../shared/empty_prompt';
+import { ManagedOtlpCallout } from '../shared/managed_otlp_callout';
+
+const EDOT_CLOUD_FORWARDER_DOCS_URL =
+ 'https://www.elastic.co/docs/reference/opentelemetry/edot-cloud-forwarder/aws';
+
+// CloudFormation template base URLs for different log types
+const CLOUDFORMATION_TEMPLATES = {
+ vpcflow: {
+ templateUrl:
+ 'https://edot-cloud-forwarder.s3.amazonaws.com/v1/latest/cloudformation/s3_logs-cloudformation.yaml',
+ stackName: 'edot-cloud-forwarder-vpcflow',
+ logType: 'vpcflow',
+ label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.vpcflow', {
+ defaultMessage: 'VPC Flow Logs',
+ }),
+ },
+ elbaccess: {
+ templateUrl:
+ 'https://edot-cloud-forwarder.s3.amazonaws.com/v1/latest/cloudformation/s3_logs-cloudformation.yaml',
+ stackName: 'edot-cloud-forwarder-elbaccess',
+ logType: 'elbaccess',
+ label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.elbaccess', {
+ defaultMessage: 'ELB Access Logs',
+ }),
+ },
+ cloudtrail: {
+ templateUrl:
+ 'https://edot-cloud-forwarder.s3.amazonaws.com/v1/latest/cloudformation/s3_logs-cloudformation.yaml',
+ stackName: 'edot-cloud-forwarder-cloudtrail',
+ logType: 'cloudtrail',
+ label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.cloudtrail', {
+ defaultMessage: 'CloudTrail Logs',
+ }),
+ },
+};
+
+type LogType = keyof typeof CLOUDFORMATION_TEMPLATES;
+
+function buildCloudFormationUrl(logType: LogType, otlpEndpoint: string): string {
+ const config = CLOUDFORMATION_TEMPLATES[logType];
+ const url = new URL('https://console.aws.amazon.com/cloudformation/home');
+ const params = new URLSearchParams({
+ templateURL: config.templateUrl,
+ stackName: config.stackName,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ param_EdotCloudForwarderS3LogsType: config.logType,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ param_OTLPEndpoint: otlpEndpoint,
+ });
+
+ url.hash = `/stacks/create/review?${params.toString()}`;
+ return url.toString();
+}
+
+export function CloudForwarderPanel() {
+ useFlowBreadcrumb({
+ text: i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.breadcrumbs.cloudforwarder',
+ {
+ defaultMessage: 'EDOT Cloud Forwarder',
+ }
+ ),
+ });
+
+ const [selectedLogType, setSelectedLogType] = useState('vpcflow');
+ const { data, status, error, refetch } = useCloudForwarderFlow();
+ const { onPageReady } = usePerformanceContext();
+
+ useEffect(() => {
+ if (data) {
+ onPageReady({
+ meta: {
+ description: `[ttfmp_onboarding] Request to create the onboarding flow succeeded and the flow's UI has rendered`,
+ },
+ });
+ }
+ }, [data, onPageReady]);
+
+ if (error !== undefined) {
+ return ;
+ }
+
+ const logTypeOptions = Object.entries(CLOUDFORMATION_TEMPLATES).map(([id, config]) => ({
+ id,
+ label: config.label,
+ }));
+
+ const steps = [
+ {
+ title: i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.prerequisitesTitle',
+ {
+ defaultMessage: 'Prerequisites',
+ }
+ ),
+ children: (
+ <>
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.documentationLinkLabel',
+ { defaultMessage: 'Check the documentation' }
+ )}
+
+ ),
+ }}
+ />
+
+
+ >
+ ),
+ },
+ {
+ title: i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.copyCredentialsTitle',
+ {
+ defaultMessage: 'Copy your OTLP credentials',
+ }
+ ),
+ children: (
+ <>
+ {status !== FETCH_STATUS.SUCCESS && (
+ <>
+
+
+
+
+ >
+ )}
+ {status === FETCH_STATUS.SUCCESS && data !== undefined && (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )}
+ >
+ ),
+ },
+ {
+ title: i18n.translate('xpack.observability_onboarding.cloudforwarderPanel.launchStackTitle', {
+ defaultMessage: 'Deploy the EDOT Cloud Forwarder in AWS',
+ }),
+ children: (
+ <>
+ {status !== FETCH_STATUS.SUCCESS && (
+ <>
+
+
+
+ >
+ )}
+ {status === FETCH_STATUS.SUCCESS && data !== undefined && (
+ <>
+
+
+
+
+
+
+ setSelectedLogType(id as LogType)}
+ buttonSize="m"
+ />
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.launchStackButtonLabel',
+ { defaultMessage: 'Launch Stack in AWS' }
+ )}
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.cloudForwarderPanel.code.apikeyYOURAPIKEYLabel',
+ { defaultMessage: 'ApiKey YOUR_API_KEY' }
+ )}
+
+ ),
+ }}
+ />
+
+
+ >
+ )}
+ >
+ ),
+ },
+ {
+ title: i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.visualizeDataTitle',
+ {
+ defaultMessage: 'Visualize your data',
+ }
+ ),
+ children: (
+ <>
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.cloudForwarderPanel.strong.logsawsLabel',
+ { defaultMessage: 'logs-aws.*' }
+ )}
+
+ ),
+ }}
+ />
+
+
+ >
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
+ );
+}
+
+function CredentialsTable({
+ managedOtlpServiceUrl,
+ apiKeyEncoded,
+}: {
+ managedOtlpServiceUrl: string;
+ apiKeyEncoded: string;
+}) {
+ const items = [
+ {
+ setting: i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.credentials.otlpEndpoint',
+ {
+ defaultMessage: 'OTLP Endpoint',
+ }
+ ),
+ value: managedOtlpServiceUrl,
+ description: i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.credentials.otlpEndpointDesc',
+ {
+ defaultMessage: 'Pre-populated in the CloudFormation URL',
+ }
+ ),
+ },
+ {
+ setting: i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.credentials.apiKey',
+ {
+ defaultMessage: 'API Key',
+ }
+ ),
+ value: apiKeyEncoded,
+ description: i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.credentials.apiKeyDesc',
+ {
+ defaultMessage: 'Copy and paste into AWS CloudFormation',
+ }
+ ),
+ },
+ ];
+
+ const columns: Array>> = [
+ {
+ field: 'setting',
+ width: '20%',
+ name: i18n.translate('xpack.observability_onboarding.cloudforwarderPanel.configSetting', {
+ defaultMessage: 'Setting',
+ }),
+ },
+ {
+ field: 'value',
+ width: '50%',
+ name: i18n.translate('xpack.observability_onboarding.cloudforwarderPanel.configValue', {
+ defaultMessage: 'Value',
+ }),
+ render: (_, { value }) => (
+ <>
+
+ {value}
+
+ {value && (
+ copyToClipboard(value)}
+ />
+ )}
+ >
+ ),
+ },
+ {
+ field: 'description',
+ width: '30%',
+ name: i18n.translate('xpack.observability_onboarding.cloudforwarderPanel.configDescription', {
+ defaultMessage: 'Note',
+ }),
+ render: (_, { description }) => (
+
+ {description}
+
+ ),
+ },
+ ];
+
+ return (
+
+ );
+}
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/use_cloudforwarder_flow.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/use_cloudforwarder_flow.ts
new file mode 100644
index 0000000000000..dcbf1c7a4ae31
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/use_cloudforwarder_flow.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 } from 'react';
+import { OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT } from '../../../../common/telemetry_events';
+import type { ObservabilityOnboardingAppServices } from '../../..';
+import { useFetcher } from '../../../hooks/use_fetcher';
+
+export function useCloudForwarderFlow() {
+ const {
+ services: {
+ analytics,
+ context: { cloudServiceProvider },
+ },
+ } = useKibana();
+ const { data, status, error, refetch } = useFetcher(
+ (callApi) => {
+ return callApi('POST /internal/observability_onboarding/cloudforwarder/flow');
+ },
+ [],
+ { showToastOnError: false }
+ );
+
+ useEffect(() => {
+ if (data?.onboardingId !== undefined) {
+ analytics?.reportEvent(OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT.eventType, {
+ onboardingFlowType: 'cloudforwarder',
+ onboardingId: data?.onboardingId,
+ step: 'in_progress',
+ context: {
+ cloudforwarder: {
+ cloudServiceProvider,
+ },
+ },
+ });
+ }
+ }, [analytics, cloudServiceProvider, data?.onboardingId]);
+
+ return { data, status, error, refetch } as const;
+}
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
new file mode 100644
index 0000000000000..6179549a9fe65
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { v4 as uuidv4 } from 'uuid';
+import Boom from '@hapi/boom';
+import { createManagedOtlpServiceApiKey } from '../../lib/api_key/create_managed_otlp_service_api_key';
+import { hasLogMonitoringPrivileges } from '../../lib/api_key/has_log_monitoring_privileges';
+import { createObservabilityOnboardingServerRoute } from '../create_observability_onboarding_server_route';
+import { getManagedOtlpServiceUrl } from '../../lib/get_managed_otlp_service_url';
+
+export interface CreateCloudForwarderOnboardingFlowRouteResponse {
+ onboardingId: string;
+ apiKeyEncoded: string;
+ managedOtlpServiceUrl: string;
+}
+
+const createCloudForwarderOnboardingFlowRoute = createObservabilityOnboardingServerRoute({
+ endpoint: 'POST /internal/observability_onboarding/cloudforwarder/flow',
+ security: {
+ authz: {
+ enabled: false,
+ reason: 'Authorization is checked by custom logic using Elasticsearch client',
+ },
+ },
+ async handler(resources): Promise {
+ const { context, plugins } = resources;
+ const {
+ elasticsearch: { client },
+ } = await context.core;
+
+ const hasPrivileges = await hasLogMonitoringPrivileges(client.asCurrentUser, true);
+
+ if (!hasPrivileges) {
+ throw Boom.forbidden(
+ "You don't have enough privileges to start a new onboarding flow. Contact your system administrator to grant you the required privileges."
+ );
+ }
+
+ const { encoded: apiKeyEncoded } = await createManagedOtlpServiceApiKey(
+ client.asCurrentUser,
+ 'ingest-cloudforwarder'
+ );
+
+ return {
+ onboardingId: uuidv4(),
+ apiKeyEncoded,
+ managedOtlpServiceUrl: getManagedOtlpServiceUrl(plugins),
+ };
+ },
+});
+
+export const cloudforwarderOnboardingRouteRepository = {
+ ...createCloudForwarderOnboardingFlowRoute,
+};
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/index.ts b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/index.ts
index acc2414d9d3c1..10adaccd826d1 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/index.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/index.ts
@@ -10,6 +10,7 @@ import { kubernetesOnboardingRouteRepository } from './kubernetes/route';
import { firehoseOnboardingRouteRepository } from './firehose/route';
import { otelHostOnboardingRouteRepository } from './otel_host/route';
import { otelApmOnboardingRouteRepository } from './otel_apm/route';
+import { cloudforwarderOnboardingRouteRepository } from './cloudforwarder/route';
function getTypedObservabilityOnboardingServerRouteRepository() {
const repository = {
@@ -18,6 +19,7 @@ function getTypedObservabilityOnboardingServerRouteRepository() {
...firehoseOnboardingRouteRepository,
...otelHostOnboardingRouteRepository,
...otelApmOnboardingRouteRepository,
+ ...cloudforwarderOnboardingRouteRepository,
};
return repository;
From 9843f6030a20b4c90f0f99c0cbdc5a237319e366 Mon Sep 17 00:00:00 2001
From: William Easton
Date: Sun, 25 Jan 2026 11:45:19 -0600
Subject: [PATCH 02/13] PR Cleanup
---
.../common/telemetry_events.ts | 20 ++++++++++
.../onboarding_flow_form.tsx | 1 -
.../onboarding_flow_form/use_custom_cards.tsx | 12 +++---
.../application/pages/cloudforwarder.tsx | 3 +-
.../quickstart_flows/cloudforwarder/index.tsx | 38 +++++++++++++------
5 files changed, 55 insertions(+), 19 deletions(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts b/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts
index c7f901ad6f015..d777d8f897c9a 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts
@@ -157,6 +157,10 @@ interface OnboardingAutoDetectEventContext {
title: string;
}
+interface OnboardingCloudForwarderEventContext {
+ cloudServiceProvider?: string;
+}
+
/**
* Additional flow-specific context that might
* be attached to telemetry events.
@@ -164,6 +168,7 @@ interface OnboardingAutoDetectEventContext {
export interface OnboardingFlowEventContext {
autoDetect?: OnboardingAutoDetectEventContext;
firehose?: OnboardingFirehoseFlowEventContext;
+ cloudforwarder?: OnboardingCloudForwarderEventContext;
}
const flowContextSchema: SchemaValue = {
@@ -210,6 +215,21 @@ const flowContextSchema: SchemaValue = {
optional: true,
},
},
+ cloudforwarder: {
+ properties: {
+ cloudServiceProvider: {
+ type: 'keyword',
+ _meta: {
+ description:
+ "The cloud service provider where the cloud forwarder is deployed. Can be 'aws', 'gcp' or 'azure'",
+ optional: true,
+ },
+ },
+ },
+ _meta: {
+ optional: true,
+ },
+ },
},
_meta: {
optional: true,
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx
index 42339485038cd..c481a5f1b50c9 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx
@@ -187,7 +187,6 @@ export const OnboardingFlowForm: FunctionComponent = () => {
'azure-logs-virtual',
'aws-logs-virtual',
'gcp-logs-virtual',
- 'cloudforwarder-quick-start',
],
};
const customCards = useCustomCards(createCollectionCardHandler);
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx
index bff8f1cd21fcd..5c4a9954f5050 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx
@@ -27,7 +27,7 @@ export function useCustomCards(
services: {
application,
http,
- context: { isServerless, isCloud },
+ context: { isServerless, isCloud, isDev },
share,
},
} = useKibana();
@@ -100,7 +100,8 @@ export function useCustomCards(
description: i18n.translate(
'xpack.observability_onboarding.packageList.cloudforwarderDescription',
{
- defaultMessage: 'Forward AWS CloudWatch logs to Elastic using OpenTelemetry.',
+ defaultMessage:
+ 'Forward logs from AWS S3 to Elastic using the EDOT Cloud Forwarder, running as a Lambda function.',
}
),
categories: ['observability'],
@@ -114,7 +115,6 @@ export function useCustomCards(
version: '',
integration: '',
isQuickstart: true,
- release: 'preview',
};
return [
@@ -493,13 +493,15 @@ export function useCustomCards(
* The new Firehose card should only be visible on Cloud
* as Firehose integration requires additional proxy,
* which is not available for on-prem customers.
+ * Also visible in dev mode for local development.
*/
- ...(isCloud ? [firehoseQuickstartCard] : []),
+ ...(isCloud || isDev ? [firehoseQuickstartCard] : []),
/**
* The EDOT Cloud Forwarder card should only be visible on Cloud
* as it requires Elastic Cloud infrastructure.
+ * Also visible in dev mode for local development.
*/
- ...(isCloud ? [cloudforwarderQuickstartCard] : []),
+ ...(isCloud || isDev ? [cloudforwarderQuickstartCard] : []),
];
}
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
index 56f0f00d3df2f..ac68c325e56c1 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
@@ -26,10 +26,9 @@ export const CloudForwarderPage = () => (
'xpack.observability_onboarding.experimentalOnboardingFlow.customHeader.cloudforwarder.caption.description',
{
defaultMessage:
- 'Deploy the Elastic Distribution of OpenTelemetry Cloud Forwarder to forward AWS CloudWatch logs and metrics to Elastic.',
+ 'Deploy the EDOT Cloud Forwarder as a Lambda function to forward logs from AWS S3 to Elastic.',
}
)}
- isTechnicalPreview={true}
/>
}
>
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
index 80399d1036f37..4bc8b5e59b917 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
@@ -25,7 +25,9 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { usePerformanceContext } from '@kbn/ebt-tools';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { ValuesType } from 'utility-types';
+import type { ObservabilityOnboardingAppServices } from '../../..';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { FeedbackButtons } from '../shared/feedback_buttons';
import { useFlowBreadcrumb } from '../../shared/use_flow_breadcrumbs';
@@ -95,6 +97,11 @@ export function CloudForwarderPanel() {
),
});
+ const {
+ services: {
+ context: { cloudServiceProvider },
+ },
+ } = useKibana();
const [selectedLogType, setSelectedLogType] = useState('vpcflow');
const { data, status, error, refetch } = useCloudForwarderFlow();
const { onPageReady } = usePerformanceContext();
@@ -110,7 +117,16 @@ export function CloudForwarderPanel() {
}, [data, onPageReady]);
if (error !== undefined) {
- return ;
+ return (
+
+ );
}
const logTypeOptions = Object.entries(CLOUDFORMATION_TEMPLATES).map(([id, config]) => ({
@@ -237,6 +253,7 @@ export function CloudForwarderPanel() {
- {i18n.translate(
- 'xpack.observability_onboarding.cloudForwarderPanel.code.apikeyYOURAPIKEYLabel',
- { defaultMessage: 'ApiKey YOUR_API_KEY' }
- )}
-
- ),
+ paramName: ElasticAPIKey,
}}
/>
@@ -313,7 +323,7 @@ export function CloudForwarderPanel() {
logsPrefix: (
{i18n.translate(
- 'xpack.observability_onboarding.cloudForwarderPanel.strong.logsawsLabel',
+ 'xpack.observability_onboarding.cloudforwarderPanel.strong.logsawsLabel',
{ defaultMessage: 'logs-aws.*' }
)}
@@ -429,6 +439,12 @@ function CredentialsTable({
);
From a96b47b0759b1602b08c3a701c31919c46a08283 Mon Sep 17 00:00:00 2001
From: William Easton
Date: Sun, 25 Jan 2026 11:51:57 -0600
Subject: [PATCH 03/13] Additional PR Cleanup
---
.../observability_onboarding/common/telemetry_events.ts | 9 +++++++++
.../stateful/pom/pages/onboarding_home.page.ts | 4 ++++
.../quickstart_flows/cloudforwarder/index.tsx | 2 +-
.../server/routes/cloudforwarder/route.ts | 2 +-
.../scout/ui/fixtures/page_objects/onboarding_app.ts | 4 ++++
5 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts b/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts
index d777d8f897c9a..c5cba970c36ce 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts
@@ -158,6 +158,7 @@ interface OnboardingAutoDetectEventContext {
}
interface OnboardingCloudForwarderEventContext {
+ selectedLogType?: string;
cloudServiceProvider?: string;
}
@@ -217,6 +218,14 @@ const flowContextSchema: SchemaValue = {
},
cloudforwarder: {
properties: {
+ selectedLogType: {
+ type: 'keyword',
+ _meta: {
+ description:
+ 'Which log type is selected in the UI (e.g. vpcflow, elbaccess, cloudtrail). Serves as a good indication of the type of logs the user chose to forward.',
+ optional: true,
+ },
+ },
cloudServiceProvider: {
type: 'keyword',
_meta: {
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/playwright/stateful/pom/pages/onboarding_home.page.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/playwright/stateful/pom/pages/onboarding_home.page.ts
index 6055b345a0d1e..a69faa1ff4f39 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/playwright/stateful/pom/pages/onboarding_home.page.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/playwright/stateful/pom/pages/onboarding_home.page.ts
@@ -20,6 +20,7 @@ export class OnboardingHomePage {
private readonly otelHostCard: Locator;
readonly awsCollectionCard: Locator;
readonly firehoseQuickstartCard: Locator;
+ readonly cloudforwarderQuickstartCard: Locator;
constructor(page: Page) {
this.page = page;
@@ -46,6 +47,9 @@ export class OnboardingHomePage {
this.otelHostCard = this.page.getByTestId('integration-card:otel-logs');
this.awsCollectionCard = this.page.getByTestId('integration-card:aws-logs-virtual');
this.firehoseQuickstartCard = this.page.getByTestId('integration-card:firehose-quick-start');
+ this.cloudforwarderQuickstartCard = this.page.getByTestId(
+ 'integration-card:cloudforwarder-quick-start'
+ );
}
public async selectHostUseCase() {
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
index 4bc8b5e59b917..ece25394efd28 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
@@ -122,7 +122,7 @@ export function CloudForwarderPanel() {
onboardingFlowType="cloudforwarder"
error={error}
telemetryEventContext={{
- cloudforwarder: { cloudServiceProvider },
+ cloudforwarder: { cloudServiceProvider, selectedLogType },
}}
onRetryClick={refetch}
/>
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
index 6179549a9fe65..04fa810eff796 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
@@ -23,7 +23,7 @@ const createCloudForwarderOnboardingFlowRoute = createObservabilityOnboardingSer
security: {
authz: {
enabled: false,
- reason: 'Authorization is checked by custom logic using Elasticsearch client',
+ reason: 'This route has custom authorization logic using Elasticsearch client',
},
},
async handler(resources): Promise {
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/test/scout/ui/fixtures/page_objects/onboarding_app.ts b/x-pack/solutions/observability/plugins/observability_onboarding/test/scout/ui/fixtures/page_objects/onboarding_app.ts
index 7de1047cc64f9..687f87e04030f 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/test/scout/ui/fixtures/page_objects/onboarding_app.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/test/scout/ui/fixtures/page_objects/onboarding_app.ts
@@ -75,6 +75,10 @@ export class OnboardingApp {
return this.page.getByTestId('integration-card:firehose-quick-start');
}
+ public get cloudforwarderQuickstartCard() {
+ return this.page.getByTestId('integration-card:cloudforwarder-quick-start');
+ }
+
public get useCaseGrid() {
return this.page.getByRole('group', { name: 'What do you want to monitor?' });
}
From 6292d4d2d63d57fc8b7d8e8f35763091fcc2a94e Mon Sep 17 00:00:00 2001
From: William Easton
Date: Sun, 25 Jan 2026 12:00:02 -0600
Subject: [PATCH 04/13] Improve comments
---
.../quickstart_flows/cloudforwarder/index.tsx | 11 ++++++++++-
.../server/routes/cloudforwarder/route.ts | 5 +++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
index ece25394efd28..d3add37c0d2e4 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
@@ -38,7 +38,11 @@ import { ManagedOtlpCallout } from '../shared/managed_otlp_callout';
const EDOT_CLOUD_FORWARDER_DOCS_URL =
'https://www.elastic.co/docs/reference/opentelemetry/edot-cloud-forwarder/aws';
-// CloudFormation template base URLs for different log types
+/**
+ * CloudFormation template configurations for different AWS log types.
+ * All log types use the same template URL but with different parameters
+ * (log type and OTLP endpoint) passed via URL hash parameters.
+ */
const CLOUDFORMATION_TEMPLATES = {
vpcflow: {
templateUrl:
@@ -71,6 +75,11 @@ const CLOUDFORMATION_TEMPLATES = {
type LogType = keyof typeof CLOUDFORMATION_TEMPLATES;
+/**
+ * Builds a CloudFormation console URL with pre-filled parameters for deploying
+ * the EDOT Cloud Forwarder. The URL includes the template URL, stack name, log type,
+ * and OTLP endpoint as hash parameters for the AWS CloudFormation console.
+ */
function buildCloudFormationUrl(logType: LogType, otlpEndpoint: string): string {
const config = CLOUDFORMATION_TEMPLATES[logType];
const url = new URL('https://console.aws.amazon.com/cloudformation/home');
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
index 04fa810eff796..876679b0a4927 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
@@ -32,6 +32,11 @@ const createCloudForwarderOnboardingFlowRoute = createObservabilityOnboardingSer
elasticsearch: { client },
} = await context.core;
+ /**
+ * Check for log monitoring privileges with APM support (second parameter = true)
+ * since CloudForwarder uses the managed OTLP service which may handle traces.
+ * This is consistent with other OTLP-based flows (otel_apm, kubernetes_otel).
+ */
const hasPrivileges = await hasLogMonitoringPrivileges(client.asCurrentUser, true);
if (!hasPrivileges) {
From 9ad536c4020194aaa3864fdfa75884338f0cccdd Mon Sep 17 00:00:00 2001
From: William Easton
Date: Sun, 25 Jan 2026 12:30:38 -0600
Subject: [PATCH 05/13] Update route permissions for cloud forwarder onboarding
---
.../server/routes/cloudforwarder/route.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
index 876679b0a4927..494b56051201a 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts
@@ -33,11 +33,11 @@ const createCloudForwarderOnboardingFlowRoute = createObservabilityOnboardingSer
} = await context.core;
/**
- * Check for log monitoring privileges with APM support (second parameter = true)
- * since CloudForwarder uses the managed OTLP service which may handle traces.
- * This is consistent with other OTLP-based flows (otel_apm, kubernetes_otel).
+ * Check for log monitoring privileges (logs and metrics only, no traces).
+ * CloudForwarder only forwards logs from AWS S3 (VPC Flow Logs, ELB Access Logs, CloudTrail).
+ * This is consistent with other log-only flows (firehose, otel_host).
*/
- const hasPrivileges = await hasLogMonitoringPrivileges(client.asCurrentUser, true);
+ const hasPrivileges = await hasLogMonitoringPrivileges(client.asCurrentUser);
if (!hasPrivileges) {
throw Boom.forbidden(
From ee9020b54b548fb5a798599c6c29eac2381e3608 Mon Sep 17 00:00:00 2001
From: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Date: Sun, 25 Jan 2026 19:02:34 +0000
Subject: [PATCH 06/13] Changes from node scripts/eslint_all_files --no-cache
--fix
---
.../onboarding_flow_form/onboarding_flow_form.tsx | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx
index c481a5f1b50c9..e575d13a475c5 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx
@@ -183,11 +183,7 @@ export const OnboardingFlowForm: FunctionComponent = () => {
host: ['auto-detect-logs', 'otel-logs'],
kubernetes: ['kubernetes-quick-start', 'otel-kubernetes'],
application: ['apm-virtual', 'otel-virtual', 'synthetics-virtual'],
- cloud: [
- 'azure-logs-virtual',
- 'aws-logs-virtual',
- 'gcp-logs-virtual',
- ],
+ cloud: ['azure-logs-virtual', 'aws-logs-virtual', 'gcp-logs-virtual'],
};
const customCards = useCustomCards(createCollectionCardHandler);
const featuredCardsForCategory: IntegrationCardItem[] = customCards.filter((card) => {
From 5507cbb3d25983e37f359274eaa8c8e5abb43b97 Mon Sep 17 00:00:00 2001
From: William Easton
Date: Sun, 25 Jan 2026 13:57:26 -0600
Subject: [PATCH 07/13] use unique ids for copy buttons
---
.../quickstart_flows/cloudforwarder/index.tsx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
index d3add37c0d2e4..d6ce77a2584c0 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
@@ -408,14 +408,14 @@ function CredentialsTable({
name: i18n.translate('xpack.observability_onboarding.cloudforwarderPanel.configValue', {
defaultMessage: 'Value',
}),
- render: (_, { value }) => (
+ render: (_, item) => (
<>
- {value}
+ {item.value}
- {value && (
+ {item.value && (
copyToClipboard(value)}
+ onClick={() => copyToClipboard(item.value)}
/>
)}
>
From 7c64c5338c49915045ecddc8b900a73198ae7285 Mon Sep 17 00:00:00 2001
From: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Date: Sun, 25 Jan 2026 20:27:42 +0000
Subject: [PATCH 08/13] Changes from node scripts/eslint_all_files --no-cache
--fix
---
.../application/quickstart_flows/cloudforwarder/index.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
index d6ce77a2584c0..ce21b2d76c6f9 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
@@ -415,7 +415,9 @@ function CredentialsTable({
{item.value && (
Date: Mon, 26 Jan 2026 11:47:09 -0600
Subject: [PATCH 09/13] Limit to just serverless
---
.../public/application/observability_onboarding_flow.tsx | 4 ++--
.../application/onboarding_flow_form/use_custom_cards.tsx | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx
index 4ba7c8f819a81..2d835963005b4 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx
@@ -30,7 +30,7 @@ export function ObservabilityOnboardingFlow() {
const { pathname } = useLocation();
const {
services: {
- context: { isDev, isCloud },
+ context: { isDev, isCloud, isServerless },
},
} = useKibana();
@@ -67,7 +67,7 @@ export function ObservabilityOnboardingFlow() {
)}
- {(isCloud || isDev) && (
+ {(isServerless || isDev) && (
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx
index 5c4a9954f5050..07667ddb6a743 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx
@@ -497,11 +497,11 @@ export function useCustomCards(
*/
...(isCloud || isDev ? [firehoseQuickstartCard] : []),
/**
- * The EDOT Cloud Forwarder card should only be visible on Cloud
+ * The EDOT Cloud Forwarder card should only be visible on Serverless
* as it requires Elastic Cloud infrastructure.
* Also visible in dev mode for local development.
*/
- ...(isCloud || isDev ? [cloudforwarderQuickstartCard] : []),
+ ...(isServerless || isDev ? [cloudforwarderQuickstartCard] : []),
];
}
From 615d1675387ce9be1f42d136ea1513fba2b16089 Mon Sep 17 00:00:00 2001
From: William Easton
Date: Mon, 26 Jan 2026 23:06:13 -0600
Subject: [PATCH 10/13] Simplify onboarding flow
---
.../application/pages/cloudforwarder.tsx | 34 +--
.../quickstart_flows/cloudforwarder/index.tsx | 266 ++++++++----------
2 files changed, 129 insertions(+), 171 deletions(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
index ac68c325e56c1..9b4924feefdfb 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
@@ -12,23 +12,23 @@ import { PageTemplate } from './template';
import { CustomHeader } from '../header';
export const CloudForwarderPage = () => (
-
}
>
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
index ce21b2d76c6f9..c5443bfca4d18 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
@@ -6,13 +6,12 @@
*/
import React, { useEffect, useState } from 'react';
-import type { EuiBasicTableColumn } from '@elastic/eui';
import {
- EuiBasicTable,
EuiButton,
EuiButtonGroup,
- EuiButtonIcon,
EuiCallOut,
+ EuiFieldText,
+ EuiFormRow,
EuiLink,
EuiPanel,
EuiSkeletonRectangle,
@@ -20,20 +19,17 @@ import {
EuiSpacer,
EuiSteps,
EuiText,
- copyToClipboard,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { usePerformanceContext } from '@kbn/ebt-tools';
import { useKibana } from '@kbn/kibana-react-plugin/public';
-import type { ValuesType } from 'utility-types';
import type { ObservabilityOnboardingAppServices } from '../../..';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { FeedbackButtons } from '../shared/feedback_buttons';
import { useFlowBreadcrumb } from '../../shared/use_flow_breadcrumbs';
import { useCloudForwarderFlow } from './use_cloudforwarder_flow';
import { EmptyPrompt } from '../shared/empty_prompt';
-import { ManagedOtlpCallout } from '../shared/managed_otlp_callout';
const EDOT_CLOUD_FORWARDER_DOCS_URL =
'https://www.elastic.co/docs/reference/opentelemetry/edot-cloud-forwarder/aws';
@@ -75,12 +71,38 @@ const CLOUDFORMATION_TEMPLATES = {
type LogType = keyof typeof CLOUDFORMATION_TEMPLATES;
+/**
+ * Validates S3 bucket names according to AWS naming rules:
+ * - 3-63 characters long
+ * - Only lowercase letters, numbers, hyphens, and periods
+ * - Must start and end with a letter or number
+ * - Cannot contain consecutive periods
+ */
+const S3_BUCKET_NAME_REGEX = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
+
+function isValidS3BucketName(bucketName: string): boolean {
+ return S3_BUCKET_NAME_REGEX.test(bucketName) && !bucketName.includes('..');
+}
+
+/**
+ * Builds an S3 bucket ARN from a bucket name.
+ * Format: arn:aws:s3:::bucket-name
+ */
+function buildS3BucketArn(bucketName: string): string {
+ return `arn:aws:s3:::${bucketName}`;
+}
+
/**
* Builds a CloudFormation console URL with pre-filled parameters for deploying
* the EDOT Cloud Forwarder. The URL includes the template URL, stack name, log type,
- * and OTLP endpoint as hash parameters for the AWS CloudFormation console.
+ * OTLP endpoint, API key, and S3 bucket ARN as hash parameters for the AWS CloudFormation console.
*/
-function buildCloudFormationUrl(logType: LogType, otlpEndpoint: string): string {
+function buildCloudFormationUrl(
+ logType: LogType,
+ otlpEndpoint: string,
+ apiKey: string,
+ s3BucketArn: string
+): string {
const config = CLOUDFORMATION_TEMPLATES[logType];
const url = new URL('https://console.aws.amazon.com/cloudformation/home');
const params = new URLSearchParams({
@@ -90,6 +112,10 @@ function buildCloudFormationUrl(logType: LogType, otlpEndpoint: string): string
param_EdotCloudForwarderS3LogsType: config.logType,
// eslint-disable-next-line @typescript-eslint/naming-convention
param_OTLPEndpoint: otlpEndpoint,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ param_ElasticAPIKey: apiKey,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ param_SourceS3BucketARN: s3BucketArn,
});
url.hash = `/stacks/create/review?${params.toString()}`;
@@ -112,7 +138,12 @@ export function CloudForwarderPanel() {
},
} = useKibana();
const [selectedLogType, setSelectedLogType] = useState('vpcflow');
+ const [s3BucketName, setS3BucketName] = useState('');
const { data, status, error, refetch } = useCloudForwarderFlow();
+
+ const trimmedBucketName = s3BucketName.trim();
+ const isBucketNameInvalid =
+ trimmedBucketName.length > 0 && !isValidS3BucketName(trimmedBucketName);
const { onPageReady } = usePerformanceContext();
useEffect(() => {
@@ -201,9 +232,9 @@ export function CloudForwarderPanel() {
},
{
title: i18n.translate(
- 'xpack.observability_onboarding.cloudforwarderPanel.copyCredentialsTitle',
+ 'xpack.observability_onboarding.cloudforwarderPanel.configureForwarderTitle',
{
- defaultMessage: 'Copy your OTLP credentials',
+ defaultMessage: 'Configure the Cloud Forwarder',
}
),
children: (
@@ -212,25 +243,79 @@ export function CloudForwarderPanel() {
<>
+
+
-
+
+
>
)}
{status === FETCH_STATUS.SUCCESS && data !== undefined && (
<>
-
+
+
+
+
+ setS3BucketName(e.target.value)}
+ isInvalid={isBucketNameInvalid}
+ placeholder={i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.s3BucketNamePlaceholder',
+ {
+ defaultMessage: 'my-logs-bucket',
+ }
+ )}
+ />
+
+
+
+
+
- setSelectedLogType(id as LogType)}
+ buttonSize="m"
/>
>
)}
@@ -245,7 +330,7 @@ export function CloudForwarderPanel() {
<>
{status !== FETCH_STATUS.SUCCESS && (
<>
-
+
>
@@ -255,51 +340,31 @@ export function CloudForwarderPanel() {
-
- setSelectedLogType(id as LogType)}
- buttonSize="m"
- />
{i18n.translate(
'xpack.observability_onboarding.cloudforwarderPanel.launchStackButtonLabel',
{ defaultMessage: 'Launch Stack in AWS' }
)}
-
-
-
- ElasticAPIKey,
- }}
- />
-
-
>
)}
>
@@ -353,110 +418,3 @@ export function CloudForwarderPanel() {
);
}
-
-function CredentialsTable({
- managedOtlpServiceUrl,
- apiKeyEncoded,
-}: {
- managedOtlpServiceUrl: string;
- apiKeyEncoded: string;
-}) {
- const items = [
- {
- setting: i18n.translate(
- 'xpack.observability_onboarding.cloudforwarderPanel.credentials.otlpEndpoint',
- {
- defaultMessage: 'OTLP Endpoint',
- }
- ),
- value: managedOtlpServiceUrl,
- description: i18n.translate(
- 'xpack.observability_onboarding.cloudforwarderPanel.credentials.otlpEndpointDesc',
- {
- defaultMessage: 'Pre-populated in the CloudFormation URL',
- }
- ),
- },
- {
- setting: i18n.translate(
- 'xpack.observability_onboarding.cloudforwarderPanel.credentials.apiKey',
- {
- defaultMessage: 'API Key',
- }
- ),
- value: apiKeyEncoded,
- description: i18n.translate(
- 'xpack.observability_onboarding.cloudforwarderPanel.credentials.apiKeyDesc',
- {
- defaultMessage: 'Copy and paste into AWS CloudFormation',
- }
- ),
- },
- ];
-
- const columns: Array>> = [
- {
- field: 'setting',
- width: '20%',
- name: i18n.translate('xpack.observability_onboarding.cloudforwarderPanel.configSetting', {
- defaultMessage: 'Setting',
- }),
- },
- {
- field: 'value',
- width: '50%',
- name: i18n.translate('xpack.observability_onboarding.cloudforwarderPanel.configValue', {
- defaultMessage: 'Value',
- }),
- render: (_, item) => (
- <>
-
- {item.value}
-
- {item.value && (
- copyToClipboard(item.value)}
- />
- )}
- >
- ),
- },
- {
- field: 'description',
- width: '30%',
- name: i18n.translate('xpack.observability_onboarding.cloudforwarderPanel.configDescription', {
- defaultMessage: 'Note',
- }),
- render: (_, { description }) => (
-
- {description}
-
- ),
- },
- ];
-
- return (
-
- );
-}
From 6ee6147c01b8eb60dee6f909a4d38fb4db20485a Mon Sep 17 00:00:00 2001
From: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 27 Jan 2026 05:34:24 +0000
Subject: [PATCH 11/13] Changes from node scripts/eslint_all_files --no-cache
--fix
---
.../application/pages/cloudforwarder.tsx | 34 +++++++++----------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
index 9b4924feefdfb..ac68c325e56c1 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx
@@ -12,23 +12,23 @@ import { PageTemplate } from './template';
import { CustomHeader } from '../header';
export const CloudForwarderPage = () => (
-
}
>
From 3c3b204638178c95d37e4acc7f3ae130ab4aba34 Mon Sep 17 00:00:00 2001
From: William Easton
Date: Wed, 28 Jan 2026 11:16:36 -0600
Subject: [PATCH 12/13] Updates for tests
---
.../common/aws_cloudforwarder.ts | 29 ++
.../common/telemetry_events.ts | 2 +-
.../quickstart_flows/cloudforwarder/index.tsx | 270 +++++++-----------
.../cloudforwarder/utils.test.ts | 65 +++++
.../quickstart_flows/cloudforwarder/utils.ts | 71 +++++
5 files changed, 273 insertions(+), 164 deletions(-)
create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/common/aws_cloudforwarder.ts
create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.test.ts
create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.ts
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/common/aws_cloudforwarder.ts b/x-pack/solutions/observability/plugins/observability_onboarding/common/aws_cloudforwarder.ts
new file mode 100644
index 0000000000000..a236b06f9e9a3
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/common/aws_cloudforwarder.ts
@@ -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.
+ */
+
+export const CLOUDFORWARDER_CLOUDFORMATION_TEMPLATE_URL =
+ 'https://edot-cloud-forwarder.s3.amazonaws.com/v1/latest/cloudformation/s3_logs-cloudformation.yaml';
+
+/**
+ * CloudFormation stack configurations for different AWS log types.
+ */
+export const CLOUDFORMATION_STACK_CONFIGS = {
+ vpcflow: {
+ stackName: 'edot-cloud-forwarder-vpcflow',
+ logType: 'vpcflow',
+ },
+ elbaccess: {
+ stackName: 'edot-cloud-forwarder-elbaccess',
+ logType: 'elbaccess',
+ },
+ cloudtrail: {
+ stackName: 'edot-cloud-forwarder-cloudtrail',
+ logType: 'cloudtrail',
+ },
+} as const;
+
+export type LogType = keyof typeof CLOUDFORMATION_STACK_CONFIGS;
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts b/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts
index c5cba970c36ce..40b4431d6e51d 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts
@@ -270,7 +270,7 @@ export const OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT: EventTypeOp
type: 'keyword',
_meta: {
description:
- 'The current step in the onboarding flow. Possible values: "in_progress", "awaiting_data", "data_received"',
+ 'The current step in the onboarding flow. Possible values: "in_progress", "awaiting_data", "data_received", "aws_launch_stack"',
},
},
context: flowContextSchema,
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
index c5443bfca4d18..00a954ac944c8 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
@@ -30,98 +30,13 @@ import { FeedbackButtons } from '../shared/feedback_buttons';
import { useFlowBreadcrumb } from '../../shared/use_flow_breadcrumbs';
import { useCloudForwarderFlow } from './use_cloudforwarder_flow';
import { EmptyPrompt } from '../shared/empty_prompt';
+import { OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT } from '../../../../common/telemetry_events';
+import { type LogType } from '../../../../common/aws_cloudforwarder';
+import { isValidS3BucketName, buildS3BucketArn, buildCloudFormationUrl } from './utils';
const EDOT_CLOUD_FORWARDER_DOCS_URL =
'https://www.elastic.co/docs/reference/opentelemetry/edot-cloud-forwarder/aws';
-/**
- * CloudFormation template configurations for different AWS log types.
- * All log types use the same template URL but with different parameters
- * (log type and OTLP endpoint) passed via URL hash parameters.
- */
-const CLOUDFORMATION_TEMPLATES = {
- vpcflow: {
- templateUrl:
- 'https://edot-cloud-forwarder.s3.amazonaws.com/v1/latest/cloudformation/s3_logs-cloudformation.yaml',
- stackName: 'edot-cloud-forwarder-vpcflow',
- logType: 'vpcflow',
- label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.vpcflow', {
- defaultMessage: 'VPC Flow Logs',
- }),
- },
- elbaccess: {
- templateUrl:
- 'https://edot-cloud-forwarder.s3.amazonaws.com/v1/latest/cloudformation/s3_logs-cloudformation.yaml',
- stackName: 'edot-cloud-forwarder-elbaccess',
- logType: 'elbaccess',
- label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.elbaccess', {
- defaultMessage: 'ELB Access Logs',
- }),
- },
- cloudtrail: {
- templateUrl:
- 'https://edot-cloud-forwarder.s3.amazonaws.com/v1/latest/cloudformation/s3_logs-cloudformation.yaml',
- stackName: 'edot-cloud-forwarder-cloudtrail',
- logType: 'cloudtrail',
- label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.cloudtrail', {
- defaultMessage: 'CloudTrail Logs',
- }),
- },
-};
-
-type LogType = keyof typeof CLOUDFORMATION_TEMPLATES;
-
-/**
- * Validates S3 bucket names according to AWS naming rules:
- * - 3-63 characters long
- * - Only lowercase letters, numbers, hyphens, and periods
- * - Must start and end with a letter or number
- * - Cannot contain consecutive periods
- */
-const S3_BUCKET_NAME_REGEX = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
-
-function isValidS3BucketName(bucketName: string): boolean {
- return S3_BUCKET_NAME_REGEX.test(bucketName) && !bucketName.includes('..');
-}
-
-/**
- * Builds an S3 bucket ARN from a bucket name.
- * Format: arn:aws:s3:::bucket-name
- */
-function buildS3BucketArn(bucketName: string): string {
- return `arn:aws:s3:::${bucketName}`;
-}
-
-/**
- * Builds a CloudFormation console URL with pre-filled parameters for deploying
- * the EDOT Cloud Forwarder. The URL includes the template URL, stack name, log type,
- * OTLP endpoint, API key, and S3 bucket ARN as hash parameters for the AWS CloudFormation console.
- */
-function buildCloudFormationUrl(
- logType: LogType,
- otlpEndpoint: string,
- apiKey: string,
- s3BucketArn: string
-): string {
- const config = CLOUDFORMATION_TEMPLATES[logType];
- const url = new URL('https://console.aws.amazon.com/cloudformation/home');
- const params = new URLSearchParams({
- templateURL: config.templateUrl,
- stackName: config.stackName,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- param_EdotCloudForwarderS3LogsType: config.logType,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- param_OTLPEndpoint: otlpEndpoint,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- param_ElasticAPIKey: apiKey,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- param_SourceS3BucketARN: s3BucketArn,
- });
-
- url.hash = `/stacks/create/review?${params.toString()}`;
- return url.toString();
-}
-
export function CloudForwarderPanel() {
useFlowBreadcrumb({
text: i18n.translate(
@@ -134,6 +49,7 @@ export function CloudForwarderPanel() {
const {
services: {
+ analytics,
context: { cloudServiceProvider },
},
} = useKibana();
@@ -162,17 +78,33 @@ export function CloudForwarderPanel() {
onboardingFlowType="cloudforwarder"
error={error}
telemetryEventContext={{
- cloudforwarder: { cloudServiceProvider, selectedLogType },
+ cloudforwarder: { cloudServiceProvider },
}}
onRetryClick={refetch}
/>
);
}
- const logTypeOptions = Object.entries(CLOUDFORMATION_TEMPLATES).map(([id, config]) => ({
- id,
- label: config.label,
- }));
+ const logTypeOptions: Array<{ id: LogType; label: string }> = [
+ {
+ id: 'vpcflow',
+ label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.vpcflow', {
+ defaultMessage: 'VPC Flow Logs',
+ }),
+ },
+ {
+ id: 'elbaccess',
+ label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.elbaccess', {
+ defaultMessage: 'ELB Access Logs',
+ }),
+ },
+ {
+ id: 'cloudtrail',
+ label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.cloudtrail', {
+ defaultMessage: 'CloudTrail Logs',
+ }),
+ },
+ ];
const steps = [
{
@@ -183,51 +115,49 @@ export function CloudForwarderPanel() {
}
),
children: (
- <>
-
-
+
+
+
+
+
-
-
+
+
- {i18n.translate(
- 'xpack.observability_onboarding.cloudforwarderPanel.documentationLinkLabel',
- { defaultMessage: 'Check the documentation' }
- )}
-
- ),
- }}
+ id="xpack.observability_onboarding.cloudforwarderPanel.prerequisiteS3"
+ defaultMessage="An S3 bucket containing your AWS logs (VPC Flow Logs, ELB Access Logs, or CloudTrail)"
/>
-
-
- >
+
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.documentationLinkLabel',
+ { defaultMessage: 'Check the documentation' }
+ )}
+
+ ),
+ }}
+ />
+
+
),
},
{
@@ -359,6 +289,22 @@ export function CloudForwarderPanel() {
iconType="popout"
fill
isDisabled={!isValidS3BucketName(trimmedBucketName)}
+ onClick={() => {
+ analytics?.reportEvent(
+ OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT.eventType,
+ {
+ onboardingFlowType: 'cloudforwarder',
+ onboardingId: data.onboardingId,
+ step: 'aws_launch_stack',
+ context: {
+ cloudforwarder: {
+ cloudServiceProvider,
+ selectedLogType,
+ },
+ },
+ }
+ );
+ }}
>
{i18n.translate(
'xpack.observability_onboarding.cloudforwarderPanel.launchStackButtonLabel',
@@ -378,35 +324,33 @@ export function CloudForwarderPanel() {
}
),
children: (
- <>
-
-
-
- {i18n.translate(
- 'xpack.observability_onboarding.cloudforwarderPanel.strong.logsawsLabel',
- { defaultMessage: 'logs-aws.*' }
- )}
-
- ),
- }}
- />
-
-
- >
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.cloudforwarderPanel.strong.logsawsLabel',
+ { defaultMessage: 'logs-aws.*' }
+ )}
+
+ ),
+ }}
+ />
+
+
),
},
];
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.test.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.test.ts
new file mode 100644
index 0000000000000..9337ee0701b9b
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.test.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 expect from 'expect';
+import { isValidS3BucketName, buildS3BucketArn, buildCloudFormationUrl } from './utils';
+
+describe('isValidS3BucketName()', () => {
+ it('accepts valid bucket names', () => {
+ expect(isValidS3BucketName('my-bucket')).toBe(true);
+ expect(isValidS3BucketName('my.bucket.123')).toBe(true);
+ expect(isValidS3BucketName('abc')).toBe(true); // min length
+ });
+
+ it('rejects invalid bucket names', () => {
+ expect(isValidS3BucketName('')).toBe(false); // empty
+ expect(isValidS3BucketName('ab')).toBe(false); // too short
+ expect(isValidS3BucketName('My-Bucket')).toBe(false); // uppercase
+ expect(isValidS3BucketName('my..bucket')).toBe(false); // consecutive periods
+ });
+
+ it('rejects AWS reserved patterns', () => {
+ // These are obscure AWS rules worth testing explicitly
+ expect(isValidS3BucketName('xn--my-bucket')).toBe(false); // S3 access point prefix
+ expect(isValidS3BucketName('my-bucket-s3alias')).toBe(false); // S3 alias suffix
+ });
+});
+
+describe('buildS3BucketArn()', () => {
+ it('builds correct ARN from bucket name', () => {
+ expect(buildS3BucketArn('my-bucket')).toBe('arn:aws:s3:::my-bucket');
+ });
+});
+
+describe('buildCloudFormationUrl()', () => {
+ it('builds correct CloudFormation URL with all parameters', () => {
+ const url = buildCloudFormationUrl(
+ 'vpcflow',
+ 'https://otlp.example.com',
+ 'api-key',
+ 'arn:aws:s3:::bucket'
+ );
+
+ expect(url).toContain('console.aws.amazon.com/cloudformation');
+ expect(url).toContain('stackName=edot-cloud-forwarder-vpcflow');
+ expect(url).toContain('param_EdotCloudForwarderS3LogsType=vpcflow');
+ expect(url).toContain('param_ElasticAPIKey=api-key');
+ expect(url).toContain('param_SourceS3BucketARN=');
+ });
+
+ it('properly encodes special characters in parameters', () => {
+ const url = buildCloudFormationUrl(
+ 'cloudtrail',
+ 'https://otlp.example.com/v1/logs',
+ 'key+with/special=chars',
+ 'arn:aws:s3:::bucket'
+ );
+
+ expect(url).toContain('param_ElasticAPIKey=key%2Bwith%2Fspecial%3Dchars');
+ expect(url).toContain('param_OTLPEndpoint=https%3A%2F%2Fotlp.example.com%2Fv1%2Flogs');
+ });
+});
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.ts
new file mode 100644
index 0000000000000..758c70e3eabb0
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 {
+ CLOUDFORWARDER_CLOUDFORMATION_TEMPLATE_URL,
+ CLOUDFORMATION_STACK_CONFIGS,
+ type LogType,
+} from '../../../../common/aws_cloudforwarder';
+
+export type { LogType };
+
+/**
+ * Validates S3 bucket names according to AWS naming rules:
+ * - 3-63 characters long
+ * - Only lowercase letters, numbers, hyphens, and periods
+ * - Must start and end with a letter or number
+ * - Cannot contain consecutive periods
+ * - Cannot start with 'xn--' (reserved for S3 access points)
+ * - Cannot end with '-s3alias' (reserved for S3 access point aliases)
+ *
+ * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
+ */
+const S3_BUCKET_NAME_REGEX = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
+
+export function isValidS3BucketName(bucketName: string): boolean {
+ return (
+ S3_BUCKET_NAME_REGEX.test(bucketName) &&
+ !bucketName.includes('..') &&
+ !bucketName.startsWith('xn--') &&
+ !bucketName.endsWith('-s3alias')
+ );
+}
+
+/**
+ * Builds an S3 bucket ARN from a bucket name.
+ * Format: arn:aws:s3:::bucket-name
+ */
+export function buildS3BucketArn(bucketName: string): string {
+ return `arn:aws:s3:::${bucketName}`;
+}
+
+/**
+ * Builds a CloudFormation console URL with pre-filled parameters for deploying
+ * the EDOT Cloud Forwarder. The URL includes the template URL, stack name, log type,
+ * OTLP endpoint, API key, and S3 bucket ARN as hash parameters for the AWS CloudFormation console.
+ */
+export function buildCloudFormationUrl(
+ logType: LogType,
+ otlpEndpoint: string,
+ apiKey: string,
+ s3BucketArn: string
+): string {
+ const config = CLOUDFORMATION_STACK_CONFIGS[logType];
+ const url = new URL('https://console.aws.amazon.com/cloudformation/home');
+ // The param_* names below must match the CloudFormation template parameter names exactly
+ const params = new URLSearchParams({
+ templateURL: CLOUDFORWARDER_CLOUDFORMATION_TEMPLATE_URL,
+ stackName: config.stackName,
+ param_EdotCloudForwarderS3LogsType: config.logType, // eslint-disable-line @typescript-eslint/naming-convention
+ param_OTLPEndpoint: otlpEndpoint, // eslint-disable-line @typescript-eslint/naming-convention
+ param_ElasticAPIKey: apiKey, // eslint-disable-line @typescript-eslint/naming-convention
+ param_SourceS3BucketARN: s3BucketArn, // eslint-disable-line @typescript-eslint/naming-convention
+ });
+
+ url.hash = `/stacks/create/review?${params.toString()}`;
+ return url.toString();
+}
From 45f277de4ff0509b21e372eaa4d332bf77e48fe9 Mon Sep 17 00:00:00 2001
From: William Easton
Date: Thu, 29 Jan 2026 14:31:20 -0600
Subject: [PATCH 13/13] Update language to reflect stack vs bucket region
---
.../application/quickstart_flows/cloudforwarder/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
index 00a954ac944c8..cd65f37d60612 100644
--- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
+++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx
@@ -271,7 +271,7 @@ export function CloudForwarderPanel() {