From 0e326c6936a83a3d51112ced447c42ea007f5ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 20 Jul 2020 14:00:01 +0200 Subject: [PATCH] Add capability check --- ...ants.test.ts => anomaly_detection.test.ts} | 4 +- .../plugins/apm/common/anomaly_detection.ts | 64 +++++++++++++++ x-pack/plugins/apm/common/ml_job_constants.ts | 28 ------- .../ServiceMap/Popover/AnomalyDetection.tsx | 6 +- .../generate_service_map_elements.ts | 2 +- .../app/ServiceMap/cytoscapeOptions.ts | 7 +- .../anomaly_detection/add_environments.tsx | 17 +++- .../Settings/anomaly_detection/create_jobs.ts | 80 ++++++++++++------- .../app/Settings/anomaly_detection/index.tsx | 32 ++++++-- .../Settings/anomaly_detection/jobs_list.tsx | 57 ++++--------- .../apm/AnomalyDetectionSetupLink.test.tsx | 38 +++++---- .../Links/apm/AnomalyDetectionSetupLink.tsx | 20 ++++- .../anomaly_detection_error.ts | 16 ++++ .../create_anomaly_detection_jobs.ts | 19 ++--- .../get_anomaly_detection_jobs.ts | 9 +-- .../routes/settings/anomaly_detection.ts | 43 +++++++--- 16 files changed, 283 insertions(+), 159 deletions(-) rename x-pack/plugins/apm/common/{ml_job_constants.test.ts => anomaly_detection.test.ts} (91%) delete mode 100644 x-pack/plugins/apm/common/ml_job_constants.ts create mode 100644 x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts diff --git a/x-pack/plugins/apm/common/ml_job_constants.test.ts b/x-pack/plugins/apm/common/anomaly_detection.test.ts similarity index 91% rename from x-pack/plugins/apm/common/ml_job_constants.test.ts rename to x-pack/plugins/apm/common/anomaly_detection.test.ts index 96e3ba826d20109..a53b526987a12a6 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.test.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSeverity, severity } from './ml_job_constants'; +import { getSeverity, severity } from './anomaly_detection'; -describe('ml_job_constants', () => { +describe('anomaly_detection', () => { describe('getSeverity', () => { describe('when score is undefined', () => { it('returns undefined', () => { diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 1fd927d82f18666..39df4b724d47512 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -4,9 +4,73 @@ * 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 enum severity { + critical = 'critical', + major = 'major', + minor = 'minor', + warning = 'warning', +} + +export function getSeverity(score?: number) { + if (typeof score !== 'number') { + return undefined; + } else if (score < 25) { + return severity.warning; + } else if (score >= 25 && score < 50) { + return severity.minor; + } else if (score >= 50 && score < 75) { + return severity.major; + } else if (score >= 75) { + return severity.critical; + } else { + return undefined; + } +} + +// error message +export const MLErrorMessages: Record = { + INSUFFICIENT_LICENSE: + 'You must have a platinum license to use Anomaly Detection', + MISSING_READ_PRIVILEGES: i18n.translate( + 'xpack.apm.anomaly_detection.error.insufficient_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.insufficient_privileges', + { + defaultMessage: + 'You must have "write" privileges to Machine Learning and APM in order to view Anomaly Detection jobs', + } + ), + ML_NOT_AVAILABLE: 'Machine learning is not available', + NOT_AVAILABLE_IN_SPACE: i18n.translate( + 'xpack.apm.anomaly_detection.error.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', + NOT_AVAILABLE_IN_SPACE = 'NOT_AVAILABLE_IN_SPACE', + UNEXPECTED = 'UNEXPECTED', +} diff --git a/x-pack/plugins/apm/common/ml_job_constants.ts b/x-pack/plugins/apm/common/ml_job_constants.ts deleted file mode 100644 index b8c2546bd0c84a3..000000000000000 --- a/x-pack/plugins/apm/common/ml_job_constants.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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. - */ - -export enum severity { - critical = 'critical', - major = 'major', - minor = 'minor', - warning = 'warning', -} - -export function getSeverity(score?: number) { - if (typeof score !== 'number') { - return undefined; - } else if (score < 25) { - return severity.warning; - } else if (score >= 25 && score < 50) { - return severity.minor; - } else if (score >= 50 && score < 75) { - return severity.major; - } else if (score >= 75) { - return severity.critical; - } else { - return undefined; - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index 410ba8b5027fbe0..83993273e027589 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -19,9 +19,11 @@ 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 { + ServiceAnomalyStats, + getSeverity, +} from '../../../../../common/anomaly_detection'; const HealthStatusTitle = styled(EuiTitle)` display: inline; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts index e7d55cd57071011..a89b5414bf19779 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts @@ -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 '../../../../../common/anomaly_detection'; export function generateServiceMapElements(size: number): any[] { const services = range(size).map((i) => { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index dfcfbee1806a481..cfea8c1e8b3fac2 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -10,9 +10,12 @@ 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 { + ServiceAnomalyStats, + severity, + getSeverity, +} from '../../../../common/anomaly_detection'; export const popoverWidth = 280; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index 4c056d48f4b14a4..c9328c4988e5f7d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -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'; @@ -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({ @@ -56,6 +60,17 @@ export const AddEnvironments = ({ Array> >([]); + if (!canCreateJob) { + return ( + + {MLErrorMessages.MISSING_WRITE_PRIVILEGES}} + /> + + ); + } + const isLoading = status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; return ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts index 614632a5a3b0920..acea38732b40a22 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts @@ -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, @@ -16,7 +27,7 @@ export async function createJobs({ toasts: NotificationsStart['toasts']; }) { try { - await callApmApi({ + const res = await callApmApi({ pathname: '/api/apm/settings/anomaly-detection/jobs', method: 'POST', params: { @@ -24,41 +35,50 @@ export async function createJobs({ }, }); + // 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, + }, + }); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 99b97b273a200c2..abbe1e2c83c7b0f 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -7,7 +7,9 @@ import React, { useState } from 'react'; import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiPanel } from '@elastic/eui'; +import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui'; +import { MLErrorMessages } from '../../../../../common/anomaly_detection'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; import { useFetcher } from '../../../../hooks/useFetcher'; @@ -23,19 +25,24 @@ export type AnomalyDetectionApiResponse = APIReturnType< const DEFAULT_VALUE: AnomalyDetectionApiResponse = { jobs: [], hasLegacyJobs: false, - error: undefined, + errorCode: undefined, }; export const AnomalyDetection = () => { + const plugin = useApmPluginContext(); + const canGetJobs = !!plugin.core.application.capabilities.ml.canGetJobs; const license = useLicense(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); const [viewAddEnvironments, setViewAddEnvironments] = useState(false); const { refetch, data = DEFAULT_VALUE, status } = useFetcher( - (callApmApi) => - callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), - [], + (callApmApi) => { + if (canGetJobs) { + return callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }); + } + }, + [canGetJobs], { preservePreviousData: false, showToastOnError: false } ); @@ -55,6 +62,17 @@ export const AnomalyDetection = () => { ); } + if (!canGetJobs) { + return ( + + {MLErrorMessages.MISSING_READ_PRIVILEGES}} + /> + + ); + } + return ( <> @@ -85,10 +103,8 @@ export const AnomalyDetection = () => { /> ) : ( { setViewAddEnvironments(true); }} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 3f2dba6ddfe6266..dfc100ecbba1d9e 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -16,6 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { MLErrorMessages } from '../../../../../common/anomaly_detection'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -57,23 +58,16 @@ const columns: Array> = [ ]; interface Props { + data: AnomalyDetectionApiResponse; status: FETCH_STATUS; onAddEnvironments: () => void; - jobs: Jobs; - hasLegacyJobs: boolean; - errorMessage?: string; } -export const JobsList = ({ - status, - onAddEnvironments, - jobs, - hasLegacyJobs, - errorMessage, -}: Props) => { +export const JobsList = ({ data, status, onAddEnvironments }: Props) => { + const { jobs, hasLegacyJobs, errorCode } = data; const isLoading = status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; - const hasFetchFailure = status === FETCH_STATUS.FAILURE || errorMessage; + const hasFetchFailure = status === FETCH_STATUS.FAILURE || !!errorCode; return ( @@ -127,14 +121,13 @@ export const JobsList = ({ isLoading ? ( ) : // Handled error - errorMessage ? ( - errorMessage + errorCode ? ( + MLErrorMessages[errorCode] ) : // Unhandled error hasFetchFailure ? ( - + unhandledErrorText ) : ( - // empty state - + emptyStateText ) } columns={columns} @@ -147,28 +140,12 @@ export const JobsList = ({ ); }; -function EmptyStatePrompt() { - return ( - <> - {i18n.translate( - 'xpack.apm.settings.anomalyDetection.jobList.emptyListText', - { - defaultMessage: 'No anomaly detection jobs.', - } - )} - - ); -} +const emptyStateText = i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.emptyListText', + { defaultMessage: 'No anomaly detection jobs.' } +); -function FailureStatePrompt({ errorMessage: string }) { - return ( - <> - {i18n.translate( - 'xpack.apm.settings.anomalyDetection.jobList.failedFetchText', - { - defaultMessage: 'Unabled to fetch anomaly detection jobs.', - } - )} - - ); -} +const unhandledErrorText = i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.failedFetchText', + { defaultMessage: 'Unabled to fetch anomaly detection jobs.' } +); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx index 268d8bd7ea82399..de588df35c115b8 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx @@ -6,37 +6,47 @@ import { showAlert } from './AnomalyDetectionSetupLink'; +const dataWithJobs = { + hasLegacyJobs: false, + jobs: [ + { job_id: 'job1', environment: 'staging' }, + { job_id: 'job2', environment: 'production' }, + ], +}; + +const dataWithoutJobs = ({ jobs: [] } as unknown) as any; +const dataWithErrorCode = ({ errorCode: 'any' } as unknown) as any; + describe('#showAlert', () => { describe('when an environment is selected', () => { it('should return true when there are no jobs', () => { - const result = showAlert([], 'testing'); + const result = showAlert(dataWithoutJobs, 'testing'); expect(result).toBe(true); }); it('should return true when environment is not included in the jobs', () => { - const result = showAlert( - [{ environment: 'staging' }, { environment: 'production' }], - 'testing' - ); + const result = showAlert(dataWithJobs, 'testing'); expect(result).toBe(true); }); it('should return false when environment is included in the jobs', () => { - const result = showAlert( - [{ environment: 'staging' }, { environment: 'production' }], - 'staging' - ); + const result = showAlert(dataWithJobs, 'staging'); expect(result).toBe(false); }); }); + describe('there is no environment selected (All)', () => { it('should return true when there are no jobs', () => { - const result = showAlert([], undefined); + const result = showAlert(dataWithoutJobs, undefined); expect(result).toBe(true); }); it('should return false when there are any number of jobs', () => { - const result = showAlert( - [{ environment: 'staging' }, { environment: 'production' }], - undefined - ); + const result = showAlert(dataWithJobs, undefined); + expect(result).toBe(false); + }); + }); + + describe('when a known error occurred', () => { + it('should return false', () => { + const result = showAlert(dataWithErrorCode, undefined); expect(result).toBe(false); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx index 6f3a5df480d7e7f..457c6bed68c6a99 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx @@ -6,16 +6,24 @@ import React from 'react'; import { EuiButtonEmpty, EuiToolTip, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMLink } from './APMLink'; import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; +export type AnomalyDetectionApiResponse = APIReturnType< + '/api/apm/settings/anomaly-detection', + 'GET' +>; + +const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false, errorCode: undefined }; + export function AnomalyDetectionSetupLink() { const { uiFilters } = useUrlParams(); const environment = uiFilters.environment; - const { data = { jobs: [], hasLegacyJobs: false }, status } = useFetcher( + const { data = DEFAULT_DATA, status } = useFetcher( (callApmApi) => callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), [], @@ -28,7 +36,7 @@ export function AnomalyDetectionSetupLink() { {ANOMALY_DETECTION_LINK_LABEL} - {isFetchSuccess && showAlert(data.jobs, environment) && ( + {isFetchSuccess && showAlert(data, environment) && ( @@ -59,9 +67,15 @@ const ANOMALY_DETECTION_LINK_LABEL = i18n.translate( ); export function showAlert( - jobs: Array<{ environment: string }> = [], + { jobs = [], errorCode }: AnomalyDetectionApiResponse, environment: string | undefined ) { + // don't show warning if an error occurred + // might be due to insufficient access in which case we shouldn't draw attention to the feature + if (errorCode) { + return false; + } + return ( // No job exists, or jobs.length === 0 || diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts new file mode 100644 index 000000000000000..993dcf4c5354bfb --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts @@ -0,0 +1,16 @@ +/* + * 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 { ErrorCode, MLErrorMessages } from '../../../common/anomaly_detection'; + +export class AnomalyDetectionError extends Error { + constructor(public code: ErrorCode) { + super(MLErrorMessages[code]); + + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 1dba360e07b5257..05bcf81e1a3dc0c 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -7,7 +7,7 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; -import Boom from 'boom'; +import { ErrorCode } from '../../../common/anomaly_detection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; import { @@ -16,6 +16,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { AnomalyDetectionError } from './anomaly_detection_error'; export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType< typeof createAnomalyDetectionJobs @@ -28,19 +29,16 @@ export async function createAnomalyDetectionJobs( const { ml, indices } = setup; if (!ml) { - throw Boom.internal('Machine learning is not available'); + throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE); } const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw Boom.internal( - 'Anomaly detection feature is not enabled for the space.' - ); + throw new AnomalyDetectionError(ErrorCode.NOT_AVAILABLE_IN_SPACE); } + if (!mlCapabilities.isPlatinumOrTrialLicense) { - throw Boom.internal( - 'Unable to create anomaly detection jobs due to insufficient license.' - ); + throw new AnomalyDetectionError(ErrorCode.INSUFFICIENT_LICENSE); } logger.info( @@ -62,9 +60,8 @@ export async function createAnomalyDetectionJobs( `Failed to create anomaly detection ML jobs for: [${failedJobIds}]:` ); failedJobs.forEach(({ error }) => logger.error(JSON.stringify(error))); - throw Boom.internal( - `Failed to create anomaly detection ML jobs for: [${failedJobIds}].` - ); + + throw new AnomalyDetectionError(ErrorCode.UNEXPECTED); } return jobResponses; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts index 93e42f45a07992a..7ae544e8ec84df9 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts @@ -5,9 +5,10 @@ */ import { Logger } from 'kibana/server'; -import Boom from 'boom'; +import { ErrorCode } from '../../../common/anomaly_detection'; import { Setup } from '../helpers/setup_request'; import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; +import { AnomalyDetectionError } from './anomaly_detection_error'; export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { const { ml } = setup; @@ -17,13 +18,11 @@ export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw Boom.internal( - 'Machine learning is not available in the selected space' - ); + throw new AnomalyDetectionError(ErrorCode.NOT_AVAILABLE_IN_SPACE); } if (!mlCapabilities.isPlatinumOrTrialLicense) { - throw Boom.internal('You must have a platinum license to use ML anomalies'); + throw new AnomalyDetectionError(ErrorCode.INSUFFICIENT_LICENSE); } const response = await getMlJobsWithAPMGroup(ml); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 9b3566e187f526e..f7992b4fedc26e6 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import Boom from 'boom'; +import { ErrorCode } from '../../../common/anomaly_detection'; import { PromiseReturnType } from '../../../typings/common'; import { InsufficientMLCapabilities } from '../../../../ml/server'; import { createRoute } from '../create_route'; @@ -14,6 +14,7 @@ import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_a import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; +import { AnomalyDetectionError } from '../../lib/anomaly_detection/anomaly_detection_error'; type Jobs = PromiseReturnType; @@ -39,25 +40,25 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({ return { jobs: [] as Jobs, hasLegacyJobs: false, - error: - 'You must have "read" privileges to Machine Learning in order to view ML jobs', + errorCode: ErrorCode.MISSING_READ_PRIVILEGES, }; } - // Boom error - if (Boom.isBoom(e)) { + // AnomalyDetectionError error + if (e instanceof AnomalyDetectionError) { return { jobs: [] as Jobs, hasLegacyJobs: false, - error: e.message, + errorCode: e.code, }; } - // unknown error + // unexpected error context.logger.warn(e.message); return { jobs: [] as Jobs, hasLegacyJobs: false, + errorCode: ErrorCode.UNEXPECTED, }; } }, @@ -79,11 +80,29 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ const { environments } = context.params.body; const setup = await setupRequest(context, request); - return await createAnomalyDetectionJobs( - setup, - environments, - context.logger - ); + try { + await createAnomalyDetectionJobs(setup, environments, context.logger); + } catch (e) { + // InsufficientMLCapabilities + if (e instanceof InsufficientMLCapabilities) { + return { + errorCode: ErrorCode.MISSING_READ_PRIVILEGES, + }; + } + + // AnomalyDetectionError + if (e instanceof AnomalyDetectionError) { + return { + errorCode: e.code, + }; + } + + // unexpected error + context.logger.warn(e.message); + return { + errorCode: ErrorCode.UNEXPECTED, + }; + } }, }));