Skip to content

Commit

Permalink
Add capability check
Browse files Browse the repository at this point in the history
  • Loading branch information
sorenlouv committed Jul 20, 2020
1 parent 8708539 commit 0e326c6
Show file tree
Hide file tree
Showing 16 changed files with 283 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
64 changes: 64 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,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<ErrorCode, string> = {
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',
}
28 changes: 0 additions & 28 deletions x-pack/plugins/apm/common/ml_job_constants.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
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 '../../../../../common/anomaly_detection';

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,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;

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,
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 }
);

Expand All @@ -55,6 +62,17 @@ export const AnomalyDetection = () => {
);
}

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

return (
<>
<EuiTitle size="l">
Expand Down Expand Up @@ -85,10 +103,8 @@ export const AnomalyDetection = () => {
/>
) : (
<JobsList
data={data}
status={status}
errorMessage={data.error}
jobs={data.jobs}
hasLegacyJobs={data.hasLegacyJobs}
onAddEnvironments={() => {
setViewAddEnvironments(true);
}}
Expand Down
Loading

0 comments on commit 0e326c6

Please sign in to comment.