diff --git a/x-pack/plugins/apm/common/environment_filter_values.ts b/x-pack/plugins/apm/common/environment_filter_values.ts
index 239378d0ea94a..38b6f480ca3d3 100644
--- a/x-pack/plugins/apm/common/environment_filter_values.ts
+++ b/x-pack/plugins/apm/common/environment_filter_values.ts
@@ -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;
+}
diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx
index f612ac0d383ef..bcc834fef6a6a 100644
--- a/x-pack/plugins/apm/public/components/app/Home/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx
@@ -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';
@@ -118,6 +119,9 @@ export function Home({ tab }: Props) {
+
+
+
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 2da3c12563104..98b4ae2f4b63f 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
@@ -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[];
@@ -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),
}));
@@ -155,10 +155,3 @@ export const AddEnvironments = ({
);
};
-
-const NOT_DEFINED_OPTION_LABEL = i18n.translate(
- 'xpack.apm.filter.environment.notDefinedLabel',
- {
- defaultMessage: 'Not defined',
- }
-);
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 83d19aa27ac11..34687e5a8094e 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
@@ -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> = [
@@ -32,14 +32,7 @@ const columns: Array> = [
'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',
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
new file mode 100644
index 0000000000000..88d15239b8fba
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx
@@ -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 (
+
+
+ {ANOMALY_DETECTION_LINK_LABEL}
+
+ {showAlert && (
+
+
+
+ )}
+
+ );
+}
+
+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` }
+);
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 e723393a24013..c387c5152b1c5 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
@@ -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
@@ -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),
],
},
},
@@ -109,13 +106,3 @@ async function createAnomalyDetectionJob({
],
});
}
-
-const ENVIRONMENT_NOT_DEFINED_FILTER = {
- bool: {
- must_not: {
- exists: {
- field: SERVICE_ENVIRONMENT,
- },
- },
- },
-};
diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts
index 0f0a11a868d6d..800f809727eb6 100644
--- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts
@@ -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
);
diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts
index 63d222a7fcb6e..87bc8dc968373 100644
--- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts
@@ -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 } }];
}
diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts
index b34d5535d58cc..c1405b44f2a8a 100644
--- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts
@@ -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];
}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
index be92bfe5a0099..dd5d19b620c51 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
@@ -9,7 +9,6 @@ import { ESFilter } from '../../../typings/elasticsearch';
import { rangeFilter } from '../../../common/utils/range_filter';
import {
PROCESSOR_EVENT,
- SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_DURATION,
TRANSACTION_TYPE,
@@ -22,7 +21,7 @@ import {
TRANSACTION_REQUEST,
TRANSACTION_PAGE_LOAD,
} from '../../../common/transaction_types';
-import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
+import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';
interface Options {
setup: Setup & SetupTimeRange;
@@ -43,30 +42,14 @@ export async function getServiceMapServiceNodeInfo({
}: Options & { serviceName: string; environment?: string }) {
const { start, end } = setup;
- const environmentNotDefinedFilter = {
- bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] },
- };
-
const filter: ESFilter[] = [
{ range: rangeFilter(start, end) },
{ term: { [SERVICE_NAME]: serviceName } },
+ ...getEnvironmentUiFilterES(environment),
];
- if (environment) {
- filter.push(
- environment === ENVIRONMENT_NOT_DEFINED
- ? environmentNotDefinedFilter
- : { term: { [SERVICE_ENVIRONMENT]: environment } }
- );
- }
-
const minutes = Math.abs((end - start) / (1000 * 60));
-
- const taskParams = {
- setup,
- minutes,
- filter,
- };
+ const taskParams = { setup, minutes, filter };
const [
errorMetrics,
@@ -97,11 +80,7 @@ async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) {
size: 0,
query: {
bool: {
- filter: filter.concat({
- term: {
- [PROCESSOR_EVENT]: 'error',
- },
- }),
+ filter: filter.concat({ term: { [PROCESSOR_EVENT]: 'error' } }),
},
},
track_total_hits: true,
@@ -134,11 +113,7 @@ async function getTransactionStats({
bool: {
filter: [
...filter,
- {
- term: {
- [PROCESSOR_EVENT]: 'transaction',
- },
- },
+ { term: { [PROCESSOR_EVENT]: 'transaction' } },
{
terms: {
[TRANSACTION_TYPE]: [
@@ -151,13 +126,7 @@ async function getTransactionStats({
},
},
track_total_hits: true,
- aggs: {
- duration: {
- avg: {
- field: TRANSACTION_DURATION,
- },
- },
- },
+ aggs: { duration: { avg: { field: TRANSACTION_DURATION } } },
},
};
const response = await client.search(params);
@@ -181,32 +150,16 @@ async function getCpuMetrics({
query: {
bool: {
filter: filter.concat([
- {
- term: {
- [PROCESSOR_EVENT]: 'metric',
- },
- },
- {
- exists: {
- field: METRIC_SYSTEM_CPU_PERCENT,
- },
- },
+ { term: { [PROCESSOR_EVENT]: 'metric' } },
+ { exists: { field: METRIC_SYSTEM_CPU_PERCENT } },
]),
},
},
- aggs: {
- avgCpuUsage: {
- avg: {
- field: METRIC_SYSTEM_CPU_PERCENT,
- },
- },
- },
+ aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } },
},
});
- return {
- avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null,
- };
+ return { avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null };
}
async function getMemoryMetrics({
@@ -220,31 +173,13 @@ async function getMemoryMetrics({
query: {
bool: {
filter: filter.concat([
- {
- term: {
- [PROCESSOR_EVENT]: 'metric',
- },
- },
- {
- exists: {
- field: METRIC_SYSTEM_FREE_MEMORY,
- },
- },
- {
- exists: {
- field: METRIC_SYSTEM_TOTAL_MEMORY,
- },
- },
+ { term: { [PROCESSOR_EVENT]: 'metric' } },
+ { exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
+ { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
]),
},
},
- aggs: {
- avgMemoryUsage: {
- avg: {
- script: percentMemoryUsedScript,
- },
- },
- },
+ aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } },
},
});
diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts
index 6da5d195cf194..6a8aaf8dca8a6 100644
--- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts
+++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts
@@ -29,14 +29,9 @@ export async function getDerivedServiceAnnotations({
const filter: ESFilter[] = [
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
{ term: { [SERVICE_NAME]: serviceName } },
+ ...getEnvironmentUiFilterES(environment),
];
- const environmentFilter = getEnvironmentUiFilterES(environment);
-
- if (environmentFilter) {
- filter.push(environmentFilter);
- }
-
const versions =
(
await client.search({
diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts
index 75aeb27ea2122..6e3ae0181ddee 100644
--- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts
+++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts
@@ -29,8 +29,6 @@ export async function getStoredAnnotations({
logger: Logger;
}): Promise {
try {
- const environmentFilter = getEnvironmentUiFilterES(environment);
-
const response: ESSearchResponse = (await apiCaller(
'search',
{
@@ -51,7 +49,7 @@ export async function getStoredAnnotations({
{ term: { 'annotation.type': 'deployment' } },
{ term: { tags: 'apm' } },
{ term: { [SERVICE_NAME]: serviceName } },
- ...(environmentFilter ? [environmentFilter] : []),
+ ...getEnvironmentUiFilterES(environment),
],
},
},