Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] Adds 'Anomaly detection' settings page to create ML jobs per environment #70560

Merged
merged 15 commits into from
Jul 7, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUr
import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n';
import { TraceLink } from '../../TraceLink';
import { CustomizeUI } from '../../Settings/CustomizeUI';
import { AnomalyDetection } from '../../Settings/anomaly_detection';
import {
EditAgentConfigurationRouteHandler,
CreateAgentConfigurationRouteHandler,
Expand Down Expand Up @@ -268,4 +269,20 @@ export const routes: BreadcrumbRoute[] = [
}),
name: RouteName.RUM_OVERVIEW,
},
{
exact: true,
path: '/settings/anomaly-detection',
component: () => (
<Settings>
<AnomalyDetection />
</Settings>
),
breadcrumb: i18n.translate(
'xpack.apm.breadcrumb.settings.anomalyDetection',
{
defaultMessage: 'Anomaly detection',
}
),
name: RouteName.ANOMALY_DETECTION,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export enum RouteName {
LINK_TO_TRACE = 'link_to_trace',
CUSTOMIZE_UI = 'customize_ui',
RUM_OVERVIEW = 'rum_overview',
ANOMALY_DETECTION = 'anomaly_detection',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useState } from 'react';
import {
EuiPanel,
EuiTitle,
EuiText,
EuiSpacer,
EuiButton,
EuiButtonEmpty,
EuiComboBox,
EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { createJobs } from './create_jobs';

interface Props {
currentEnvironments: string[];
onCreateJobSuccess: () => void;
onCancel: () => void;
}
export const AddEnvironments = ({
currentEnvironments,
onCreateJobSuccess,
onCancel,
}: Props) => {
const { toasts } = useApmPluginContext().core.notifications;
const { data = [], status } = useFetcher(
(callApmApi) =>
callApmApi({
pathname: `/api/apm/settings/anomaly-detection/environments`,
}),
[],
{ preservePreviousData: false }
);

const availableEnvironmentOptions = data.map((env) => ({
label: env,
value: env,
disabled: currentEnvironments.includes(env),
}));

const [selectedOptions, setSelected] = useState<
Array<EuiComboBoxOptionOption<string>>
>([]);

const isLoading =
status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
return (
<EuiPanel>
<EuiTitle>
<h2>
{i18n.translate(
'xpack.apm.settings.anomalyDetection.addEnvironments.titleText',
{
defaultMessage: 'Select environments',
}
)}
</h2>
</EuiTitle>
<EuiSpacer size="l" />
<EuiText>
{i18n.translate(
'xpack.apm.settings.anomalyDetection.addEnvironments.descriptionText',
{
defaultMessage:
'Choose the service environments that you want to enable anomaly detection for. Anomalies will surface for all the services and their transaction types.',
ogupte marked this conversation as resolved.
Show resolved Hide resolved
}
)}
</EuiText>
<EuiSpacer size="l" />
<EuiFormRow
label={i18n.translate(
'xpack.apm.settings.anomalyDetection.addEnvironments.selectorLabel',
{
defaultMessage: 'Environments',
}
)}
fullWidth
>
<EuiComboBox
isLoading={isLoading}
placeholder={i18n.translate(
'xpack.apm.settings.anomalyDetection.addEnvironments.selectorPlaceholder',
{
defaultMessage: 'Select or add environments',
}
)}
options={availableEnvironmentOptions}
selectedOptions={selectedOptions}
onChange={(nextSelectedOptions) => {
setSelected(nextSelectedOptions);
}}
onCreateOption={(searchValue) => {
if (currentEnvironments.includes(searchValue)) {
return;
}
const newOption = {
label: searchValue,
value: searchValue,
};
setSelected([...selectedOptions, newOption]);
}}
isClearable={true}
/>
</EuiFormRow>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty aria-label="Cancel" onClick={onCancel}>
{i18n.translate(
'xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText',
{
defaultMessage: 'Cancel',
}
)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
disabled={selectedOptions.length === 0}
onClick={async () => {
const selectedEnvironments = selectedOptions.map(
({ value }) => value as string
);
const success = await createJobs({
environments: selectedEnvironments,
toasts,
});
if (success) {
onCreateJobSuccess();
}
}}
>
{i18n.translate(
'xpack.apm.settings.anomalyDetection.addEnvironments.createJobsButtonText',
{
defaultMessage: 'Create Jobs',
}
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
</EuiPanel>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';
import { NotificationsStart } from 'kibana/public';
import { callApmApi } from '../../../../services/rest/createCallApmApi';

export async function createJobs({
environments,
toasts,
}: {
environments: string[];
toasts: NotificationsStart['toasts'];
}) {
try {
await callApmApi({
pathname: '/api/apm/settings/anomaly-detection/jobs',
method: 'POST',
params: {
body: { environments },
},
});

toasts.addSuccess({
title: i18n.translate(
'xpack.apm.anomalyDetection.createJobs.succeeded.title',
{ defaultMessage: 'Anomaly detection jobs created' }
),
text: i18n.translate(
'xpack.apm.anomalyDetection.createJobs.succeeded.text',
{
defaultMessage:
'Anomaly detection jobs successfully created for APM service environments [{environments}]. It will take some time for machine learning to start analyzing traffic for anomalies.',
values: { environments: environments.join(', ') },
}
),
});
return true;
} catch (error) {
toasts.addDanger({
title: i18n.translate(
'xpack.apm.anomalyDetection.createJobs.failed.title',
{
defaultMessage: 'Anomaly detection jobs could not be created',
}
),
text: i18n.translate(
'xpack.apm.anomalyDetection.createJobs.failed.text',
{
defaultMessage:
'Something went wrong when creating one ore more anomaly detection jobs for APM service environments [{environments}]. Error: "{errorMessage}"',
values: {
environments: environments.join(', '),
errorMessage: error.message,
},
}
),
});
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useState } from 'react';
import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { JobsList } from './jobs_list';
import { AddEnvironments } from './add_environments';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';

export const AnomalyDetection = () => {
const [viewAddEnvironments, setViewAddEnvironments] = useState(false);

const { refetch, data = [], status } = useFetcher(
(callApmApi) =>
callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }),
[],
{ preservePreviousData: false }
);

const isLoading =
status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
const hasFetchFailure = status === FETCH_STATUS.FAILURE;

return (
<>
<EuiTitle size="l">
<h1>
{i18n.translate('xpack.apm.settings.anomalyDetection.titleText', {
defaultMessage: 'Anomaly detection',
})}
</h1>
</EuiTitle>
<EuiSpacer size="l" />
<EuiText>
{i18n.translate('xpack.apm.settings.anomalyDetection.descriptionText', {
defaultMessage:
'The Machine Learning integration enables you to see the health status of your applications in the Service map and identifies anomalies in your transaction duration to show unexpected increase in response time.',
ogupte marked this conversation as resolved.
Show resolved Hide resolved
})}
</EuiText>
<EuiSpacer size="l" />
{viewAddEnvironments ? (
<AddEnvironments
currentEnvironments={data.map(({ environment }) => environment)}
onCreateJobSuccess={() => {
refetch();
setViewAddEnvironments(false);
}}
onCancel={() => {
setViewAddEnvironments(false);
}}
/>
) : (
<JobsList
isLoading={isLoading}
hasFetchFailure={hasFetchFailure}
anomalyDetectionJobsByEnv={data}
onAddEnvironments={() => {
setViewAddEnvironments(true);
}}
/>
)}
</>
);
};
Loading