Skip to content

Commit

Permalink
[APM] Adds 'Anomaly detection' settings page to create ML jobs per en…
Browse files Browse the repository at this point in the history
…vironment (#70560) (#70945)
  • Loading branch information
sorenlouv committed Jul 7, 2020
1 parent 4bba510 commit 607ee02
Show file tree
Hide file tree
Showing 21 changed files with 906 additions and 72 deletions.
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,164 @@
/*
* 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';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';

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 environmentOptions = data.map((env) => ({
label: env === ENVIRONMENT_NOT_DEFINED ? NOT_DEFINED_OPTION_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:
'Select the service environments that you want to enable anomaly detection in. Anomalies will surface for all services and transaction types within the selected environments.',
}
)}
</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={environmentOptions}
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>
);
};

const NOT_DEFINED_OPTION_LABEL = i18n.translate(
'xpack.apm.filter.environment.notDefinedLabel',
{
defaultMessage: 'Not defined',
}
);
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 anomaly detection integration enables application health status indicators in the Service map by identifying transaction duration anomalies.',
})}
</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

0 comments on commit 607ee02

Please sign in to comment.