[Onboarding] AWS Firehose Flow (#187904)

Closes https://github.com/elastic/kibana/issues/187249
Closes https://github.com/elastic/kibana/issues/190792
Closes https://github.com/elastic/kibana/issues/190793

This change adds a new Firehose quickstart flow and replaces the old one
in the onboarding recommendations.

The new card is only visible on Cloud deployments, but you can still
access the flow locally via `app/observabilityOnboarding/firehose` path
if needed.

![CleanShot 2024-08-02 at 16 23
35@2x](https://github.com/user-attachments/assets/aaba2aec-e17e-410d-87b5-a183586c2ac0)

### How to test

You going to need an AWS account. You can use just a personal one or
"Elastic Observability" account which you can access through Okta (type
"AWS" in Okta's search and you should see "AWS - Elastic
Observability").

In case you decide to use the shared "Elastic Observability" account,
make sure it does not already has
`Elastic-CloudwatchLogsAndMetricsToFirehose` CloudFormation stack left
from the previous tester. Feel free to delete it if it's there.

You also need to [install and setup AWS
CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
In case you use "Elastic Observability" account here is [the instruction
to get CLI
credentials.](https://github.com/elastic/observability-dev/blob/main/docs/how-we-work/aws-onboarding.md#programmatic-access-for-developers).

1. In AWS account, create a few entities that generate logs and put them
into a CloudWatch log group (see instructions below for a few services).
2. Generate some logs by accessing the entities that you've created and
make sure they appear in CloudWatch (mind that there is a ~1 minute
delay). **If you don't see anything in CloudWatch, there is no point in
proceeding further, make sure to fix your AWS setup before starting the
flow in Kibana.**
3. Go to the [serverless Kibana instance deployed from this
PR](https://issue-serverless-skhgw-pr187904-e0f30a.kb.eu-west-1.aws.qa.elastic.cloud)
4. Add Data → Collect and analyze logs → View AWS Collection → Firehose
quickstart
5. Open the Firehose flow and copy the command snippet
6. Run the command in your terminal. It should succeed and output the
new Stack ID.
7. Wait for the stack to finish creating. The quickstart flow screen in
Kibana has a command to check current status or you can monitor it
directly in AWS.
8. Generate some some logs by accessing the AWS services you've created.
9. Go back to the Kibana screen, after a minute or so incoming logs
should be detected and corresponding AWS service will be highlighted.
10. Expand one of the detected services and navigate the the suggested
dashboard, make sure you see some data there.


### Example AWS Services Configs

**Before creating any resources, make sure you're in the same region
(top right corner in AWS Console) you've used while configuring AWS
CLI.**

#### API Gateway

1. [Create an IAM
role](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html#set-up-access-logging-permissions)
to grant API Gateway permissions to write into a CloudWatch log groups
1. Copy the ARN of the created role
1. Open "CloudWatch" in AWS and select "Log groups" in the sidebar
1. Create a new log group with default parameters
1. Copy the ARN of the new group
1. Open **API Gateway** in AWS
1. Navigate to "Settings" in the sidebar
1. In the "Logging" section click "Edit" and paste the ARN of the IAM
role you created in step 1. Hit "Save changes"
1. Now go back to "APIs" in the sidebar and click "Create API"
1. In "REST API" click "Build"
1. Select "Example API" and click "Create API"
1. Click on "Deploy API"
1. For "Stage" dropdown select "New stage", give it any name and click
"Deploy"
1. You will now see "Invoke URL", you can use it later to access this
API and generate logs
1. Scroll to "Logs and tracing" section and click "Edit"
1. In the dropdown select "Full request and response logs"
1. Toggle "Custom access logging"
1. Paste the ARN of the CloudWatch log group you've created in step 4.
But make sure to not include ":*" symbols at the end.
1. In the log format input paste [this format from our
docs](https://www.elastic.co/docs/current/integrations/aws/apigateway#data-streams)
and click "Save"
```
{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","caller":"$context.identity.caller","user":"$context.identity.user","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength","apiId":"$context.apiId","domainName":"$context.domainName","stage":"$context.stage"}
```
1. Now when you access this API, you should see logs coming into the
CloudWatch group.

#### WAF

**This sets up WAF for an API Gateway, see above if you don't have one
already.**

1. Open WAF in AWS
3. Click "Web ACLs" in the sidebar
4. Click "Create web ACL"
5. Select the region where you've created your API Gateway and give ACL
any name
6. In the "Associated AWS resources" section click "Add AWS resources"
7. Select you API Gateway and click "Add"
8. Click "Next"
9. Create some basic rule, for example to block requests that have a
specific parameter in the URL
10. Click through the other configuration step leaving everything as is
and then finally click "Create web ACL"
11. Select the created ACL and click on the "Logging and metrics" tab
12. Click "Edit" in "Logging" section 
13. Click "Create new" in the "Amazon CloudWatch Logs log group" section
14. Create a new log group. **The log group name should start with
`aws-waf-logs-`**.
15. Select the new group in the dropdown and click "Save"
16. Now you should have logs generated and saved into the log group when
you access your API gateway

#### VPC

1. [Create an IAM
role](https://docs.aws.amazon.com/vpc/latest/tgw/flow-logs-cwl.html#flow-logs-iam)
to write flow logs to CloudWatch log groups.
3. Create and EC2 instance and configure there some HTTP server like
Nginx. Using Docker would probably be the fastest way.
4. Create a CloudWatch log group with default parameters
5. Open "VPC" in AWS and select the VPC where you've created the EC2
instance.
6. Click the "Flow logs" tab and click "Create flow logs"
7. In "Maximum aggregation interval" select 1 minute to see logs faster
8. In "Destination log group" select the log group you've created in
step 3
9. In "IAM role" select the role you've created in step 1
10. Click "Create flow log"
11. Now when you access your EC2 instance, you should see logs in the
CloudWatch log group
This commit is contained in:
Mykola Harmash 2024-09-06 11:11:11 +02:00 committed by GitHub
parent 24e1b67027
commit 8a429b5953
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1184 additions and 26 deletions

View file

@ -48,6 +48,7 @@ export type LogDocument = Fields &
'orchestrator.resource.id'?: string;
'kubernetes.pod.uid'?: string;
'aws.s3.bucket.name'?: string;
'aws.kinesis.name'?: string;
'orchestrator.namespace'?: string;
'container.name'?: string;
'cloud.provider'?: string;

View file

@ -0,0 +1,49 @@
/*
* 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 FIREHOSE_CLOUDFORMATION_STACK_NAME = 'Elastic-CloudwatchLogsAndMetricsToFirehose';
export const FIREHOSE_LOGS_STREAM_NAME = 'Elastic-CloudwatchLogs';
export const FIREHOSE_METRICS_STREAM_NAME = 'Elastic-CloudwatchMetrics';
export const FIREHOSE_CLOUDFORMATION_TEMPLATE_URL =
'https://elastic-cloudformation-templates.s3.amazonaws.com/v0.1.0/firehose_default_start.yml';
export const AWS_INDEX_NAME_LIST = [
// Logs
'logs-aws.vpcflow',
'logs-aws.apigateway_logs',
'logs-aws.cloudtrail',
'logs-aws.firewall_logs',
'logs-aws.route53_public_logs',
'logs-aws.route53_resolver_logs',
'logs-aws.waf',
'logs-awsfirehose',
// Metrics
'metrics-aws.apigateway_metrics',
'metrics-aws.dynamodb',
'metrics-aws.ebs',
'metrics-aws.ec2_metrics',
'metrics-aws.ecs_metrics',
'metrics-aws.elb_metrics',
'metrics-aws.emr_metrics',
'metrics-aws.firewall_metrics',
'metrics-aws.kafka_metrics',
'metrics-aws.kinesis',
'metrics-aws.lambda',
'metrics-aws.natgateway',
'metrics-aws.rds',
'metrics-aws.s3_storage_lens',
'metrics-aws.s3_daily_storage',
'metrics-aws.s3_request',
'metrics-aws.sns',
'metrics-aws.sqs',
'metrics-aws.transitgateway',
'metrics-aws.usage',
'metrics-aws.vpn',
] as const;
export type AWSIndexName = (typeof AWS_INDEX_NAME_LIST)[number];

View file

@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React, { useEffect } from 'react';
import { Route, Routes } from '@kbn/shared-ux-router';
import { useLocation } from 'react-router-dom-v5-compat';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
AutoDetectPage,
CustomLogsPage,
@ -16,12 +17,19 @@ import {
LandingPage,
OtelLogsPage,
SystemLogsPage,
FirehosePage,
} from './pages';
import { ObservabilityOnboardingAppServices } from '..';
const queryClient = new QueryClient();
export function ObservabilityOnboardingFlow() {
const { pathname } = useLocation();
const {
services: {
context: { isDev, isCloud },
},
} = useKibana<ObservabilityOnboardingAppServices>();
useEffect(() => {
window.scrollTo(0, 0);
@ -45,6 +53,11 @@ export function ObservabilityOnboardingFlow() {
<Route path="/otel-logs">
<OtelLogsPage />
</Route>
{(isCloud || isDev) && (
<Route path="/firehose">
<FirehosePage />
</Route>
)}
<Route>
<LandingPage />
</Route>

View file

@ -24,12 +24,14 @@ import {
} from '@elastic/eui';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { OnboardingFlowPackageList } from '../packages_list';
import { useCustomMargin } from '../shared/use_custom_margin';
import { Category } from './types';
import { useCustomCardsForCategory } from './use_custom_cards_for_category';
import { useVirtualSearchResults } from './use_virtual_search_results';
import { LogoIcon, SupportedLogo } from '../shared/logo_icon';
import { ObservabilityOnboardingAppServices } from '../..';
interface UseCaseOption {
id: Category;
@ -90,6 +92,11 @@ export const OnboardingFlowForm: FunctionComponent = () => {
},
];
const {
services: {
context: { isCloud },
},
} = useKibana<ObservabilityOnboardingAppServices>();
const customMargin = useCustomMargin();
const radioGroupId = useGeneratedHtmlId({ prefix: 'onboardingCategory' });
const categorySelectorTitleId = useGeneratedHtmlId();
@ -134,6 +141,12 @@ export const OnboardingFlowForm: FunctionComponent = () => {
searchParams.get('category') as Category | null
);
const virtualSearchResults = useVirtualSearchResults();
/**
* Cloud deployments have the new Firehose quick start
* flow enabled, so the ond card 'epr:awsfirehose' should
* not show up in the search results.
*/
const searchExcludePackageIdList = isCloud ? ['epr:awsfirehose'] : [];
let isSelectingCategoryWithKeyboard: boolean = false;
@ -264,6 +277,7 @@ export const OnboardingFlowForm: FunctionComponent = () => {
(card) => card.type === 'virtual' && !card.isCollectionCard
)
.concat(virtualSearchResults)}
excludePackageIdList={searchExcludePackageIdList}
joinCardLists
/>
</div>

View file

@ -6,14 +6,48 @@
*/
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useHistory } from 'react-router-dom';
import { reactRouterNavigate, useKibana } from '@kbn/kibana-react-plugin/public';
import { CustomCard } from '../packages_list/types';
import { ObservabilityOnboardingAppServices } from '../..';
export function useVirtualSearchResults(): CustomCard[] {
const {
services: { application },
} = useKibana();
services: {
application,
context: { isCloud },
},
} = useKibana<ObservabilityOnboardingAppServices>();
const history = useHistory();
const { href: firehoseUrl } = reactRouterNavigate(history, `/firehose/${location.search}`);
const getUrlForApp = application?.getUrlForApp;
const firehoseQuickstartCard: CustomCard = {
id: 'firehose-quick-start',
type: 'virtual',
title: i18n.translate('xpack.observability_onboarding.packageList.uploadFileTitle', {
defaultMessage: 'AWS Firehose',
}),
release: 'preview',
description: i18n.translate(
'xpack.observability_onboarding.packageList.uploadFileDescription',
{
defaultMessage: 'Collect logs and metrics from Amazon Web Services (AWS).',
}
),
name: 'firehose-quick-start',
categories: [],
icons: [
{
type: 'svg',
src: 'https://epr.elastic.co/package/awsfirehose/1.1.0/img/logo_firehose.svg',
},
],
url: firehoseUrl,
version: '',
integration: '',
isCollectionCard: false,
};
return [
{
@ -42,5 +76,11 @@ export function useVirtualSearchResults(): CustomCard[] {
integration: '',
isCollectionCard: false,
},
/**
* The new Firehose card should only be visible on Cloud
* as Firehose integration requires additional proxy,
* which is not available for on-prem customers.
*/
...(isCloud ? [firehoseQuickstartCard] : []),
];
}

View file

@ -38,6 +38,7 @@ interface Props {
* When enabled, custom and integration cards are joined into a single list.
*/
joinCardLists?: boolean;
excludePackageIdList?: string[];
onLoaded?: () => void;
}
@ -58,6 +59,7 @@ const PackageListGridWrapper = ({
flowCategory,
flowSearch,
joinCardLists = false,
excludePackageIdList = [],
onLoaded,
}: WrapperProps) => {
const customMargin = useCustomMargin();
@ -68,6 +70,7 @@ const PackageListGridWrapper = ({
const list: IntegrationCardItem[] = useIntegrationCardList(
filteredCards,
selectedCategory,
excludePackageIdList,
customCards,
flowCategory,
flowSearch,

View file

@ -80,10 +80,12 @@ function useFilteredCards(
rewriteUrl: (card: IntegrationCardItem) => IntegrationCardItem,
integrationsList: IntegrationCardItem[],
selectedCategory: string[],
excludePackageIdList: string[],
customCards?: CustomCard[]
) {
return useMemo(() => {
const integrationCards = integrationsList
.filter((card) => !excludePackageIdList.includes(card.id))
.filter((card) => card.categories.some((category) => selectedCategory.includes(category)))
.map(rewriteUrl)
.map(toCustomCard);
@ -99,7 +101,7 @@ function useFilteredCards(
),
integrationCards,
};
}, [integrationsList, customCards, selectedCategory, rewriteUrl]);
}, [integrationsList, rewriteUrl, customCards, excludePackageIdList, selectedCategory]);
}
/**
@ -113,6 +115,7 @@ function useFilteredCards(
export function useIntegrationCardList(
integrationsList: IntegrationCardItem[],
selectedCategory: string[],
excludePackageIdList: string[],
customCards?: CustomCard[],
flowCategory?: string | null,
flowSearch?: string,
@ -123,6 +126,7 @@ export function useIntegrationCardList(
rewriteUrl,
integrationsList,
selectedCategory,
excludePackageIdList,
customCards
);

View file

@ -7,7 +7,11 @@
import { IntegrationCardItem } from '@kbn/fleet-plugin/public';
export const QUICKSTART_FLOWS = ['auto-detect-logs-virtual', 'kubernetes-quick-start'];
export const QUICKSTART_FLOWS = [
'auto-detect-logs-virtual',
'kubernetes-quick-start',
'firehose-quick-start',
];
export const toCustomCard = (card: IntegrationCardItem) => ({
...card,

View file

@ -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 { i18n } from '@kbn/i18n';
import React from 'react';
import { FirehosePanel } from '../quickstart_flows/firehose';
import { PageTemplate } from './template';
import { CustomHeader } from '../header';
export const FirehosePage = () => (
<PageTemplate
customHeader={
<CustomHeader
logo="firehose"
headlineCopy={i18n.translate(
'xpack.observability_onboarding.experimentalOnboardingFlow.customHeader.firehose.text',
{
defaultMessage: 'Setting up Amazon Data Firehose',
}
)}
captionCopy={i18n.translate(
'xpack.observability_onboarding.experimentalOnboardingFlow.customHeader.firehose.caption.description',
{
defaultMessage:
'This installation is tailored for setting up Firehose in your Observability project with minimal configuration.',
}
)}
/>
}
>
<FirehosePanel />
</PageTemplate>
);

View file

@ -11,3 +11,4 @@ export { KubernetesPage } from './kubernetes';
export { LandingPage } from './landing';
export { OtelLogsPage } from './otel_logs';
export { SystemLogsPage } from './system_logs';
export { FirehosePage } from './firehose';

View file

@ -24,6 +24,8 @@ import {
type SingleDatasetLocatorParams,
SINGLE_DATASET_LOCATOR_ID,
} from '@kbn/deeplinks-observability/locators';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { getAutoDetectCommand } from './get_auto_detect_command';
import { DASHBOARDS, useOnboardingFlow } from './use_onboarding_flow';
import { ProgressIndicator } from '../shared/progress_indicator';
@ -34,12 +36,16 @@ import { LocatorButtonEmpty } from '../shared/locator_button_empty';
import { GetStartedPanel } from '../shared/get_started_panel';
import { isSupportedLogo, LogoIcon } from '../../shared/logo_icon';
import { FeedbackButtons } from '../shared/feedback_buttons';
import { ObservabilityOnboardingContextValue } from '../../../plugin';
import { useAutoDetectTelemetry } from './use_auto_detect_telemetry';
export const AutoDetectPanel: FunctionComponent = () => {
const { status, data, error, refetch, installedIntegrations } = useOnboardingFlow();
const command = data ? getAutoDetectCommand(data) : undefined;
const accordionId = useGeneratedHtmlId({ prefix: 'accordion' });
const {
services: { share },
} = useKibana<ObservabilityOnboardingContextValue>();
useAutoDetectTelemetry(
status,
@ -61,6 +67,7 @@ export const AutoDetectPanel: FunctionComponent = () => {
const customIntegrations = installedIntegrations.filter(
(integration) => integration.installSource === 'custom'
);
const dashboardLocator = share.url.locators.get(DASHBOARD_APP_LOCATOR);
return (
<EuiPanel hasBorder paddingSize="xl">
@ -173,10 +180,14 @@ export const AutoDetectPanel: FunctionComponent = () => {
integration={integration.pkgName}
newTab
isLoading={status !== 'dataReceived'}
dashboardLinks={integration.kibanaAssets
actionLinks={integration.kibanaAssets
.filter((asset) => asset.type === 'dashboard')
.map((asset) => {
const dashboard = DASHBOARDS[asset.id as keyof typeof DASHBOARDS];
const href =
dashboardLocator?.getRedirectUrl({
dashboardId: asset.id,
}) ?? '';
return {
id: asset.id,
@ -210,6 +221,7 @@ export const AutoDetectPanel: FunctionComponent = () => {
defaultMessage: 'Explore logs data',
}
),
href,
};
})}
/>

View file

@ -0,0 +1,105 @@
/*
* 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 from 'react';
import {
EuiAccordion,
EuiCodeBlock,
EuiLink,
EuiSpacer,
EuiText,
useGeneratedHtmlId,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
FIREHOSE_CLOUDFORMATION_STACK_NAME,
FIREHOSE_LOGS_STREAM_NAME,
FIREHOSE_METRICS_STREAM_NAME,
} from '../../../../common/aws_firehose';
import { CopyToClipboardButton } from '../shared/copy_to_clipboard_button';
import { buildCreateStackCommand, buildStackStatusCommand } from './utils';
interface Props {
encodedApiKey: string;
onboardingId: string;
elasticsearchUrl: string;
templateUrl: string;
isCopyPrimaryAction: boolean;
}
export function CreateStackCommandSnippet({
encodedApiKey,
elasticsearchUrl,
templateUrl,
isCopyPrimaryAction,
}: Props) {
const stackStatusAccordionId = useGeneratedHtmlId({ prefix: 'stackStatusAccordion' });
const createStackCommand = buildCreateStackCommand({
templateUrl,
stackName: FIREHOSE_CLOUDFORMATION_STACK_NAME,
logsStreamName: FIREHOSE_LOGS_STREAM_NAME,
metricsStreamName: FIREHOSE_METRICS_STREAM_NAME,
encodedApiKey,
elasticsearchUrl,
});
const stackStatusCommand = buildStackStatusCommand({
stackName: FIREHOSE_CLOUDFORMATION_STACK_NAME,
});
return (
<>
<EuiText>
<p>
<FormattedMessage
id="xpack.observability_onboarding.firehosePanel.createFirehoseStreamDescription"
defaultMessage="Run the command bellow in your terminal where you have {awsCLIInstallGuideLink} configured. The command will create a CloudFormation stack that includes a Firehose delivery, backup S3 bucket, CloudWatch subscription filter and metrics stream along with required IAM roles."
values={{
awsCLIInstallGuideLink: (
<EuiLink
data-test-subj="observabilityOnboardingFirehosePanelAwsInstallGuideLink"
href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
external
target="_blank"
>
{i18n.translate(
'xpack.observability_onboarding.firehosePanel.awsCLIInstallGuideLinkLabel',
{ defaultMessage: 'AWS CLI' }
)}
</EuiLink>
),
}}
/>
</p>
</EuiText>
<EuiSpacer />
<EuiCodeBlock
language="text"
paddingSize="m"
fontSize="m"
data-test-subj="observabilityOnboardingFirehoseCreateStackCommand"
>
{createStackCommand}
</EuiCodeBlock>
<EuiSpacer />
<CopyToClipboardButton textToCopy={createStackCommand} fill={isCopyPrimaryAction} />
<EuiSpacer />
<EuiAccordion id={stackStatusAccordionId} buttonContent="Check stack status">
<EuiSpacer size="xs" />
<EuiCodeBlock language="text" paddingSize="m" fontSize="m" isCopyable>
{stackStatusCommand}
</EuiCodeBlock>
</EuiAccordion>
</>
);
}

View file

@ -0,0 +1,78 @@
/*
* 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, { useState } from 'react';
import {
EuiPanel,
EuiSkeletonRectangle,
EuiSkeletonText,
EuiSpacer,
EuiSteps,
EuiStepStatus,
} from '@elastic/eui';
import useEvent from 'react-use/lib/useEvent';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { EmptyPrompt } from '../shared/empty_prompt';
import { CreateStackCommandSnippet } from './create_stack_command_snippet';
import { VisualizeData } from './visualize_data';
export function FirehosePanel() {
const [windowLostFocus, setWindowLostFocus] = useState(false);
const { data, status, error, refetch } = useFetcher(
(callApi) => {
return callApi('POST /internal/observability_onboarding/firehose/flow');
},
[],
{ showToastOnError: false }
);
useEvent('blur', () => setWindowLostFocus(true), window);
if (error !== undefined) {
return <EmptyPrompt error={error} onRetryClick={refetch} />;
}
const isVisualizeStepActive =
status === FETCH_STATUS.SUCCESS && data !== undefined && windowLostFocus;
const steps = [
{
title: 'Create a Firehose delivery stream and ingest CloudWatch logs',
children: (
<>
{status !== FETCH_STATUS.SUCCESS && (
<>
<EuiSkeletonText lines={5} />
<EuiSpacer />
<EuiSkeletonRectangle width="170px" height="40px" />
</>
)}
{status === FETCH_STATUS.SUCCESS && data !== undefined && (
<CreateStackCommandSnippet
templateUrl={data.templateUrl}
encodedApiKey={data.apiKeyEncoded}
onboardingId={data.onboardingId}
elasticsearchUrl={data.elasticsearchUrl}
isCopyPrimaryAction={!isVisualizeStepActive}
/>
)}
</>
),
},
{
title: 'Visualize your data',
status: (isVisualizeStepActive ? 'current' : 'incomplete') as EuiStepStatus,
children: isVisualizeStepActive && <VisualizeData />,
},
];
return (
<EuiPanel hasBorder paddingSize="xl">
<EuiSteps steps={steps} />
</EuiPanel>
);
}

View file

@ -0,0 +1,368 @@
/*
* 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 { useCallback, useMemo } from 'react';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { SINGLE_DATASET_LOCATOR_ID } from '@kbn/deeplinks-observability';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common';
import { AWSIndexName } from '../../../../common/aws_firehose';
import { ObservabilityOnboardingContextValue } from '../../../plugin';
interface AWSServiceGetStartedConfig {
id: string;
indexNameList: AWSIndexName[];
title: string;
logoURL: string;
previewImage?: string;
actionLinks: Array<{
id: string;
title: string;
label: string;
href: string;
}>;
}
export function useAWSServiceGetStartedList(): AWSServiceGetStartedConfig[] {
const {
services: { share },
} = useKibana<ObservabilityOnboardingContextValue>();
const dashboardLocator = share.url.locators.get(DASHBOARD_APP_LOCATOR);
const singleDatasetLocator = share.url.locators.get(SINGLE_DATASET_LOCATOR_ID);
const discoverLocator = share.url.locators.get(DISCOVER_APP_LOCATOR);
const generateMetricsDashboardActionLink = useCallback(
(dashboardId: string, name?: string) => ({
id: `dashboard-${dashboardId}`,
title: i18n.translate(
'xpack.observability_onboarding.firehosePanel.exploreMetricsDataTitle',
{
defaultMessage: 'Overview{name} metrics data with this pre-made dashboard',
values: { name: name ? ` ${name}` : '' },
}
),
label: i18n.translate(
'xpack.observability_onboarding.firehosePanel.exploreMetricsDataLabel',
{
defaultMessage: 'Explore metrics data',
}
),
href:
dashboardLocator?.getRedirectUrl({
dashboardId,
}) ?? '',
}),
[dashboardLocator]
);
const generateLogsDashboardActionLink = useCallback(
(dashboardId: string) => ({
id: `dashboard-${dashboardId}`,
title: i18n.translate('xpack.observability_onboarding.firehosePanel.exploreLogsDataTitle', {
defaultMessage: 'Overview your logs data with this pre-made dashboard',
}),
label: i18n.translate('xpack.observability_onboarding.firehosePanel.exploreLogsDataLabel', {
defaultMessage: 'Explore logs data',
}),
href:
dashboardLocator?.getRedirectUrl({
dashboardId,
}) ?? '',
}),
[dashboardLocator]
);
const generateLogsExplorerActionLink = useCallback(
(dataset: string, name: string) => ({
id: `logs-explorer-${dataset}`,
title: i18n.translate('xpack.observability_onboarding.firehosePanel.exploreDataTitle', {
defaultMessage: 'See {name} data in Logs Explorer',
values: { name },
}),
label: i18n.translate('xpack.observability_onboarding.firehosePanel.exploreDataLabel', {
defaultMessage: 'Explore',
}),
href:
singleDatasetLocator?.getRedirectUrl({
integration: 'AWS',
dataset,
}) ?? '',
}),
[singleDatasetLocator]
);
const generateMetricsDiscoverActionLink = useCallback(
(namespace: string, name: string) => ({
id: `logs-explorer-${namespace}`,
title: i18n.translate('xpack.observability_onboarding.firehosePanel.exploreDataTitle', {
defaultMessage: 'See {name} metrics data in Discover',
values: { name },
}),
label: i18n.translate('xpack.observability_onboarding.firehosePanel.exploreDataLabel', {
defaultMessage: 'Explore',
}),
href:
discoverLocator?.getRedirectUrl({
dataViewId: `metrics-*`,
query: {
query: `aws.cloudwatch.namespace: ${namespace}`,
language: 'kuery',
},
}) ?? '',
}),
[discoverLocator]
);
return useMemo(
() => [
{
id: 'vpc-flow',
indexNameList: ['logs-aws.vpcflow'],
title: 'VPC',
logoURL: 'https://epr.elastic.co/package/aws/2.21.0/img/logo_vpcflow.svg',
actionLinks: [generateLogsDashboardActionLink('aws-15503340-4488-11ea-ad63-791a5dc86f10')],
},
{
id: 'api-gateway',
indexNameList: ['logs-aws.apigateway_logs', 'metrics-aws.apigateway_metrics'],
title: 'API Gateway',
logoURL: 'https://epr.elastic.co/package/aws/2.21.0/img/logo_apigateway.svg',
actionLinks: [
generateLogsDashboardActionLink('aws-5465f0f0-26e4-11ee-9051-011d57d86fe2'),
generateMetricsDashboardActionLink('aws-bff88770-56d6-11ee-893f-c96e4c6c871e'),
],
},
{
id: 'cloudtrail',
indexNameList: ['logs-aws.cloudtrail'],
title: 'CloudTrail',
logoURL: 'https://epr.elastic.co/package/aws/2.21.0/img/logo_cloudtrail.svg',
actionLinks: [generateLogsDashboardActionLink('aws-9c09cd20-7399-11ea-a345-f985c61fe654')],
},
{
id: 'firewall',
indexNameList: ['logs-aws.firewall_logs'],
title: 'Network Firewall',
logoURL: 'https://epr.elastic.co/package/aws/2.21.0/img/logo_firewall.svg',
actionLinks: [
generateLogsDashboardActionLink('aws-2ba11b50-4b9d-11ec-8282-5342b8988acc'),
generateMetricsDashboardActionLink('aws-3abffe60-4ba9-11ec-8282-5342b8988acc'),
],
},
{
id: 'route53',
indexNameList: ['logs-aws.route53_public_logs', 'logs-aws.route53_resolver_logs'],
title: 'Route53',
logoURL: 'https://epr.elastic.co/package/aws/2.21.0/img/logo_route53.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateLogsExplorerActionLink('route53_public_logs', 'Route53 public'),
generateLogsExplorerActionLink('route53_resolver_logs', 'Route53 resolver'),
],
},
{
id: 'waf',
indexNameList: ['logs-aws.waf'],
title: 'WAF',
logoURL: 'https://epr.elastic.co/package/aws/2.21.0/img/logo_waf.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [generateLogsExplorerActionLink('waf', 'WAF')],
},
{
id: 'dynamodb',
indexNameList: ['metrics-aws.dynamodb'],
title: 'DynamoDB',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_dynamodb.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-68ba7bd0-20b6-11ea-8f72-2f8d21e50b0c'),
],
},
{
id: 'ebs',
indexNameList: ['metrics-aws.ebs'],
title: 'EBS',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_ebs.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-44ce4680-b7ba-11e9-8349-f15f850c5cd0'),
],
},
{
id: 'ec2',
indexNameList: ['metrics-aws.ec2_metrics'],
title: 'EC2',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_ec2.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-c5846400-f7fb-11e8-af03-c999c9dea608'),
],
},
{
id: 'ecs',
indexNameList: ['metrics-aws.ecs_metrics'],
title: 'ECS',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_ecs.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [generateMetricsDiscoverActionLink('AWS/ECS', 'ECS')],
},
{
id: 'elb',
indexNameList: ['metrics-aws.elb_metrics'],
title: 'ELB',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_elb.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-e74bf320-b3ce-11e9-87a4-078dbbae220d'),
],
},
{
id: 'emr',
indexNameList: ['metrics-aws.emr_metrics'],
title: 'EMR',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_emr.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-98f85120-0ea4-11ee-9c37-e55025c0278a'),
],
},
{
id: 'msk',
indexNameList: ['metrics-aws.kafka_metrics'],
title: 'MSK',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_msk.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-62d43b00-d10d-11ee-b93f-db5ae1f208de'),
],
},
{
id: 'kinesis',
indexNameList: ['metrics-aws.kinesis'],
title: 'Kinesis Data Stream',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_kinesis.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-07d67a60-d872-11eb-8220-c9141cc1b15c'),
],
},
{
id: 'lambda',
indexNameList: ['metrics-aws.lambda'],
title: 'Lambda',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_lambda.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-7ac8e1d0-28d2-11ea-ba6c-49a884eb104f'),
],
},
{
id: 'nat-gateway',
indexNameList: ['metrics-aws.natgateway'],
title: 'NAT Gateway',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_natgateway.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-c2b1cbc0-6891-11ea-b0ac-95d4ecb1fecd'),
],
},
{
id: 'rds',
indexNameList: ['metrics-aws.rds'],
title: 'RDS',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_rds.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-3367c170-921f-11e9-aa19-159bf182e06f'),
],
},
{
id: 's3',
indexNameList: [
'metrics-aws.s3_storage_lens',
'metrics-aws.s3_daily_storage',
'metrics-aws.s3_request',
],
title: 'S3',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_s3.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink(
'aws-80ed1380-41a6-11ec-a605-bff67d9b7872',
'S3 Storage Lens'
),
generateMetricsDiscoverActionLink('AWS/S3', 'S3'),
],
},
{
id: 'sns',
indexNameList: ['metrics-aws.sns'],
title: 'SNS',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_sns.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-d17b1000-17a4-11ea-8e91-03c7047cbb9d'),
],
},
{
id: 'sqs',
indexNameList: ['metrics-aws.sqs'],
title: 'SQS',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_sqs.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-234aeda0-43b7-11e9-8697-530f39afc6eb'),
],
},
{
id: 'transitgateway',
indexNameList: ['metrics-aws.transitgateway'],
title: 'Transit Gateway',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_transitgateway.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-0eb5a6a0-694f-11ea-b0ac-95d4ecb1fecd'),
],
},
{
id: 'usage',
indexNameList: ['metrics-aws.usage'],
title: 'AWS Usage',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_aws.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-917a07b0-178e-11ea-8650-fb606deb5be4'),
],
},
{
id: 'vpn',
indexNameList: ['metrics-aws.vpn'],
title: 'VPN',
logoURL: 'https://epr.elastic.co/package/aws/2.23.0/img/logo_vpn.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [
generateMetricsDashboardActionLink('aws-67c9f900-693e-11ea-b0ac-95d4ecb1fecd'),
],
},
{
id: 'firehose',
indexNameList: ['logs-awsfirehose'],
title: 'Uncategorized Firehose Logs',
logoURL: 'https://epr.elastic.co/package/awsfirehose/1.1.0/img/logo_firehose.svg',
previewImage: 'waterfall_screen.svg',
actionLinks: [generateLogsExplorerActionLink('awsfirehose', 'Firehose')],
},
],
[
generateLogsDashboardActionLink,
generateLogsExplorerActionLink,
generateMetricsDashboardActionLink,
generateMetricsDiscoverActionLink,
]
);
}

View file

@ -0,0 +1,50 @@
/*
* 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 function buildCreateStackCommand({
templateUrl,
stackName,
logsStreamName,
metricsStreamName,
encodedApiKey,
elasticsearchUrl,
}: {
templateUrl: string;
stackName: string;
logsStreamName: string;
metricsStreamName: string;
encodedApiKey: string;
elasticsearchUrl: string;
}) {
const escapedElasticsearchUrl = elasticsearchUrl.replace(/\//g, '\\/');
const escapedTemplateUrl = templateUrl.replace(/\//g, '\\/');
return `
aws cloudformation create-stack
--stack-name ${stackName}
--template-url ${escapedTemplateUrl}
--parameters ParameterKey=FirehoseStreamNameForLogs,ParameterValue=${logsStreamName}
ParameterKey=FirehoseStreamNameForMetrics,ParameterValue=${metricsStreamName}
ParameterKey=ElasticEndpointURL,ParameterValue=${escapedElasticsearchUrl}
ParameterKey=ElasticAPIKey,ParameterValue=${encodedApiKey}
--capabilities CAPABILITY_IAM
`
.trim()
.replace(/\n/g, ' ')
.replace(/\s\s+/g, ' ');
}
export function buildStackStatusCommand({ stackName }: { stackName: string }) {
return `
aws cloudformation describe-stacks
--stack-name ${stackName}
--query "Stacks[0].StackStatus"
`
.trim()
.replace(/\n/g, ' ')
.replace(/\s\s+/g, ' ');
}

View file

@ -0,0 +1,114 @@
/*
* 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 { EuiIcon, EuiSpacer, useEuiTheme, useGeneratedHtmlId } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import useInterval from 'react-use/lib/useInterval';
import {
FIREHOSE_CLOUDFORMATION_STACK_NAME,
FIREHOSE_LOGS_STREAM_NAME,
} from '../../../../common/aws_firehose';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { AccordionWithIcon } from '../shared/accordion_with_icon';
import { GetStartedPanel } from '../shared/get_started_panel';
import { ProgressIndicator } from '../shared/progress_indicator';
import { useAWSServiceGetStartedList } from './use_aws_service_get_started_list';
const FETCH_INTERVAL = 2000;
const REQUEST_PENDING_STATUS_LIST = [FETCH_STATUS.LOADING, FETCH_STATUS.NOT_INITIATED];
export function VisualizeData() {
const accordionId = useGeneratedHtmlId({ prefix: 'accordion' });
const { euiTheme } = useEuiTheme();
const {
data: populatedAWSLogsIndexList,
status,
refetch,
} = useFetcher((callApi) => {
return callApi('GET /internal/observability_onboarding/firehose/has-data', {
params: {
query: {
logsStreamName: FIREHOSE_LOGS_STREAM_NAME,
stackName: FIREHOSE_CLOUDFORMATION_STACK_NAME,
},
},
});
}, []);
const awsServiceGetStartedConfigList = useAWSServiceGetStartedList();
useInterval(() => {
if (REQUEST_PENDING_STATUS_LIST.includes(status)) {
return;
}
refetch();
}, FETCH_INTERVAL);
if (populatedAWSLogsIndexList === undefined) {
return null;
}
return (
<>
<ProgressIndicator
title={i18n.translate('xpack.observability_onboarding.firehosePanel.waitingForDataTitle', {
defaultMessage: 'Waiting for data from the Firehose stream',
})}
isLoading={true}
css={css`
max-width: 40%;
`}
/>
<EuiSpacer size="xl" />
<div data-test-subj="observabilityOnboardingAWSServiceList">
{awsServiceGetStartedConfigList.map(
({ id, indexNameList, actionLinks, title, logoURL, previewImage }) => {
const isEnabled = indexNameList.some((indexName) =>
populatedAWSLogsIndexList.includes(indexName)
);
return (
<AccordionWithIcon
data-test-subj={`observabilityOnboardingAWSService-${id}`}
key={id}
id={`${accordionId}_${id}`}
icon={<EuiIcon type={logoURL} size="l" />}
title={i18n.translate(
'xpack.observability_onboarding.firehosePanel.awsServiceDataFoundTitle',
{
defaultMessage: '{title}',
values: { title },
}
)}
extraAction={
isEnabled ? <EuiIcon type="checkInCircleFilled" color="success" /> : null
}
isDisabled={!isEnabled}
css={{
paddingRight: euiTheme.size.s,
filter: `grayscale(${isEnabled ? 0 : 1})`,
}}
>
<GetStartedPanel
integration="aws"
newTab
isLoading={false}
actionLinks={actionLinks}
previewImage={previewImage}
/>
</AccordionWithIcon>
);
}
)}
</div>
</>
);
}

View file

@ -10,6 +10,8 @@ import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import {
StepsProgress,
useFlowProgressTelemetry,
@ -17,6 +19,7 @@ import {
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { ProgressIndicator } from '../shared/progress_indicator';
import { GetStartedPanel } from '../shared/get_started_panel';
import { ObservabilityOnboardingContextValue } from '../../../plugin';
interface Props {
onboardingId: string;
@ -29,6 +32,10 @@ const CLUSTER_OVERVIEW_DASHBOARD_ID = 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8
export function DataIngestStatus({ onboardingId }: Props) {
const [progress, setProgress] = useState<StepsProgress | undefined>(undefined);
const [checkDataStartTime] = useState(Date.now());
const {
services: { share },
} = useKibana<ObservabilityOnboardingContextValue>();
const dashboardLocator = share.url.locators.get(DASHBOARD_APP_LOCATOR);
const { data, status, refetch } = useFetcher(
(callApi) => {
@ -112,7 +119,7 @@ export function DataIngestStatus({ onboardingId }: Props) {
integration="kubernetes"
newTab={false}
isLoading={false}
dashboardLinks={[
actionLinks={[
{
id: CLUSTER_OVERVIEW_DASHBOARD_ID,
label: i18n.translate(
@ -127,6 +134,10 @@ export function DataIngestStatus({ onboardingId }: Props) {
defaultMessage: 'Overview your Kubernetes cluster with this pre-made dashboard',
}
),
href:
dashboardLocator?.getRedirectUrl({
dashboardId: CLUSTER_OVERVIEW_DASHBOARD_ID,
}) ?? '',
},
]}
/>

View file

@ -24,9 +24,13 @@ import { FeedbackButtons } from '../shared/feedback_buttons';
export const KubernetesPanel: React.FC = () => {
const [windowLostFocus, setWindowLostFocus] = useState(false);
const { data, status, error, refetch } = useFetcher((callApi) => {
return callApi('POST /internal/observability_onboarding/kubernetes/flow');
}, []);
const { data, status, error, refetch } = useFetcher(
(callApi) => {
return callApi('POST /internal/observability_onboarding/kubernetes/flow');
},
[],
{ showToastOnError: false }
);
useEvent('blur', () => setWindowLostFocus(true), window);

View file

@ -17,31 +17,31 @@ import {
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { FormattedMessage } from '@kbn/i18n-react';
import { ObservabilityOnboardingContextValue } from '../../../plugin';
export function GetStartedPanel({
integration,
dashboardLinks,
actionLinks,
previewImage = 'charts_screen.svg',
newTab,
isLoading,
}: {
integration: string;
newTab: boolean;
dashboardLinks: Array<{
actionLinks: Array<{
id: string;
label: string;
title: string;
label: string;
href: string;
}>;
previewImage?: string;
isLoading: boolean;
}) {
const {
services: { http, share },
services: { http },
} = useKibana<ObservabilityOnboardingContextValue>();
const dashboardLocator = share.url.locators.get(DASHBOARD_APP_LOCATOR);
return (
<>
<EuiFlexGroup alignItems="center" responsive={false}>
@ -50,7 +50,7 @@ export function GetStartedPanel({
<EuiSkeletonRectangle width={162} height={117} />
) : (
<EuiImage
src={http.staticAssets.getPluginAssetHref('charts_screen.svg')}
src={http.staticAssets.getPluginAssetHref(previewImage)}
width={162}
height={117}
alt=""
@ -61,17 +61,15 @@ export function GetStartedPanel({
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="s">
{dashboardLinks.map(({ id, label, title }) => (
{actionLinks.map(({ id, title, label, href }) => (
<EuiFlexItem key={id}>
<EuiFlexGroup direction="column" gutterSize="xs" alignItems="flexStart">
<EuiText key={id} size="s">
<p>{title}</p>
</EuiText>
<EuiLink
data-test-subj={`observabilityOnboardingDataIngestStatusViewDashboardLink-${id}`}
href={dashboardLocator?.getRedirectUrl({
dashboardId: id,
})}
data-test-subj={`observabilityOnboardingDataIngestStatusActionLink-${id}`}
href={href}
target={newTab ? '_blank' : '_self'}
>
{label}

View file

@ -23,7 +23,8 @@ export type SupportedLogo =
| 'nginx'
| 'apache'
| 'system'
| 'opentelemetry';
| 'opentelemetry'
| 'firehose';
export function isSupportedLogo(logo: string): logo is SupportedLogo {
return [

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon-Architecture/16/Arch_Amazon-Kinesis-Data-Firehose_16</title>
<g id="Icon-Architecture/16/Arch_Amazon-Kinesis-Data-Firehose_16" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icon-Architecture-BG/16/Analytics" fill="#8C4FFF">
<rect id="Rectangle" x="0" y="0" width="24" height="24"></rect>
</g>
<g id="Icon-Service/16/Amazon-Kinesis-Data-Firehose" transform="translate(5.000000, 6.000000)" fill="#FFFFFF">
<path d="M14.5836386,7.241 L13.9526386,8.017 C13.1876386,7.396 12.0286386,6.994 11.0006386,6.994 L11.0006386,6.056 L11.0006386,5.994 L11.0006386,5.056 C12.1386386,5.056 13.2796386,4.507 13.9696386,3.992 L14.5666386,4.794 C14.2016386,5.067 12.9546386,5.9 11.3996386,6.03 C12.5466386,6.117 13.7346386,6.552 14.5836386,7.241 L14.5836386,7.241 Z M9.00063863,6.62 L7.00063863,7.12 L7.00063863,4.902 L9.00063863,5.402 L9.00063863,6.62 Z M6.00063863,8 L5.00063863,8 L5.00063863,4 L6.00063863,4 L6.00063863,4.261 L6.00063863,7.761 L6.00063863,8 Z M9.62163863,4.526 L7.00063863,3.871 L7.00063863,3.5 C7.00063863,3.223 6.77663863,3 6.50063863,3 L4.50063863,3 C4.22463863,3 4.00063863,3.223 4.00063863,3.5 L4.00063863,4.409 C1.46663863,4.086 1.00063863,3.16 1.00063863,1.42 L1.00063863,1.77635684e-15 L0.000638629816,1.77635684e-15 L0.000638629816,1.42 C0.000638629816,3.918 1.14663863,5.056 4.00063863,5.403 L4.00063863,5.5 L0.000638629816,5.5 L0.000638629816,6.5 L4.00063863,6.5 L4.00063863,6.604 C1.21763863,6.983 0.00163862982,8.274 0.00163862982,10.882 L0.00163862982,10.99 C-0.000361370184,11.279 -0.00136137018,11.995 0.00363862982,12.048 L1.00063863,11.97 C0.99663863,11.874 0.99963863,11.252 1.00163863,10.994 L1.00163863,10.882 C1.00163863,9.148 1.54263863,7.991 4.00063863,7.615 L4.00063863,8.5 C4.00063863,8.776 4.22463863,9 4.50063863,9 L6.50063863,9 C6.77663863,9 7.00063863,8.776 7.00063863,8.5 L7.00063863,8.151 L9.62163863,7.496 C9.84463863,7.44 10.0006386,7.241 10.0006386,7.011 L10.0006386,5.011 C10.0006386,4.782 9.84463863,4.582 9.62163863,4.526 L9.62163863,4.526 Z" id="Amazon-Kinesis-Data-Firehose_Icon_16_Squid"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -30,6 +30,8 @@ export interface ConfigSchema {
}
export interface AppContext {
isDev: boolean;
isCloud: boolean;
isServerless: boolean;
stackVersion: string;
}

View file

@ -81,7 +81,8 @@ export class ObservabilityOnboardingPlugin
const {
ui: { enabled: isObservabilityOnboardingUiEnabled },
} = config;
const isServerlessBuild = this.ctx.env.packageInfo.buildFlavor === 'serverless';
const isDevEnvironment = this.ctx.env.mode.dev;
const pluginSetupDeps = plugins;
// set xpack.observability_onboarding.ui.enabled: true
@ -112,7 +113,10 @@ export class ObservabilityOnboardingPlugin
corePlugins: corePlugins as ObservabilityOnboardingPluginStartDeps,
config,
context: {
isServerless: Boolean(pluginSetupDeps.cloud?.isServerlessEnabled),
isDev: isDevEnvironment,
isCloud: Boolean(pluginSetupDeps.cloud?.isCloudEnabled),
isServerless:
Boolean(pluginSetupDeps.cloud?.isServerlessEnabled) || isServerlessBuild,
stackVersion,
},
});

View file

@ -0,0 +1,141 @@
/*
* 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 * as t from 'io-ts';
import { termQuery, wildcardQuery } from '@kbn/observability-plugin/server';
import type { estypes } from '@elastic/elasticsearch';
import {
AWSIndexName,
AWS_INDEX_NAME_LIST,
FIREHOSE_CLOUDFORMATION_TEMPLATE_URL,
} from '../../../common/aws_firehose';
import { getFallbackESUrl } from '../../lib/get_fallback_urls';
import { createObservabilityOnboardingServerRoute } from '../create_observability_onboarding_server_route';
import { hasLogMonitoringPrivileges } from '../../lib/api_key/has_log_monitoring_privileges';
import { createShipperApiKey } from '../../lib/api_key/create_shipper_api_key';
export interface CreateFirehoseOnboardingFlowRouteResponse {
apiKeyEncoded: string;
onboardingId: string;
elasticsearchUrl: string;
templateUrl: string;
}
export type HasFirehoseDataRouteResponse = AWSIndexName[];
interface DocumentCountPerIndexBucket {
key: string;
doc_count: number;
}
const createFirehoseOnboardingFlowRoute = createObservabilityOnboardingServerRoute({
endpoint: 'POST /internal/observability_onboarding/firehose/flow',
options: { tags: [] },
async handler({
context,
request,
plugins,
services,
}): Promise<CreateFirehoseOnboardingFlowRouteResponse> {
const {
elasticsearch: { client },
} = await context.core;
const hasPrivileges = await hasLogMonitoringPrivileges(client.asCurrentUser);
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 fleetPluginStart = await plugins.fleet.start();
const packageClient = fleetPluginStart.packageService.asScoped(request);
const [{ encoded: apiKeyEncoded }] = await Promise.all([
createShipperApiKey(client.asCurrentUser, 'firehose_onboarding'),
packageClient.ensureInstalledPackage({ pkgName: 'awsfirehose' }),
packageClient.ensureInstalledPackage({ pkgName: 'aws' }),
]);
const elasticsearchUrlList = plugins.cloud?.setup?.elasticsearchUrl
? [plugins.cloud?.setup?.elasticsearchUrl]
: await getFallbackESUrl(services.esLegacyConfigService);
return {
onboardingId: uuidv4(),
apiKeyEncoded,
elasticsearchUrl: elasticsearchUrlList.length > 0 ? elasticsearchUrlList[0] : '',
templateUrl: FIREHOSE_CLOUDFORMATION_TEMPLATE_URL,
};
},
});
const hasFirehoseDataRoute = createObservabilityOnboardingServerRoute({
endpoint: 'GET /internal/observability_onboarding/firehose/has-data',
params: t.type({
query: t.type({
logsStreamName: t.string,
stackName: t.string,
}),
}),
options: { tags: [] },
async handler(resources): Promise<HasFirehoseDataRouteResponse> {
const { logsStreamName, stackName } = resources.params.query;
const { elasticsearch } = await resources.context.core;
const indexPatternList = AWS_INDEX_NAME_LIST.map((index) => `${index}-*`);
try {
const result = await elasticsearch.client.asCurrentUser.search<
unknown,
Record<
'documents_per_index',
estypes.AggregationsMultiBucketAggregateBase<DocumentCountPerIndexBucket>
>
>({
index: indexPatternList,
ignore_unavailable: true,
size: 0,
query: {
bool: {
should: [
...termQuery('aws.kinesis.name', logsStreamName),
...wildcardQuery('aws.exporter.arn', stackName),
],
},
},
aggs: {
documents_per_index: {
terms: {
field: '_index',
size: indexPatternList.length,
},
},
},
});
const buckets = result.aggregations?.documents_per_index.buckets;
if (!Array.isArray(buckets)) {
return [];
}
return AWS_INDEX_NAME_LIST.filter((indexName) => {
return buckets.some((bucket) => bucket.key.includes(indexName) && bucket.doc_count > 0);
});
} catch (error) {
throw Boom.internal(`Elasticsearch responded with an error. ${error.message}`);
}
},
});
export const firehoseOnboardingRouteRepository = {
...createFirehoseOnboardingFlowRoute,
...hasFirehoseDataRoute,
};

View file

@ -9,6 +9,7 @@ import { elasticAgentRouteRepository } from './elastic_agent/route';
import { flowRouteRepository } from './flow/route';
import { kubernetesOnboardingRouteRepository } from './kubernetes/route';
import { logsOnboardingRouteRepository } from './logs/route';
import { firehoseOnboardingRouteRepository } from './firehose/route';
function getTypedObservabilityOnboardingServerRouteRepository() {
const repository = {
@ -16,6 +17,7 @@ function getTypedObservabilityOnboardingServerRouteRepository() {
...logsOnboardingRouteRepository,
...elasticAgentRouteRepository,
...kubernetesOnboardingRouteRepository,
...firehoseOnboardingRouteRepository,
};
return repository;

View file

@ -0,0 +1,89 @@
/*
* 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 { log, timerange } from '@kbn/apm-synthtrace-client';
import moment from 'moment';
import { FtrProviderContext } from '../../../ftr_provider_context';
const CF_COMMAND_REGEXP =
/aws cloudformation create-stack --stack-name (\S+) --template-url \S+ --parameters ParameterKey=FirehoseStreamNameForLogs,ParameterValue=(\S+) .+? --capabilities CAPABILITY_IAM/;
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'svlCommonPage']);
const browser = getService('browser');
const testSubjects = getService('testSubjects');
const synthtrace = getService('svlLogsSynthtraceClient');
describe('Onboarding Firehose Quickstart Flow', () => {
before(async () => {
await PageObjects.svlCommonPage.loginAsAdmin(); // Onboarding requires admin role
await PageObjects.common.navigateToUrlWithBrowserHistory(
'observabilityOnboarding',
'/firehose',
undefined,
{
ensureCurrentUrl: false, // the check sometimes is too slow for the page so it misses the point in time before the app rewrites the URL
}
);
});
after(async () => {
await synthtrace.clean();
});
beforeEach(async () => {
await testSubjects.existOrFail('observabilityOnboardingFirehoseCreateStackCommand');
});
it('shows the correct CloudFormation command snippet', async () => {
await testSubjects.clickWhenNotDisabled('observabilityOnboardingCopyToClipboardButton');
const copiedCommand = await browser.getClipboardValue();
expect(copiedCommand).toMatch(CF_COMMAND_REGEXP);
});
it('starts to monitor for incoming data after user leaves the page', async () => {
await browser.execute(`window.dispatchEvent(new Event("blur"))`);
await testSubjects.isDisplayed('observabilityOnboardingAWSServiceList');
});
it('highlights an AWS service when data is detected', async () => {
const DATASET = 'aws.vpcflow';
const AWS_SERVICE_ID = 'vpc-flow';
await testSubjects.clickWhenNotDisabled('observabilityOnboardingCopyToClipboardButton');
const copiedCommand = await browser.getClipboardValue();
const [, _stackName, logsStreamName] = copiedCommand.match(CF_COMMAND_REGEXP) ?? [];
expect(logsStreamName).toBeDefined();
await browser.execute(`window.dispatchEvent(new Event("blur"))`);
// Simulate Firehose stream ingesting log files
const to = new Date().toISOString();
const count = 1;
await synthtrace.index(
timerange(moment(to).subtract(count, 'minute'), moment(to))
.interval('1m')
.rate(1)
.generator((timestamp) => {
return log.create().dataset(DATASET).timestamp(timestamp).defaults({
'aws.kinesis.name': logsStreamName,
});
})
);
// Checking that an AWS service item is enabled after data is detected
await testSubjects
.find(`observabilityOnboardingAWSService-${AWS_SERVICE_ID}`)
.then((el) => el.findByTagName('button'))
.then((el) => el.isEnabled());
});
});
}

View file

@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Onboarding', function () {
loadTestFile(require.resolve('./auto_detect'));
loadTestFile(require.resolve('./firehose'));
});
}