Skip to content

Commit

Permalink
[APM] Anomaly detection setup link with alert if job doesn't exist (e…
Browse files Browse the repository at this point in the history
…lastic#71229)

* Closes elastic#70440 by adding a setup link to anomaly detection setting in the home header

* PR feedback and type error fix

* Code cleanup and PR feedback

* Modified getEnvironmentUiFilterES return type from `ESFilter | undefined` to `ESFilter[]` for ease of use.

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
ogupte and elasticmachine committed Jul 13, 2020
1 parent 28c5d91 commit 3c9c187
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 149 deletions.
11 changes: 11 additions & 0 deletions x-pack/plugins/apm/common/environment_filter_values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/

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

export const ENVIRONMENT_ALL = 'ENVIRONMENT_ALL';
export const ENVIRONMENT_NOT_DEFINED = 'ENVIRONMENT_NOT_DEFINED';

export function getEnvironmentLabel(environment: string) {
if (environment === ENVIRONMENT_NOT_DEFINED) {
return i18n.translate('xpack.apm.filter.environment.notDefinedLabel', {
defaultMessage: 'Not defined',
});
}
return environment;
}
4 changes: 4 additions & 0 deletions x-pack/plugins/apm/public/components/app/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { EuiTabLink } from '../../shared/EuiTabLink';
import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink';
import { ServiceOverviewLink } from '../../shared/Links/apm/ServiceOverviewLink';
import { SettingsLink } from '../../shared/Links/apm/SettingsLink';
import { AnomalyDetectionSetupLink } from '../../shared/Links/apm/AnomalyDetectionSetupLink';
import { TraceOverviewLink } from '../../shared/Links/apm/TraceOverviewLink';
import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink';
import { ServiceMap } from '../ServiceMap';
Expand Down Expand Up @@ -118,6 +119,9 @@ export function Home({ tab }: Props) {
</EuiButtonEmpty>
</SettingsLink>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AnomalyDetectionSetupLink />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SetupInstructionsLink />
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { createJobs } from './create_jobs';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
import { getEnvironmentLabel } from '../../../../../common/environment_filter_values';

interface Props {
currentEnvironments: string[];
Expand All @@ -45,7 +45,7 @@ export const AddEnvironments = ({
);

const environmentOptions = data.map((env) => ({
label: env === ENVIRONMENT_NOT_DEFINED ? NOT_DEFINED_OPTION_LABEL : env,
label: getEnvironmentLabel(env),
value: env,
disabled: currentEnvironments.includes(env),
}));
Expand Down Expand Up @@ -155,10 +155,3 @@ export const AddEnvironments = ({
</EuiPanel>
);
};

const NOT_DEFINED_OPTION_LABEL = i18n.translate(
'xpack.apm.filter.environment.notDefinedLabel',
{
defaultMessage: 'Not defined',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection';
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
import { getEnvironmentLabel } from '../../../../../common/environment_filter_values';
import { LegacyJobsCallout } from './legacy_jobs_callout';

const columns: Array<ITableColumn<AnomalyDetectionJobByEnv>> = [
Expand All @@ -32,14 +32,7 @@ const columns: Array<ITableColumn<AnomalyDetectionJobByEnv>> = [
'xpack.apm.settings.anomalyDetection.jobList.environmentColumnLabel',
{ defaultMessage: 'Environment' }
),
render: (environment: string) => {
if (environment === ENVIRONMENT_NOT_DEFINED) {
return i18n.translate('xpack.apm.filter.environment.notDefinedLabel', {
defaultMessage: 'Not defined',
});
}
return environment;
},
render: getEnvironmentLabel,
},
{
field: 'job_id',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 React from 'react';
import { EuiButtonEmpty, EuiToolTip, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { APMLink } from './APMLink';
import { getEnvironmentLabel } from '../../../../../common/environment_filter_values';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';

export function AnomalyDetectionSetupLink() {
const { uiFilters } = useUrlParams();
const environment = uiFilters.environment;

const { data = { jobs: [], hasLegacyJobs: false }, status } = useFetcher(
(callApmApi) =>
callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }),
[],
{ preservePreviousData: false }
);
const isFetchSuccess = status === FETCH_STATUS.SUCCESS;

// Show alert if there are no jobs OR if no job matches the current environment
const showAlert =
isFetchSuccess && !data.jobs.some((job) => environment === job.environment);

return (
<APMLink path="/settings/anomaly-detection">
<EuiButtonEmpty size="s" color="primary" iconType="inspect">
{ANOMALY_DETECTION_LINK_LABEL}
</EuiButtonEmpty>
{showAlert && (
<EuiToolTip position="bottom" content={getTooltipText(environment)}>
<EuiIcon type="alert" color="danger" />
</EuiToolTip>
)}
</APMLink>
);
}

function getTooltipText(environment?: string) {
if (!environment) {
return i18n.translate('xpack.apm.anomalyDetectionSetup.notEnabledText', {
defaultMessage: `Anomaly detection is not yet enabled. Click to continue setup.`,
});
}

return i18n.translate(
'xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText',
{
defaultMessage: `Anomaly detection is not yet enabled for the "{currentEnvironment}" environment. Click to continue setup.`,
values: { currentEnvironment: getEnvironmentLabel(environment) },
}
);
}

const ANOMALY_DETECTION_LINK_LABEL = i18n.translate(
'xpack.apm.anomalyDetectionSetup.linkLabel',
{ defaultMessage: `Anomaly detection` }
);
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ import { snakeCase } from 'lodash';
import { PromiseReturnType } from '../../../../observability/typings/common';
import { Setup } from '../helpers/setup_request';
import {
SERVICE_ENVIRONMENT,
TRANSACTION_DURATION,
PROCESSOR_EVENT,
} from '../../../common/elasticsearch_fieldnames';
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants';
import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';

export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType<
typeof createAnomalyDetectionJobs
Expand Down Expand Up @@ -89,9 +88,7 @@ async function createAnomalyDetectionJob({
filter: [
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
{ exists: { field: TRANSACTION_DURATION } },
environment === ENVIRONMENT_NOT_DEFINED
? ENVIRONMENT_NOT_DEFINED_FILTER
: { term: { [SERVICE_ENVIRONMENT]: environment } },
...getEnvironmentUiFilterES(environment),
],
},
},
Expand All @@ -109,13 +106,3 @@ async function createAnomalyDetectionJob({
],
});
}

const ENVIRONMENT_NOT_DEFINED_FILTER = {
bool: {
must_not: {
exists: {
field: SERVICE_ENVIRONMENT,
},
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,23 @@
import { getEnvironmentUiFilterES } from '../get_environment_ui_filter_es';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
import { SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames';
import { ESFilter } from '../../../../../typings/elasticsearch';

describe('getEnvironmentUiFilterES', () => {
it('should return undefined, when environment is undefined', () => {
it('should return empty array, when environment is undefined', () => {
const uiFilterES = getEnvironmentUiFilterES();
expect(uiFilterES).toBeUndefined();
expect(uiFilterES).toHaveLength(0);
});

it('should create a filter for a service environment', () => {
const uiFilterES = getEnvironmentUiFilterES('test') as ESFilter;
expect(uiFilterES).toHaveProperty(['term', SERVICE_ENVIRONMENT], 'test');
const uiFilterES = getEnvironmentUiFilterES('test');
expect(uiFilterES).toHaveLength(1);
expect(uiFilterES[0]).toHaveProperty(['term', SERVICE_ENVIRONMENT], 'test');
});

it('should create a filter for missing service environments', () => {
const uiFilterES = getEnvironmentUiFilterES(
ENVIRONMENT_NOT_DEFINED
) as ESFilter;
expect(uiFilterES).toHaveProperty(
const uiFilterES = getEnvironmentUiFilterES(ENVIRONMENT_NOT_DEFINED);
expect(uiFilterES).toHaveLength(1);
expect(uiFilterES[0]).toHaveProperty(
['bool', 'must_not', 'exists', 'field'],
SERVICE_ENVIRONMENT
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,12 @@ import { ESFilter } from '../../../../typings/elasticsearch';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values';
import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames';

export function getEnvironmentUiFilterES(
environment?: string
): ESFilter | undefined {
export function getEnvironmentUiFilterES(environment?: string): ESFilter[] {
if (!environment) {
return undefined;
return [];
}

if (environment === ENVIRONMENT_NOT_DEFINED) {
return {
bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } },
};
return [{ bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } } }];
}
return {
term: { [SERVICE_ENVIRONMENT]: environment },
};
return [{ term: { [SERVICE_ENVIRONMENT]: environment } }];
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,19 @@ export function getUiFiltersES(uiFilters: UIFilters) {
};
}) as ESFilter[];

// remove undefined items from list
const esFilters = [
getKueryUiFilterES(uiFilters.kuery),
getEnvironmentUiFilterES(uiFilters.environment),
]
.filter((filter) => !!filter)
.concat(mappedFilters) as ESFilter[];
...getKueryUiFilterES(uiFilters.kuery),
...getEnvironmentUiFilterES(uiFilters.environment),
].concat(mappedFilters) as ESFilter[];

return esFilters;
}

function getKueryUiFilterES(kuery?: string) {
if (!kuery) {
return;
return [];
}

const ast = esKuery.fromKueryExpression(kuery);
return esKuery.toElasticsearchQuery(ast) as ESFilter;
return [esKuery.toElasticsearchQuery(ast) as ESFilter];
}
Loading

0 comments on commit 3c9c187

Please sign in to comment.