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

[7.x] [APM] Handle ML errors (#72316) #72582

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
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