Skip to content

Commit 7a17e73

Browse files
sorenlouvAPM User
authored andcommitted
[APM] Handle ML errors (#72316)
* [APM] Handle ML errors * Add capability check * Improve test * Address Caue’s feedback * Move getSeverity * Fix tsc * Fix copy
1 parent 9ee42d0 commit 7a17e73

File tree

17 files changed

+355
-176
lines changed

17 files changed

+355
-176
lines changed

x-pack/plugins/apm/common/anomaly_detection.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,59 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
import { i18n } from '@kbn/i18n';
8+
79
export interface ServiceAnomalyStats {
810
transactionType?: string;
911
anomalyScore?: number;
1012
actualValue?: number;
1113
jobId?: string;
1214
}
15+
16+
export const MLErrorMessages: Record<ErrorCode, string> = {
17+
INSUFFICIENT_LICENSE: i18n.translate(
18+
'xpack.apm.anomaly_detection.error.insufficient_license',
19+
{
20+
defaultMessage:
21+
'You must have a platinum license to use Anomaly Detection',
22+
}
23+
),
24+
MISSING_READ_PRIVILEGES: i18n.translate(
25+
'xpack.apm.anomaly_detection.error.missing_read_privileges',
26+
{
27+
defaultMessage:
28+
'You must have "read" privileges to Machine Learning in order to view Anomaly Detection jobs',
29+
}
30+
),
31+
MISSING_WRITE_PRIVILEGES: i18n.translate(
32+
'xpack.apm.anomaly_detection.error.missing_write_privileges',
33+
{
34+
defaultMessage:
35+
'You must have "write" privileges to Machine Learning and APM in order to create Anomaly Detection jobs',
36+
}
37+
),
38+
ML_NOT_AVAILABLE: i18n.translate(
39+
'xpack.apm.anomaly_detection.error.not_available',
40+
{
41+
defaultMessage: 'Machine learning is not available',
42+
}
43+
),
44+
ML_NOT_AVAILABLE_IN_SPACE: i18n.translate(
45+
'xpack.apm.anomaly_detection.error.not_available_in_space',
46+
{
47+
defaultMessage: 'Machine learning is not available in the selected space',
48+
}
49+
),
50+
UNEXPECTED: i18n.translate('xpack.apm.anomaly_detection.error.unexpected', {
51+
defaultMessage: 'An unexpected error occurred',
52+
}),
53+
};
54+
55+
export enum ErrorCode {
56+
INSUFFICIENT_LICENSE = 'INSUFFICIENT_LICENSE',
57+
MISSING_READ_PRIVILEGES = 'MISSING_READ_PRIVILEGES',
58+
MISSING_WRITE_PRIVILEGES = 'MISSING_WRITE_PRIVILEGES',
59+
ML_NOT_AVAILABLE = 'ML_NOT_AVAILABLE',
60+
ML_NOT_AVAILABLE_IN_SPACE = 'ML_NOT_AVAILABLE_IN_SPACE',
61+
UNEXPECTED = 'UNEXPECTED',
62+
}

x-pack/plugins/apm/common/ml_job_constants.test.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import { fontSize, px } from '../../../../style/variables';
1919
import { asInteger, asDuration } from '../../../../utils/formatters';
2020
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
2121
import { getSeverityColor, popoverWidth } from '../cytoscapeOptions';
22-
import { getSeverity } from '../../../../../common/ml_job_constants';
2322
import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types';
2423
import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection';
24+
import { getSeverity } from './getSeverity';
2525

2626
const HealthStatusTitle = styled(EuiTitle)`
2727
display: inline;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { getSeverity, severity } from './getSeverity';
8+
9+
describe('getSeverity', () => {
10+
describe('when score is undefined', () => {
11+
it('returns undefined', () => {
12+
expect(getSeverity(undefined)).toEqual(undefined);
13+
});
14+
});
15+
16+
describe('when score < 25', () => {
17+
it('returns warning', () => {
18+
expect(getSeverity(10)).toEqual(severity.warning);
19+
});
20+
});
21+
22+
describe('when score is between 25 and 50', () => {
23+
it('returns minor', () => {
24+
expect(getSeverity(40)).toEqual(severity.minor);
25+
});
26+
});
27+
28+
describe('when score is between 50 and 75', () => {
29+
it('returns major', () => {
30+
expect(getSeverity(60)).toEqual(severity.major);
31+
});
32+
});
33+
34+
describe('when score is 75 or more', () => {
35+
it('returns critical', () => {
36+
expect(getSeverity(100)).toEqual(severity.critical);
37+
});
38+
});
39+
});

x-pack/plugins/apm/common/ml_job_constants.ts renamed to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export enum severity {
1111
warning = 'warning',
1212
}
1313

14+
// TODO: Replace with `getSeverity` from:
15+
// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129
1416
export function getSeverity(score?: number) {
1517
if (typeof score !== 'number') {
1618
return undefined;

x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { getSeverity } from '../../../../../common/ml_job_constants';
7+
import { getSeverity } from '../Popover/getSeverity';
88

99
export function generateServiceMapElements(size: number): any[] {
1010
const services = range(size).map((i) => {

x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
SPAN_DESTINATION_SERVICE_RESOURCE,
1111
} from '../../../../common/elasticsearch_fieldnames';
1212
import { EuiTheme } from '../../../../../observability/public';
13-
import { severity, getSeverity } from '../../../../common/ml_job_constants';
1413
import { defaultIcon, iconForNode } from './icons';
1514
import { ServiceAnomalyStats } from '../../../../common/anomaly_detection';
15+
import { severity, getSeverity } from './Popover/getSeverity';
1616

1717
export const popoverWidth = 280;
1818

x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import {
1717
EuiFlexGroup,
1818
EuiFlexItem,
1919
EuiFormRow,
20+
EuiEmptyPrompt,
2021
} from '@elastic/eui';
2122
import { i18n } from '@kbn/i18n';
23+
import { MLErrorMessages } from '../../../../../common/anomaly_detection';
2224
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
2325
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
2426
import { createJobs } from './create_jobs';
@@ -34,7 +36,9 @@ export const AddEnvironments = ({
3436
onCreateJobSuccess,
3537
onCancel,
3638
}: Props) => {
37-
const { toasts } = useApmPluginContext().core.notifications;
39+
const { notifications, application } = useApmPluginContext().core;
40+
const canCreateJob = !!application.capabilities.ml.canCreateJob;
41+
const { toasts } = notifications;
3842
const { data = [], status } = useFetcher(
3943
(callApmApi) =>
4044
callApmApi({
@@ -56,6 +60,17 @@ export const AddEnvironments = ({
5660
Array<EuiComboBoxOptionOption<string>>
5761
>([]);
5862

63+
if (!canCreateJob) {
64+
return (
65+
<EuiPanel>
66+
<EuiEmptyPrompt
67+
iconType="warning"
68+
body={<>{MLErrorMessages.MISSING_WRITE_PRIVILEGES}</>}
69+
/>
70+
</EuiPanel>
71+
);
72+
}
73+
5974
const isLoading =
6075
status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
6176
return (

x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,19 @@
66

77
import { i18n } from '@kbn/i18n';
88
import { NotificationsStart } from 'kibana/public';
9+
import { MLErrorMessages } from '../../../../../common/anomaly_detection';
910
import { callApmApi } from '../../../../services/rest/createCallApmApi';
1011

12+
const errorToastTitle = i18n.translate(
13+
'xpack.apm.anomalyDetection.createJobs.failed.title',
14+
{ defaultMessage: 'Anomaly detection jobs could not be created' }
15+
);
16+
17+
const successToastTitle = i18n.translate(
18+
'xpack.apm.anomalyDetection.createJobs.succeeded.title',
19+
{ defaultMessage: 'Anomaly detection jobs created' }
20+
);
21+
1122
export async function createJobs({
1223
environments,
1324
toasts,
@@ -16,49 +27,58 @@ export async function createJobs({
1627
toasts: NotificationsStart['toasts'];
1728
}) {
1829
try {
19-
await callApmApi({
30+
const res = await callApmApi({
2031
pathname: '/api/apm/settings/anomaly-detection/jobs',
2132
method: 'POST',
2233
params: {
2334
body: { environments },
2435
},
2536
});
2637

38+
// a known error occurred
39+
if (res?.errorCode) {
40+
toasts.addDanger({
41+
title: errorToastTitle,
42+
text: MLErrorMessages[res.errorCode],
43+
});
44+
return false;
45+
}
46+
47+
// job created successfully
2748
toasts.addSuccess({
28-
title: i18n.translate(
29-
'xpack.apm.anomalyDetection.createJobs.succeeded.title',
30-
{ defaultMessage: 'Anomaly detection jobs created' }
31-
),
32-
text: i18n.translate(
33-
'xpack.apm.anomalyDetection.createJobs.succeeded.text',
34-
{
35-
defaultMessage:
36-
'Anomaly detection jobs successfully created for APM service environments [{environments}]. It will take some time for machine learning to start analyzing traffic for anomalies.',
37-
values: { environments: environments.join(', ') },
38-
}
39-
),
49+
title: successToastTitle,
50+
text: getSuccessToastMessage(environments),
4051
});
4152
return true;
53+
54+
// an unknown/unexpected error occurred
4255
} catch (error) {
4356
toasts.addDanger({
44-
title: i18n.translate(
45-
'xpack.apm.anomalyDetection.createJobs.failed.title',
46-
{
47-
defaultMessage: 'Anomaly detection jobs could not be created',
48-
}
49-
),
50-
text: i18n.translate(
51-
'xpack.apm.anomalyDetection.createJobs.failed.text',
52-
{
53-
defaultMessage:
54-
'Something went wrong when creating one ore more anomaly detection jobs for APM service environments [{environments}]. Error: "{errorMessage}"',
55-
values: {
56-
environments: environments.join(', '),
57-
errorMessage: error.message,
58-
},
59-
}
60-
),
57+
title: errorToastTitle,
58+
text: getErrorToastMessage(environments, error),
6159
});
6260
return false;
6361
}
6462
}
63+
64+
function getSuccessToastMessage(environments: string[]) {
65+
return i18n.translate(
66+
'xpack.apm.anomalyDetection.createJobs.succeeded.text',
67+
{
68+
defaultMessage:
69+
'Anomaly detection jobs successfully created for APM service environments [{environments}]. It will take some time for machine learning to start analyzing traffic for anomalies.',
70+
values: { environments: environments.join(', ') },
71+
}
72+
);
73+
}
74+
75+
function getErrorToastMessage(environments: string[], error: Error) {
76+
return i18n.translate('xpack.apm.anomalyDetection.createJobs.failed.text', {
77+
defaultMessage:
78+
'Something went wrong when creating one ore more anomaly detection jobs for APM service environments [{environments}]. Error: "{errorMessage}"',
79+
values: {
80+
environments: environments.join(', '),
81+
errorMessage: error.message,
82+
},
83+
});
84+
}

0 commit comments

Comments
 (0)