Skip to content

Commit

Permalink
[APM] Handle ML errors (#72316) (#72577)
Browse files Browse the repository at this point in the history
* [APM] Handle ML errors

* Add capability check

* Improve test

* Address Caue’s feedback

* Move getSeverity

* Fix tsc

* Fix copy
  • Loading branch information
sorenlouv authored Jul 21, 2020
1 parent b1b5325 commit 57d9b81
Show file tree
Hide file tree
Showing 17 changed files with 355 additions and 176 deletions.
50 changes: 50 additions & 0 deletions x-pack/plugins/apm/common/anomaly_detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,59 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';

export interface ServiceAnomalyStats {
transactionType?: string;
anomalyScore?: number;
actualValue?: number;
jobId?: string;
}

export const MLErrorMessages: Record<ErrorCode, string> = {
INSUFFICIENT_LICENSE: i18n.translate(
'xpack.apm.anomaly_detection.error.insufficient_license',
{
defaultMessage:
'You must have a platinum license to use Anomaly Detection',
}
),
MISSING_READ_PRIVILEGES: i18n.translate(
'xpack.apm.anomaly_detection.error.missing_read_privileges',
{
defaultMessage:
'You must have "read" privileges to Machine Learning in order to view Anomaly Detection jobs',
}
),
MISSING_WRITE_PRIVILEGES: i18n.translate(
'xpack.apm.anomaly_detection.error.missing_write_privileges',
{
defaultMessage:
'You must have "write" privileges to Machine Learning and APM in order to create Anomaly Detection jobs',
}
),
ML_NOT_AVAILABLE: i18n.translate(
'xpack.apm.anomaly_detection.error.not_available',
{
defaultMessage: 'Machine learning is not available',
}
),
ML_NOT_AVAILABLE_IN_SPACE: i18n.translate(
'xpack.apm.anomaly_detection.error.not_available_in_space',
{
defaultMessage: 'Machine learning is not available in the selected space',
}
),
UNEXPECTED: i18n.translate('xpack.apm.anomaly_detection.error.unexpected', {
defaultMessage: 'An unexpected error occurred',
}),
};

export enum ErrorCode {
INSUFFICIENT_LICENSE = 'INSUFFICIENT_LICENSE',
MISSING_READ_PRIVILEGES = 'MISSING_READ_PRIVILEGES',
MISSING_WRITE_PRIVILEGES = 'MISSING_WRITE_PRIVILEGES',
ML_NOT_AVAILABLE = 'ML_NOT_AVAILABLE',
ML_NOT_AVAILABLE_IN_SPACE = 'ML_NOT_AVAILABLE_IN_SPACE',
UNEXPECTED = 'UNEXPECTED',
}
41 changes: 0 additions & 41 deletions x-pack/plugins/apm/common/ml_job_constants.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import { fontSize, px } from '../../../../style/variables';
import { asInteger, asDuration } from '../../../../utils/formatters';
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
import { getSeverityColor, popoverWidth } from '../cytoscapeOptions';
import { getSeverity } from '../../../../../common/ml_job_constants';
import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types';
import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection';
import { getSeverity } from './getSeverity';

const HealthStatusTitle = styled(EuiTitle)`
display: inline;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { getSeverity, severity } from './getSeverity';

describe('getSeverity', () => {
describe('when score is undefined', () => {
it('returns undefined', () => {
expect(getSeverity(undefined)).toEqual(undefined);
});
});

describe('when score < 25', () => {
it('returns warning', () => {
expect(getSeverity(10)).toEqual(severity.warning);
});
});

describe('when score is between 25 and 50', () => {
it('returns minor', () => {
expect(getSeverity(40)).toEqual(severity.minor);
});
});

describe('when score is between 50 and 75', () => {
it('returns major', () => {
expect(getSeverity(60)).toEqual(severity.major);
});
});

describe('when score is 75 or more', () => {
it('returns critical', () => {
expect(getSeverity(100)).toEqual(severity.critical);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export enum severity {
warning = 'warning',
}

// TODO: Replace with `getSeverity` from:
// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129
export function getSeverity(score?: number) {
if (typeof score !== 'number') {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { getSeverity } from '../../../../../common/ml_job_constants';
import { getSeverity } from '../Popover/getSeverity';

export function generateServiceMapElements(size: number): any[] {
const services = range(size).map((i) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
SPAN_DESTINATION_SERVICE_RESOURCE,
} from '../../../../common/elasticsearch_fieldnames';
import { EuiTheme } from '../../../../../observability/public';
import { severity, getSeverity } from '../../../../common/ml_job_constants';
import { defaultIcon, iconForNode } from './icons';
import { ServiceAnomalyStats } from '../../../../common/anomaly_detection';
import { severity, getSeverity } from './Popover/getSeverity';

export const popoverWidth = 280;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiEmptyPrompt,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MLErrorMessages } from '../../../../../common/anomaly_detection';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { createJobs } from './create_jobs';
Expand All @@ -34,7 +36,9 @@ export const AddEnvironments = ({
onCreateJobSuccess,
onCancel,
}: Props) => {
const { toasts } = useApmPluginContext().core.notifications;
const { notifications, application } = useApmPluginContext().core;
const canCreateJob = !!application.capabilities.ml.canCreateJob;
const { toasts } = notifications;
const { data = [], status } = useFetcher(
(callApmApi) =>
callApmApi({
Expand All @@ -56,6 +60,17 @@ export const AddEnvironments = ({
Array<EuiComboBoxOptionOption<string>>
>([]);

if (!canCreateJob) {
return (
<EuiPanel>
<EuiEmptyPrompt
iconType="warning"
body={<>{MLErrorMessages.MISSING_WRITE_PRIVILEGES}</>}
/>
</EuiPanel>
);
}

const isLoading =
status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@

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

const errorToastTitle = i18n.translate(
'xpack.apm.anomalyDetection.createJobs.failed.title',
{ defaultMessage: 'Anomaly detection jobs could not be created' }
);

const successToastTitle = i18n.translate(
'xpack.apm.anomalyDetection.createJobs.succeeded.title',
{ defaultMessage: 'Anomaly detection jobs created' }
);

export async function createJobs({
environments,
toasts,
Expand All @@ -16,49 +27,58 @@ export async function createJobs({
toasts: NotificationsStart['toasts'];
}) {
try {
await callApmApi({
const res = await callApmApi({
pathname: '/api/apm/settings/anomaly-detection/jobs',
method: 'POST',
params: {
body: { environments },
},
});

// a known error occurred
if (res?.errorCode) {
toasts.addDanger({
title: errorToastTitle,
text: MLErrorMessages[res.errorCode],
});
return false;
}

// job created successfully
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(', ') },
}
),
title: successToastTitle,
text: getSuccessToastMessage(environments),
});
return true;

// an unknown/unexpected error occurred
} 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,
},
}
),
title: errorToastTitle,
text: getErrorToastMessage(environments, error),
});
return false;
}
}

function getSuccessToastMessage(environments: string[]) {
return 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(', ') },
}
);
}

function getErrorToastMessage(environments: string[], error: Error) {
return 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,
},
});
}
Loading

0 comments on commit 57d9b81

Please sign in to comment.