From f08bfcac89aec5eeb0584452ae6298b4ab910ef2 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Tue, 7 Jul 2020 02:43:59 -0700 Subject: [PATCH 01/13] Closes #69480 & #70419. - Adds anomaly detection integration to service maps backed by apm ML jobs per environment - Loads transaction stats and anomalies for each transaction types - Renders a selector in the popop to choose a transaction type to view stats --- x-pack/plugins/apm/common/service_map.ts | 8 +- .../app/ServiceMap/Popover/Contents.tsx | 8 +- .../app/ServiceMap/Popover/ServiceHealth.tsx | 168 ++++++++++++++++++ .../Popover/ServiceMetricFetcher.tsx | 46 ++++- .../ServiceMap/Popover/ServiceMetricList.tsx | 48 +---- .../app/ServiceMap/cytoscapeOptions.ts | 21 ++- .../server/lib/service_map/get_service_map.ts | 20 ++- .../get_service_map_service_node_info.ts | 53 ++++-- .../lib/service_map/get_top_anomalies.ts | 158 ++++++++++++++++ .../transform_service_map_responses.ts | 11 +- .../plugins/apm/server/routes/service_map.ts | 3 +- 11 files changed, 464 insertions(+), 80 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceHealth.tsx create mode 100644 x-pack/plugins/apm/server/lib/service_map/get_top_anomalies.ts diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 43f3585d0ebb2..2ca3ec9ba02ea 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -14,6 +14,7 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, SPAN_SUBTYPE, SPAN_TYPE, + TRANSACTION_TYPE, } from './elasticsearch_fieldnames'; export interface ServiceConnectionNode extends cytoscape.NodeDataDefinition { @@ -37,8 +38,11 @@ export interface Connection { export interface ServiceNodeMetrics { avgMemoryUsage: number | null; avgCpuUsage: number | null; - avgTransactionDuration: number | null; - avgRequestsPerMinute: number | null; + transactionMetrics: Array<{ + [TRANSACTION_TYPE]: string; + avgTransactionDuration: number | null; + avgRequestsPerMinute: number | null; + }>; avgErrorsPerMinute: number | null; } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 78779bdcc2052..358a43dd46524 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -68,8 +68,7 @@ export function Contents({ - {/* //TODO [APM ML] add service health stats here: - isService && ( + {/* isService && ( @@ -77,7 +76,10 @@ export function Contents({ )*/} {isService ? ( - + ) : ( )} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceHealth.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceHealth.tsx new file mode 100644 index 0000000000000..6b75f5925c508 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceHealth.tsx @@ -0,0 +1,168 @@ +/* + * 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, { useState, useEffect, useMemo } from 'react'; +import { isNumber } from 'lodash'; +import styled, { css } from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { EuiSelect } from '@elastic/eui'; +import { ServiceNodeMetrics } from '../../../../../common/service_map'; +import { ItemRow, ItemTitle, ItemDescription } from './ServiceMetricList'; +import { asDuration, asInteger, tpmUnit } from '../../../../utils/formatters'; +import { getSeverity } from '../../../../../common/ml_job_constants'; +import { getSeverityColor } from '../cytoscapeOptions'; +import { useTheme } from '../../../../hooks/useTheme'; +import { TRANSACTION_TYPE } from '../../../../../common/elasticsearch_fieldnames'; + +const AnomalyScore = styled.span<{ + readonly severityColor: string | undefined; +}>` + font-weight: bold; + ${(props) => + props.severityColor && + css` + color: ${props.severityColor}; + `} +`; + +const ActualValue = styled.span` + color: silver; +`; + +interface TransactionAnomaly { + [TRANSACTION_TYPE]: string; + anomaly_score: number; + actual_value: number; +} + +function getMaxAnomalyTransactionType(anomalies: TransactionAnomaly[] = []) { + const maxScore = Math.max( + ...anomalies.map(({ anomaly_score: anomalyScore }) => anomalyScore) + ); + const maxAnomaly = anomalies.find( + ({ anomaly_score: anomalyScore }) => anomalyScore === maxScore + ); + return maxAnomaly?.[TRANSACTION_TYPE] ?? anomalies[0]?.[TRANSACTION_TYPE]; +} + +interface Props { + anomalies: undefined | TransactionAnomaly[]; + transactionMetrics: ServiceNodeMetrics['transactionMetrics']; +} + +export function ServiceHealth({ anomalies, transactionMetrics }: Props) { + const theme = useTheme(); + const transactionTypes = useMemo( + () => + Array.isArray(transactionMetrics) + ? transactionMetrics.map( + (transactionTypeMetrics) => transactionTypeMetrics[TRANSACTION_TYPE] + ) + : [], + [transactionMetrics] + ); + const [selectedType, setSelectedType] = useState( + getMaxAnomalyTransactionType(anomalies) + ); + const selectedAnomaly = Array.isArray(anomalies) + ? anomalies.find((anomaly) => anomaly[TRANSACTION_TYPE] === selectedType) + : undefined; + const selectedTransactionMetrics = transactionMetrics.find( + (transactionTypeMetrics) => + transactionTypeMetrics[TRANSACTION_TYPE] === selectedType + ); + + useEffect(() => { + setSelectedType(getMaxAnomalyTransactionType(anomalies)); + }, [anomalies]); + + const listItems = []; + + if (selectedTransactionMetrics?.avgTransactionDuration) { + const { avgTransactionDuration } = selectedTransactionMetrics; + listItems.push({ + title: i18n.translate( + 'xpack.apm.serviceMap.avgTransDurationPopoverMetric', + { + defaultMessage: 'Trans. duration (avg.)', + } + ), + description: isNumber(avgTransactionDuration) + ? asDuration(avgTransactionDuration) + : null, + }); + } + + if (selectedAnomaly) { + listItems.push({ + title: i18n.translate('xpack.apm.serviceMap.anomalyScorePopoverMetric', { + defaultMessage: 'Anomaly score (max.)', + }), + description: ( + <> + + {asInteger(selectedAnomaly.anomaly_score)} + +   + + ({asDuration(selectedAnomaly.actual_value)}) + + + ), + }); + } + + if (selectedTransactionMetrics?.avgRequestsPerMinute) { + const { avgRequestsPerMinute } = selectedTransactionMetrics; + listItems.push({ + title: i18n.translate( + 'xpack.apm.serviceMap.avgReqPerMinutePopoverMetric', + { + defaultMessage: 'Req. per minute (avg.)', + } + ), + description: isNumber(avgRequestsPerMinute) + ? `${avgRequestsPerMinute.toFixed(2)} ${tpmUnit('request')}` + : null, + }); + } + + return ( + <> + ({ + value: type, + text: type, + }))} + onChange={(e) => { + setSelectedType(e.target.value); + }} + defaultValue={selectedType} + /> + + + {listItems.map( + ({ title, description }) => + description && ( + + {title} + {description} + + ) + )} + +
+ + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index 718e43984d7f3..995fb80304b77 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -5,23 +5,40 @@ */ import React from 'react'; +import { + EuiLoadingSpinner, + EuiFlexGroup, + EuiHorizontalRule, +} from '@elastic/eui'; import { ServiceNodeMetrics } from '../../../../../common/service_map'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ServiceMetricList } from './ServiceMetricList'; +import { ServiceHealth } from './ServiceHealth'; interface ServiceMetricFetcherProps { serviceName: string; + anomalies: + | undefined + | Array<{ + 'transaction.type': string; + anomaly_score: number; + actual_value: number; + }>; } export function ServiceMetricFetcher({ serviceName, + anomalies, }: ServiceMetricFetcherProps) { const { urlParams: { start, end, environment }, } = useUrlParams(); - const { data = {} as ServiceNodeMetrics, status } = useFetcher( + const { + data = ({ transactionMetrics: [] } as unknown) as ServiceNodeMetrics, + status, + } = useFetcher( (callApmApi) => { if (serviceName && start && end) { return callApmApi({ @@ -37,5 +54,30 @@ export function ServiceMetricFetcher({ ); const isLoading = status === 'loading'; - return ; + if (isLoading) { + return ; + } + + return ( + <> + + + + + ); +} + +function LoadingSpinner() { + return ( + + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index d66be9c61e42d..9fb3839f86461 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -4,25 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; import { ServiceNodeMetrics } from '../../../../../common/service_map'; -import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; - -function LoadingSpinner() { - return ( - - - - ); -} +import { asPercent } from '../../../../utils/formatters'; export const ItemRow = styled('tr')` line-height: 2; @@ -37,41 +24,14 @@ export const ItemDescription = styled('td')` text-align: right; `; -interface ServiceMetricListProps extends ServiceNodeMetrics { - isLoading: boolean; -} +type ServiceMetricListProps = ServiceNodeMetrics; export function ServiceMetricList({ - avgTransactionDuration, - avgRequestsPerMinute, avgErrorsPerMinute, avgCpuUsage, avgMemoryUsage, - isLoading, }: ServiceMetricListProps) { const listItems = [ - { - title: i18n.translate( - 'xpack.apm.serviceMap.avgTransDurationPopoverMetric', - { - defaultMessage: 'Trans. duration (avg.)', - } - ), - description: isNumber(avgTransactionDuration) - ? asDuration(avgTransactionDuration) - : null, - }, - { - title: i18n.translate( - 'xpack.apm.serviceMap.avgReqPerMinutePopoverMetric', - { - defaultMessage: 'Req. per minute (avg.)', - } - ), - description: isNumber(avgRequestsPerMinute) - ? `${avgRequestsPerMinute.toFixed(2)} ${tpmUnit('request')}` - : null, - }, { title: i18n.translate( 'xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric', @@ -100,9 +60,7 @@ export function ServiceMetricList({ }, ]; - return isLoading ? ( - - ) : ( + return ( {listItems.map( 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 5a2a3d2a2644e..03fa77f630a34 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -10,7 +10,7 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, } from '../../../../common/elasticsearch_fieldnames'; import { EuiTheme } from '../../../../../observability/public'; -import { severity } from '../../../../common/ml_job_constants'; +import { severity, getSeverity } from '../../../../common/ml_job_constants'; import { defaultIcon, iconForNode } from './icons'; export const popoverMinWidth = 280; @@ -29,12 +29,23 @@ export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) { } } +function getNodeSeverity(el: cytoscape.NodeSingular) { + const anomalies: Array<{ anomaly_score: number }> = el.data('anomalies'); + if (!Array.isArray(anomalies)) { + return; + } + const maxScore = Math.max( + ...anomalies.map(({ anomaly_score: score }) => score) + ); + return getSeverity(maxScore); +} + function getBorderColorFn( theme: EuiTheme ): cytoscape.Css.MapperFunction { return (el: cytoscape.NodeSingular) => { - const hasAnomalyDetectionJob = el.data('ml_job_id') !== undefined; - const nodeSeverity = el.data('anomaly_severity'); + const hasAnomalyDetectionJob = el.data('anomalies') !== undefined; + const nodeSeverity = getNodeSeverity(el); if (hasAnomalyDetectionJob) { return ( getSeverityColor(theme, nodeSeverity) || theme.eui.euiColorMediumShade @@ -51,7 +62,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction< cytoscape.NodeSingular, cytoscape.Css.LineStyle > = (el: cytoscape.NodeSingular) => { - const nodeSeverity = el.data('anomaly_severity'); + const nodeSeverity = getNodeSeverity(el); if (nodeSeverity === severity.critical) { return 'double'; } else { @@ -60,7 +71,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction< }; function getBorderWidth(el: cytoscape.NodeSingular) { - const nodeSeverity = el.data('anomaly_severity'); + const nodeSeverity = getNodeSeverity(el); if (nodeSeverity === severity.minor || nodeSeverity === severity.major) { return 4; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 4d488cd1a5509..1d3616a93fca2 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { chunk } from 'lodash'; +import { Logger } from 'kibana/server'; import { AGENT_NAME, SERVICE_ENVIRONMENT, @@ -16,11 +17,13 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { transformServiceMapResponses } from './transform_service_map_responses'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; +import { getTopAnomalies } from './get_top_anomalies'; export interface IEnvOptions { setup: Setup & SetupTimeRange; serviceName?: string; environment?: string; + logger: Logger; } async function getConnectionData({ @@ -132,13 +135,28 @@ export type ServicesResponse = PromiseReturnType; export type ServiceMapAPIResponse = PromiseReturnType; export async function getServiceMap(options: IEnvOptions) { - const [connectionData, servicesData] = await Promise.all([ + const { logger } = options; + let anomaliesPromise: ReturnType = Promise.resolve( + [] + ); + if (options.environment) { + anomaliesPromise = getTopAnomalies(options, options.environment).catch( + (error) => { + logger.warn(`Unable to retrieve anomalies for service maps.`); + logger.error(error); + return []; + } + ); + } + const [connectionData, servicesData, anomalies] = await Promise.all([ getConnectionData(options), getServicesData(options), + anomaliesPromise, ]); return transformServiceMapResponses({ ...connectionData, services: servicesData, + anomalies, }); } 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 e521efa687388..0514bc241eac1 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 @@ -12,6 +12,7 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_DURATION, + TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, @@ -65,7 +66,7 @@ export async function getServiceMapServiceNodeInfo({ return { ...errorMetrics, - ...transactionMetrics, + transactionMetrics, ...cpuMetrics, ...memoryMetrics, }; @@ -103,16 +104,19 @@ async function getTransactionMetrics({ setup, filter, minutes, -}: TaskParameters): Promise<{ - avgTransactionDuration: number | null; - avgRequestsPerMinute: number | null; -}> { +}: TaskParameters): Promise< + Array<{ + [TRANSACTION_TYPE]: string; + avgTransactionDuration: number | null; + avgRequestsPerMinute: number | null; + }> +> { const { indices, client } = setup; - const response = await client.search({ + const params = { index: indices['apm_oss.transactionIndices'], body: { - size: 1, + size: 0, query: { bool: { filter: filter.concat({ @@ -122,24 +126,35 @@ async function getTransactionMetrics({ }), }, }, - track_total_hits: true, aggs: { - duration: { - avg: { - field: TRANSACTION_DURATION, + transaction_types: { + terms: { + field: TRANSACTION_TYPE, + }, + aggs: { + duration: { + avg: { + field: TRANSACTION_DURATION, + }, + }, }, }, }, }, - }); - - return { - avgTransactionDuration: response.aggregations?.duration.value ?? null, - avgRequestsPerMinute: - response.hits.total.value > 0 - ? response.hits.total.value / minutes - : null, }; + const response = await client.search(params); + const transactionTypesBuckets = + response.aggregations?.transaction_types.buckets ?? []; + return transactionTypesBuckets.map((transactionTypesBucket) => { + const avgTransactionDuration = + transactionTypesBucket?.duration?.value ?? null; + const docCount = transactionTypesBucket?.doc_count ?? 0; + return { + [TRANSACTION_TYPE]: transactionTypesBucket.key as string, + avgTransactionDuration, + avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, + }; + }); } async function getCpuMetrics({ diff --git a/x-pack/plugins/apm/server/lib/service_map/get_top_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_top_anomalies.ts new file mode 100644 index 0000000000000..667564494aa97 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/get_top_anomalies.ts @@ -0,0 +1,158 @@ +/* + * 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 { Logger } from 'kibana/server'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { PromiseReturnType } from '../../../typings/common'; + +export type TopAnomaliesResponse = PromiseReturnType; + +export async function getTopAnomalies( + { + setup, + logger, + }: { + setup: Setup & SetupTimeRange; + logger: Logger; + }, + environment: string +) { + const { ml, start, end } = setup; + + if (!ml) { + logger.warn('Anomaly detection plugin is not available.'); + return []; + } + const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if (!mlCapabilities.mlFeatureEnabledInSpace) { + logger.warn('Anomaly detection feature is not enabled for the space.'); + return []; + } + if (!mlCapabilities.isPlatinumOrTrialLicense) { + logger.warn( + 'Unable to create anomaly detection jobs due to insufficient license.' + ); + return []; + } + + const mlJobId = await getMLJobId(ml, environment); + + const params = { + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + result_type: 'record', + }, + }, + { + term: { + job_id: mlJobId, + }, + }, + { + range: { + timestamp: { + gte: start, + lte: end, + format: 'epoch_millis', + }, + }, + }, + ], + }, + }, + aggs: { + service_name: { + terms: { + field: 'partition_field_value', + }, + aggs: { + transaction_type: { + terms: { + field: 'by_field_value', + }, + aggs: { + top_score: { + top_metrics: { + metrics: [ + { + field: 'actual', + }, + ], + sort: { + record_score: 'desc', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await ml.mlSystem.mlAnomalySearch(params); + return transformTopAnomalies(response as TopAnomaliesAggResponse); +} + +interface TopAnomaliesAggResponse { + aggregations: { + service_name: { + buckets: Array<{ + key: string; + transaction_type: { + buckets: Array<{ + key: string; + top_score: { + top: [{ sort: [number]; metrics: { actual: number } }]; + }; + }>; + }; + }>; + }; + }; +} + +function transformTopAnomalies(response: TopAnomaliesAggResponse) { + const services = response.aggregations.service_name.buckets.map( + ({ key: serviceName, transaction_type: transactionTypeAgg }) => { + return { + 'service.name': serviceName, + anomalies: transactionTypeAgg.buckets.map( + ({ key: transactionType, top_score: topScoreAgg }) => { + return { + 'transaction.type': transactionType, + anomaly_score: topScoreAgg.top[0].sort[0], + actual_value: topScoreAgg.top[0].metrics.actual, + }; + } + ), + }; + } + ); + return services; +} + +export async function getMLJobId( + ml: Required['ml'], + environment: string +) { + const response = await ml.anomalyDetectors.jobs('apm'); + const matchingMLJobs = response.jobs.filter( + (anomalyDetectionJob) => + // TODO [ML] remove this since job_tags is defined in another branch + // @ts-ignore + anomalyDetectionJob.custom_settings?.job_tags?.environment === environment + ); + if (matchingMLJobs.length === 0) { + throw new Error(`ML job Not Found for environment "${environment}".`); + } + return matchingMLJobs[0].job_id; +} diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 2e394f44b25b1..74851582328c7 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -18,6 +18,7 @@ import { ExternalConnectionNode, } from '../../../common/service_map'; import { ConnectionsResponse, ServicesResponse } from './get_service_map'; +import { TopAnomaliesResponse } from './get_top_anomalies'; function getConnectionNodeId(node: ConnectionNode): string { if ('span.destination.service.resource' in node) { @@ -63,10 +64,11 @@ export function getServiceNodes(allNodes: ConnectionNode[]) { export type ServiceMapResponse = ConnectionsResponse & { services: ServicesResponse; + anomalies: TopAnomaliesResponse; }; export function transformServiceMapResponses(response: ServiceMapResponse) { - const { discoveredServices, services, connections } = response; + const { discoveredServices, services, connections, anomalies } = response; const allNodes = getAllNodes(services, connections); const serviceNodes = getServiceNodes(allNodes); @@ -104,6 +106,10 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { (serviceNode) => serviceNode[SERVICE_NAME] === serviceName ); + const matchedAnomalies = anomalies.find( + (anomalyData) => anomalyData[SERVICE_NAME] === serviceName + ); + if (matchedServiceNodes.length) { return { ...map, @@ -113,7 +119,8 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { }, ...matchedServiceNodes.map((serviceNode) => pickBy(serviceNode, identity) - ) + ), + matchedAnomalies ), }; } diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index a3e2f708b0b22..50123131a42e7 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -37,11 +37,12 @@ export const serviceMapRoute = createRoute(() => ({ } context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME); + const logger = context.logger; const setup = await setupRequest(context, request); const { query: { serviceName, environment }, } = context.params; - return getServiceMap({ setup, serviceName, environment }); + return getServiceMap({ setup, serviceName, environment, logger }); }, })); From 05ec94aaff5f987f58c1b686183f8d93155a5492 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Tue, 7 Jul 2020 23:44:20 -0700 Subject: [PATCH 02/13] - implements original anomaly detection integration design for service maps popover - only aggregates transaction KPIs and anomaly scores for transaction.type = "request" or "page-load" - supports environment filter 'All' option to display data from all APM anomaly detection jobs - handle case where popover metrics don't exist for services outside the current environment filter --- .../plugins/apm/common/anomaly_detection.ts | 14 ++ x-pack/plugins/apm/common/service_map.ts | 8 +- .../ServiceMap/Popover/AnomalyDetection.tsx | 158 +++++++++++++++ .../app/ServiceMap/Popover/Contents.tsx | 12 +- .../app/ServiceMap/Popover/ServiceHealth.tsx | 168 ---------------- .../Popover/ServiceMetricFetcher.tsx | 67 +++++-- .../ServiceMap/Popover/ServiceMetricList.tsx | 27 ++- .../app/ServiceMap/cytoscapeOptions.ts | 15 +- .../Links/MachineLearningLinks/MLJobLink.tsx | 31 ++- .../useTimeSeriesExplorerHref.ts | 51 +++++ .../create_anomaly_detection_jobs.ts | 10 +- .../lib/service_map/get_max_anomalies.ts | 188 ++++++++++++++++++ .../server/lib/service_map/get_service_map.ts | 21 +- .../get_service_map_service_node_info.ts | 85 ++++---- .../lib/service_map/get_top_anomalies.ts | 158 --------------- .../transform_service_map_responses.ts | 33 ++- 16 files changed, 607 insertions(+), 439 deletions(-) create mode 100644 x-pack/plugins/apm/common/anomaly_detection.ts create mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceHealth.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts create mode 100644 x-pack/plugins/apm/server/lib/service_map/get_max_anomalies.ts delete mode 100644 x-pack/plugins/apm/server/lib/service_map/get_top_anomalies.ts diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts new file mode 100644 index 0000000000000..7b48230075dca --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -0,0 +1,14 @@ +/* + * 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 { TRANSACTION_TYPE } from './elasticsearch_fieldnames'; + +export interface MaxAnomaly { + [TRANSACTION_TYPE]?: string; + anomaly_score?: number; + actual_value?: number; + job_id?: string; +} diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 2ca3ec9ba02ea..8de530be04a63 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -14,7 +14,6 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, SPAN_SUBTYPE, SPAN_TYPE, - TRANSACTION_TYPE, } from './elasticsearch_fieldnames'; export interface ServiceConnectionNode extends cytoscape.NodeDataDefinition { @@ -36,13 +35,14 @@ export interface Connection { } export interface ServiceNodeMetrics { + hasEnvironmentData: boolean; + environmentsWithData: string[]; avgMemoryUsage: number | null; avgCpuUsage: number | null; - transactionMetrics: Array<{ - [TRANSACTION_TYPE]: string; + transactionKPIs: { avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; - }>; + }; avgErrorsPerMinute: number | null; } 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 new file mode 100644 index 0000000000000..b2ffa8f55372a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -0,0 +1,158 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiIconTip, + EuiHealth, +} from '@elastic/eui'; +import { useTheme } from '../../../../hooks/useTheme'; +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_TYPE } from '../../../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; +import { MaxAnomaly } from '../../../../../common/anomaly_detection'; + +const HealthStatusTitle = styled(EuiTitle)` + display: inline; + text-transform: uppercase; +`; + +const VerticallyCentered = styled.div` + display: flex; + align-items: center; +`; + +const SubduedText = styled.span` + color: ${({ theme }) => theme.eui.euiTextSubduedColor}; +`; + +const EnableText = styled.section` + color: ${({ theme }) => theme.eui.euiTextSubduedColor}; + line-height: 1.4; + font-size: ${fontSize}; + width: ${px(popoverWidth)}; +`; + +export const ContentLine = styled.section` + line-height: 2; +`; + +interface Props { + serviceName: string; + maxAnomaly: MaxAnomaly | undefined; +} +export function AnomalyDetection({ serviceName, maxAnomaly }: Props) { + const theme = useTheme(); + + const anomalyScore = maxAnomaly?.anomaly_score; + const anomalySeverity = getSeverity(anomalyScore); + const actualValue = maxAnomaly?.actual_value; + const mlJobId = maxAnomaly?.job_id; + const transactionType = maxAnomaly?.[TRANSACTION_TYPE]; + const hasAnomalyDetectionScore = anomalyScore !== undefined; + + return ( + <> +
+ +

{ANOMALY_DETECTION_TITLE}

+
+   + + {!mlJobId && {ANOMALY_DETECTION_DISABLED_TEXT}} +
+ {hasAnomalyDetectionScore && ( + + + + + + {ANOMALY_DETECTION_SCORE_METRIC} + + + +
+ {getDisplayedAnomalyScore(anomalyScore as number)} + {actualValue && ( +  ({asDuration(actualValue)}) + )} +
+
+
+
+ )} + {mlJobId && !hasAnomalyDetectionScore && ( + {ANOMALY_DETECTION_NO_DATA_TEXT} + )} + {mlJobId && ( + + + {ANOMALY_DETECTION_LINK} + + + )} + + ); +} + +function getDisplayedAnomalyScore(score: number) { + if (score > 0 && score < 1) { + return '< 1'; + } + return asInteger(score); +} + +const ANOMALY_DETECTION_TITLE = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverTitle', + { defaultMessage: 'Anomaly Detection' } +); + +const ANOMALY_DETECTION_TOOLTIP = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverTooltip', + { + defaultMessage: + 'Service health indicators are powered by the anomaly detection feature in machine learning', + } +); + +const ANOMALY_DETECTION_SCORE_METRIC = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric', + { defaultMessage: 'Score (max.)' } +); + +const ANOMALY_DETECTION_LINK = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverLink', + { defaultMessage: 'View anomalies' } +); + +const ANOMALY_DETECTION_DISABLED_TEXT = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverDisabled', + { + defaultMessage: + 'Display service health indicators by enabling anomaly detection in APM settings.', + } +); + +const ANOMALY_DETECTION_NO_DATA_TEXT = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverNoData', + { + defaultMessage: `We couldn't find an anomaly score within the selected time range. See details in the anomaly explorer.`, + } +); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 358a43dd46524..3a6057f26550d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -15,7 +15,7 @@ import React, { MouseEvent } from 'react'; import { Buttons } from './Buttons'; import { Info } from './Info'; import { ServiceMetricFetcher } from './ServiceMetricFetcher'; -import { popoverMinWidth } from '../cytoscapeOptions'; +import { popoverWidth } from '../cytoscapeOptions'; interface ContentsProps { isService: boolean; @@ -60,7 +60,7 @@ export function Contents({ @@ -68,17 +68,11 @@ export function Contents({ - {/* isService && ( - - - - - )*/} {isService ? ( ) : ( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceHealth.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceHealth.tsx deleted file mode 100644 index 6b75f5925c508..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceHealth.tsx +++ /dev/null @@ -1,168 +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. - */ - -import React, { useState, useEffect, useMemo } from 'react'; -import { isNumber } from 'lodash'; -import styled, { css } from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { EuiSelect } from '@elastic/eui'; -import { ServiceNodeMetrics } from '../../../../../common/service_map'; -import { ItemRow, ItemTitle, ItemDescription } from './ServiceMetricList'; -import { asDuration, asInteger, tpmUnit } from '../../../../utils/formatters'; -import { getSeverity } from '../../../../../common/ml_job_constants'; -import { getSeverityColor } from '../cytoscapeOptions'; -import { useTheme } from '../../../../hooks/useTheme'; -import { TRANSACTION_TYPE } from '../../../../../common/elasticsearch_fieldnames'; - -const AnomalyScore = styled.span<{ - readonly severityColor: string | undefined; -}>` - font-weight: bold; - ${(props) => - props.severityColor && - css` - color: ${props.severityColor}; - `} -`; - -const ActualValue = styled.span` - color: silver; -`; - -interface TransactionAnomaly { - [TRANSACTION_TYPE]: string; - anomaly_score: number; - actual_value: number; -} - -function getMaxAnomalyTransactionType(anomalies: TransactionAnomaly[] = []) { - const maxScore = Math.max( - ...anomalies.map(({ anomaly_score: anomalyScore }) => anomalyScore) - ); - const maxAnomaly = anomalies.find( - ({ anomaly_score: anomalyScore }) => anomalyScore === maxScore - ); - return maxAnomaly?.[TRANSACTION_TYPE] ?? anomalies[0]?.[TRANSACTION_TYPE]; -} - -interface Props { - anomalies: undefined | TransactionAnomaly[]; - transactionMetrics: ServiceNodeMetrics['transactionMetrics']; -} - -export function ServiceHealth({ anomalies, transactionMetrics }: Props) { - const theme = useTheme(); - const transactionTypes = useMemo( - () => - Array.isArray(transactionMetrics) - ? transactionMetrics.map( - (transactionTypeMetrics) => transactionTypeMetrics[TRANSACTION_TYPE] - ) - : [], - [transactionMetrics] - ); - const [selectedType, setSelectedType] = useState( - getMaxAnomalyTransactionType(anomalies) - ); - const selectedAnomaly = Array.isArray(anomalies) - ? anomalies.find((anomaly) => anomaly[TRANSACTION_TYPE] === selectedType) - : undefined; - const selectedTransactionMetrics = transactionMetrics.find( - (transactionTypeMetrics) => - transactionTypeMetrics[TRANSACTION_TYPE] === selectedType - ); - - useEffect(() => { - setSelectedType(getMaxAnomalyTransactionType(anomalies)); - }, [anomalies]); - - const listItems = []; - - if (selectedTransactionMetrics?.avgTransactionDuration) { - const { avgTransactionDuration } = selectedTransactionMetrics; - listItems.push({ - title: i18n.translate( - 'xpack.apm.serviceMap.avgTransDurationPopoverMetric', - { - defaultMessage: 'Trans. duration (avg.)', - } - ), - description: isNumber(avgTransactionDuration) - ? asDuration(avgTransactionDuration) - : null, - }); - } - - if (selectedAnomaly) { - listItems.push({ - title: i18n.translate('xpack.apm.serviceMap.anomalyScorePopoverMetric', { - defaultMessage: 'Anomaly score (max.)', - }), - description: ( - <> - - {asInteger(selectedAnomaly.anomaly_score)} - -   - - ({asDuration(selectedAnomaly.actual_value)}) - - - ), - }); - } - - if (selectedTransactionMetrics?.avgRequestsPerMinute) { - const { avgRequestsPerMinute } = selectedTransactionMetrics; - listItems.push({ - title: i18n.translate( - 'xpack.apm.serviceMap.avgReqPerMinutePopoverMetric', - { - defaultMessage: 'Req. per minute (avg.)', - } - ), - description: isNumber(avgRequestsPerMinute) - ? `${avgRequestsPerMinute.toFixed(2)} ${tpmUnit('request')}` - : null, - }); - } - - return ( - <> - ({ - value: type, - text: type, - }))} - onChange={(e) => { - setSelectedType(e.target.value); - }} - defaultValue={selectedType} - /> -
- - {listItems.map( - ({ title, description }) => - description && ( - - {title} - {description} - - ) - )} - -
- - ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index 995fb80304b77..1fb43494f5212 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -9,34 +9,33 @@ import { EuiLoadingSpinner, EuiFlexGroup, EuiHorizontalRule, + EuiCallOut, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ServiceNodeMetrics } from '../../../../../common/service_map'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ServiceMetricList } from './ServiceMetricList'; -import { ServiceHealth } from './ServiceHealth'; +import { AnomalyDetection } from './AnomalyDetection'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; +import { ALL_OPTION } from '../../../../hooks/useEnvironments'; +import { MaxAnomaly } from '../../../../../common/anomaly_detection'; interface ServiceMetricFetcherProps { serviceName: string; - anomalies: - | undefined - | Array<{ - 'transaction.type': string; - anomaly_score: number; - actual_value: number; - }>; + maxAnomaly: MaxAnomaly | undefined; } export function ServiceMetricFetcher({ serviceName, - anomalies, + maxAnomaly, }: ServiceMetricFetcherProps) { const { urlParams: { start, end, environment }, } = useUrlParams(); const { - data = ({ transactionMetrics: [] } as unknown) as ServiceNodeMetrics, + data = { transactionKPIs: {} } as ServiceNodeMetrics, status, } = useFetcher( (callApmApi) => { @@ -52,18 +51,45 @@ export function ServiceMetricFetcher({ preservePreviousData: false, } ); - const isLoading = status === 'loading'; + + const isLoading = + status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; if (isLoading) { return ; } + if (environment && !data.hasEnvironmentData) { + return ( + + {i18n.translate( + 'xpack.apm.serviceMap.popoverMetrics.noEnvironmentDataCallout.text', + { + defaultMessage: `This service belongs to an environment outside of the currently selected environment ({currentEnvironment}). Change the environment filter to [{environmentsWithData}] to see info on this service.`, + values: { + currentEnvironment: getEnvironmentLabel(environment), + environmentsWithData: [ + ALL_OPTION.text, + ...data.environmentsWithData.map(getEnvironmentLabel), + ].join(', '), + }, + } + )} + + ); + } return ( <> - + @@ -81,3 +107,12 @@ function LoadingSpinner() { ); } + +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/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index 9fb3839f86461..bb77e7606b1f4 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -9,7 +9,7 @@ import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; import { ServiceNodeMetrics } from '../../../../../common/service_map'; -import { asPercent } from '../../../../utils/formatters'; +import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; export const ItemRow = styled('tr')` line-height: 2; @@ -30,8 +30,33 @@ export function ServiceMetricList({ avgErrorsPerMinute, avgCpuUsage, avgMemoryUsage, + transactionKPIs, }: ServiceMetricListProps) { const listItems = [ + { + title: i18n.translate( + 'xpack.apm.serviceMap.avgTransDurationPopoverMetric', + { + defaultMessage: 'Trans. duration (avg.)', + } + ), + description: isNumber(transactionKPIs.avgTransactionDuration) + ? asDuration(transactionKPIs.avgTransactionDuration) + : null, + }, + { + title: i18n.translate( + 'xpack.apm.serviceMap.avgReqPerMinutePopoverMetric', + { + defaultMessage: 'Req. per minute (avg.)', + } + ), + description: isNumber(transactionKPIs.avgRequestsPerMinute) + ? `${transactionKPIs.avgRequestsPerMinute.toFixed(2)} ${tpmUnit( + 'request' + )}` + : null, + }, { title: i18n.translate( 'xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric', 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 03fa77f630a34..ba0fef7dfb443 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -12,8 +12,9 @@ import { import { EuiTheme } from '../../../../../observability/public'; import { severity, getSeverity } from '../../../../common/ml_job_constants'; import { defaultIcon, iconForNode } from './icons'; +import { MaxAnomaly } from '../../../../common/anomaly_detection'; -export const popoverMinWidth = 280; +export const popoverWidth = 280; export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) { switch (nodeSeverity) { @@ -30,21 +31,15 @@ export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) { } function getNodeSeverity(el: cytoscape.NodeSingular) { - const anomalies: Array<{ anomaly_score: number }> = el.data('anomalies'); - if (!Array.isArray(anomalies)) { - return; - } - const maxScore = Math.max( - ...anomalies.map(({ anomaly_score: score }) => score) - ); - return getSeverity(maxScore); + const maxAnomaly: MaxAnomaly | undefined = el.data('maxAnomaly'); + return getSeverity(maxAnomaly?.anomaly_score); } function getBorderColorFn( theme: EuiTheme ): cytoscape.Css.MapperFunction { return (el: cytoscape.NodeSingular) => { - const hasAnomalyDetectionJob = el.data('anomalies') !== undefined; + const hasAnomalyDetectionJob = el.data('maxAnomaly') !== undefined; const nodeSeverity = getNodeSeverity(el); if (hasAnomalyDetectionJob) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx index 1e1f9ea5f23b7..f3c5b49287293 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx @@ -5,24 +5,35 @@ */ import React from 'react'; -import { MLLink } from './MLLink'; +import { EuiLink } from '@elastic/eui'; +import { useTimeSeriesExplorerHref } from './useTimeSeriesExplorerHref'; interface Props { jobId: string; external?: boolean; + serviceName?: string; + transactionType?: string; } -export const MLJobLink: React.FC = (props) => { - const query = { - ml: { jobIds: [props.jobId] }, - }; +export const MLJobLink: React.FC = ({ + jobId, + serviceName, + transactionType, + external, + children, +}) => { + const href = useTimeSeriesExplorerHref({ + jobId, + serviceName, + transactionType, + }); return ( - ); }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts new file mode 100644 index 0000000000000..8aefd2ca7bbb7 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts @@ -0,0 +1,51 @@ +/* + * 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 url from 'url'; +import rison from 'rison-node'; +import { useLocation } from '../../../../hooks/useLocation'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { getTimepickerRisonData } from '../rison_helpers'; + +export function useTimeSeriesExplorerHref({ + jobId, + serviceName, + transactionType, +}: { + jobId: string; + serviceName?: string; + transactionType?: string; +}) { + const { core } = useApmPluginContext(); + const location = useLocation(); + + return url.format({ + pathname: core.http.basePath.prepend('/app/ml'), + hash: url.format({ + pathname: '/timeseriesexplorer', + query: { + _g: rison.encode({ + ml: { + jobIds: [jobId], + }, + ...getTimepickerRisonData(location.search), + }), + ...(serviceName && transactionType + ? { + _a: rison.encode({ + mlTimeSeriesExplorer: { + entities: { + 'service.name': serviceName, + 'transaction.type': transactionType, + }, + }, + }), + } + : null), + }, + }), + }); +} 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 406097805775d..71afc5733becb 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 @@ -6,6 +6,7 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; +import { snakeCase } from 'lodash'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; import { @@ -78,13 +79,12 @@ async function createAnomalyDetectionJob({ environment: string; indexPatternName?: string | undefined; }) { - const convertedEnvironmentName = convertToMLIdentifier(environment); const randomToken = uuid().substr(-4); return ml.modules.setup({ moduleId: ML_MODULE_ID_APM_TRANSACTION, - prefix: `${ML_GROUP_NAME_APM}-${convertedEnvironmentName}-${randomToken}-`, - groups: [ML_GROUP_NAME_APM, convertedEnvironmentName], + prefix: `${ML_GROUP_NAME_APM}-${snakeCase(environment)}-${randomToken}-`, + groups: [ML_GROUP_NAME_APM], indexPatternName, query: { bool: { @@ -117,7 +117,3 @@ const ENVIRONMENT_NOT_DEFINED_FILTER = { }, }, }; - -export function convertToMLIdentifier(value: string) { - return value.replace(/\s+/g, '_').toLowerCase(); -} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_max_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_max_anomalies.ts new file mode 100644 index 0000000000000..b481d7649b39b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/get_max_anomalies.ts @@ -0,0 +1,188 @@ +/* + * 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 { Logger } from 'kibana/server'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { PromiseReturnType } from '../../../typings/common'; +import { ML_GROUP_NAME_APM } from '../anomaly_detection/create_anomaly_detection_jobs'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../common/transaction_types'; +import { + TRANSACTION_TYPE, + SERVICE_NAME, +} from '../../../common/elasticsearch_fieldnames'; +import { MaxAnomaly } from '../../../common/anomaly_detection'; + +const DEFAULT_VALUE = { + mlJobIds: [], + maxAnomalies: [], +}; + +export type MaxAnomaliesResponse = PromiseReturnType; + +export async function getMaxAnomalies({ + setup, + logger, + environment, +}: { + setup: Setup & SetupTimeRange; + logger: Logger; + environment?: string; +}) { + const { ml, start, end } = setup; + + if (!ml) { + logger.warn('Anomaly detection plugin is not available.'); + return DEFAULT_VALUE; + } + const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if (!mlCapabilities.mlFeatureEnabledInSpace) { + logger.warn('Anomaly detection feature is not enabled for the space.'); + return DEFAULT_VALUE; + } + if (!mlCapabilities.isPlatinumOrTrialLicense) { + logger.warn( + 'Unable to create anomaly detection jobs due to insufficient license.' + ); + return DEFAULT_VALUE; + } + + let mlJobIds: string[] = []; + try { + mlJobIds = await getMLJobIds(ml, environment); + } catch (error) { + logger.error(error); + return DEFAULT_VALUE; + } + + const params = { + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + result_type: 'record', + }, + }, + { + terms: { + job_id: mlJobIds, + }, + }, + { + range: { + timestamp: { + gte: start, + lte: end, + format: 'epoch_millis', + }, + }, + }, + { + terms: { + by_field_value: [TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD], + }, + }, + ], + }, + }, + aggs: { + service_name: { + terms: { + field: 'partition_field_value', + }, + aggs: { + top_score: { + top_hits: { + sort: { + record_score: 'desc', + }, + _source: { + includes: ['actual', 'job_id', 'by_field_value'], + }, + size: 1, + }, + }, + }, + }, + }, + }, + }; + const response = await ml.mlSystem.mlAnomalySearch(params); + return { + mlJobIds, + maxAnomalies: transformResponseToMaxAnomalies( + response as MaxAnomaliesAggResponse + ), + }; +} + +interface MaxAnomaliesAggResponse { + aggregations: { + service_name: { + buckets: Array<{ + key: string; + top_score: { + hits: { + hits: Array<{ + sort: [number]; + _source: { + actual: [number]; + job_id: string; + by_field_value: string; + }; + }>; + }; + }; + }>; + }; + }; +} + +function transformResponseToMaxAnomalies( + response: MaxAnomaliesAggResponse +): Array<{ + [SERVICE_NAME]: string; + maxAnomaly: MaxAnomaly; +}> { + const services = response.aggregations.service_name.buckets.map( + ({ key: serviceName, top_score: topScoreAgg }) => { + return { + [SERVICE_NAME]: serviceName, + maxAnomaly: { + [TRANSACTION_TYPE]: topScoreAgg.hits.hits[0]?._source?.by_field_value, + anomaly_score: topScoreAgg.hits.hits[0]?.sort?.[0], + actual_value: topScoreAgg.hits.hits[0]?._source?.actual?.[0], + job_id: topScoreAgg.hits.hits[0]?._source?.job_id, + }, + }; + } + ); + return services; +} + +export async function getMLJobIds( + ml: Required['ml'], + environment?: string +) { + const response = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM); + const apmAnomalyDetectionMLJobs = response.jobs.filter( + (job) => job.custom_settings?.job_tags?.environment + ); + if (environment) { + const matchingMLJob = apmAnomalyDetectionMLJobs.find( + (job) => job.custom_settings?.job_tags?.environment === environment + ); + if (!matchingMLJob) { + throw new Error(`ML job Not Found for environment "${environment}".`); + } + return [matchingMLJob.job_id]; + } + return apmAnomalyDetectionMLJobs.map((job) => job.job_id); +} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 1d3616a93fca2..090c0eba1c044 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -17,7 +17,7 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { transformServiceMapResponses } from './transform_service_map_responses'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; -import { getTopAnomalies } from './get_top_anomalies'; +import { getMaxAnomalies, MaxAnomaliesResponse } from './get_max_anomalies'; export interface IEnvOptions { setup: Setup & SetupTimeRange; @@ -136,18 +136,13 @@ export type ServiceMapAPIResponse = PromiseReturnType; export async function getServiceMap(options: IEnvOptions) { const { logger } = options; - let anomaliesPromise: ReturnType = Promise.resolve( - [] - ); - if (options.environment) { - anomaliesPromise = getTopAnomalies(options, options.environment).catch( - (error) => { - logger.warn(`Unable to retrieve anomalies for service maps.`); - logger.error(error); - return []; - } - ); - } + const anomaliesPromise: Promise = getMaxAnomalies( + options + ).catch((error) => { + logger.warn(`Unable to retrieve anomalies for service maps.`); + logger.error(error); + return { mlJobIds: [], maxAnomalies: [] }; + }); const [connectionData, servicesData, anomalies] = await Promise.all([ getConnectionData(options), getServicesData(options), 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 0514bc241eac1..50814bce2988b 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 @@ -18,6 +18,11 @@ import { METRIC_SYSTEM_TOTAL_MEMORY, } from '../../../common/elasticsearch_fieldnames'; import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; +import { + TRANSACTION_REQUEST, + TRANSACTION_PAGE_LOAD, +} from '../../../common/transaction_types'; +import { getAllEnvironments } from '../environments/get_all_environments'; interface Options { setup: Setup & SetupTimeRange; @@ -53,20 +58,31 @@ export async function getServiceMapServiceNodeInfo({ }; const [ + environmentsWithData, errorMetrics, - transactionMetrics, + transactionKPIs, cpuMetrics, memoryMetrics, ] = await Promise.all([ + getAllEnvironments({ + serviceName, + setup, + includeMissing: true, + }), getErrorMetrics(taskParams), - getTransactionMetrics(taskParams), + getTransactionKPIs(taskParams), getCpuMetrics(taskParams), getMemoryMetrics(taskParams), ]); + const hasEnvironmentData = + !environment || environmentsWithData.includes(environment); + return { + hasEnvironmentData, + environmentsWithData, ...errorMetrics, - transactionMetrics, + transactionKPIs, ...cpuMetrics, ...memoryMetrics, }; @@ -100,17 +116,14 @@ async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) { }; } -async function getTransactionMetrics({ +async function getTransactionKPIs({ setup, filter, minutes, -}: TaskParameters): Promise< - Array<{ - [TRANSACTION_TYPE]: string; - avgTransactionDuration: number | null; - avgRequestsPerMinute: number | null; - }> -> { +}: TaskParameters): Promise<{ + avgTransactionDuration: number | null; + avgRequestsPerMinute: number | null; +}> { const { indices, client } = setup; const params = { @@ -119,42 +132,40 @@ async function getTransactionMetrics({ size: 0, query: { bool: { - filter: filter.concat({ - term: { - [PROCESSOR_EVENT]: 'transaction', + filter: [ + ...filter, + { + term: { + [PROCESSOR_EVENT]: 'transaction', + }, }, - }), + { + terms: { + [TRANSACTION_TYPE]: [ + TRANSACTION_REQUEST, + TRANSACTION_PAGE_LOAD, + ], + }, + }, + ], }, }, + track_total_hits: true, aggs: { - transaction_types: { - terms: { - field: TRANSACTION_TYPE, - }, - aggs: { - duration: { - avg: { - field: TRANSACTION_DURATION, - }, - }, + duration: { + avg: { + field: TRANSACTION_DURATION, }, }, }, }, }; const response = await client.search(params); - const transactionTypesBuckets = - response.aggregations?.transaction_types.buckets ?? []; - return transactionTypesBuckets.map((transactionTypesBucket) => { - const avgTransactionDuration = - transactionTypesBucket?.duration?.value ?? null; - const docCount = transactionTypesBucket?.doc_count ?? 0; - return { - [TRANSACTION_TYPE]: transactionTypesBucket.key as string, - avgTransactionDuration, - avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, - }; - }); + const docCount = response.hits.total.value; + return { + avgTransactionDuration: response.aggregations?.duration.value ?? null, + avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, + }; } async function getCpuMetrics({ diff --git a/x-pack/plugins/apm/server/lib/service_map/get_top_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_top_anomalies.ts deleted file mode 100644 index 667564494aa97..0000000000000 --- a/x-pack/plugins/apm/server/lib/service_map/get_top_anomalies.ts +++ /dev/null @@ -1,158 +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. - */ -import { Logger } from 'kibana/server'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { PromiseReturnType } from '../../../typings/common'; - -export type TopAnomaliesResponse = PromiseReturnType; - -export async function getTopAnomalies( - { - setup, - logger, - }: { - setup: Setup & SetupTimeRange; - logger: Logger; - }, - environment: string -) { - const { ml, start, end } = setup; - - if (!ml) { - logger.warn('Anomaly detection plugin is not available.'); - return []; - } - const mlCapabilities = await ml.mlSystem.mlCapabilities(); - if (!mlCapabilities.mlFeatureEnabledInSpace) { - logger.warn('Anomaly detection feature is not enabled for the space.'); - return []; - } - if (!mlCapabilities.isPlatinumOrTrialLicense) { - logger.warn( - 'Unable to create anomaly detection jobs due to insufficient license.' - ); - return []; - } - - const mlJobId = await getMLJobId(ml, environment); - - const params = { - body: { - size: 0, - query: { - bool: { - filter: [ - { - term: { - result_type: 'record', - }, - }, - { - term: { - job_id: mlJobId, - }, - }, - { - range: { - timestamp: { - gte: start, - lte: end, - format: 'epoch_millis', - }, - }, - }, - ], - }, - }, - aggs: { - service_name: { - terms: { - field: 'partition_field_value', - }, - aggs: { - transaction_type: { - terms: { - field: 'by_field_value', - }, - aggs: { - top_score: { - top_metrics: { - metrics: [ - { - field: 'actual', - }, - ], - sort: { - record_score: 'desc', - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const response = await ml.mlSystem.mlAnomalySearch(params); - return transformTopAnomalies(response as TopAnomaliesAggResponse); -} - -interface TopAnomaliesAggResponse { - aggregations: { - service_name: { - buckets: Array<{ - key: string; - transaction_type: { - buckets: Array<{ - key: string; - top_score: { - top: [{ sort: [number]; metrics: { actual: number } }]; - }; - }>; - }; - }>; - }; - }; -} - -function transformTopAnomalies(response: TopAnomaliesAggResponse) { - const services = response.aggregations.service_name.buckets.map( - ({ key: serviceName, transaction_type: transactionTypeAgg }) => { - return { - 'service.name': serviceName, - anomalies: transactionTypeAgg.buckets.map( - ({ key: transactionType, top_score: topScoreAgg }) => { - return { - 'transaction.type': transactionType, - anomaly_score: topScoreAgg.top[0].sort[0], - actual_value: topScoreAgg.top[0].metrics.actual, - }; - } - ), - }; - } - ); - return services; -} - -export async function getMLJobId( - ml: Required['ml'], - environment: string -) { - const response = await ml.anomalyDetectors.jobs('apm'); - const matchingMLJobs = response.jobs.filter( - (anomalyDetectionJob) => - // TODO [ML] remove this since job_tags is defined in another branch - // @ts-ignore - anomalyDetectionJob.custom_settings?.job_tags?.environment === environment - ); - if (matchingMLJobs.length === 0) { - throw new Error(`ML job Not Found for environment "${environment}".`); - } - return matchingMLJobs[0].job_id; -} diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 74851582328c7..0a374e11861d1 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -18,7 +18,8 @@ import { ExternalConnectionNode, } from '../../../common/service_map'; import { ConnectionsResponse, ServicesResponse } from './get_service_map'; -import { TopAnomaliesResponse } from './get_top_anomalies'; +import { MaxAnomaliesResponse } from './get_max_anomalies'; +import { MaxAnomaly } from '../../../common/anomaly_detection'; function getConnectionNodeId(node: ConnectionNode): string { if ('span.destination.service.resource' in node) { @@ -62,9 +63,31 @@ export function getServiceNodes(allNodes: ConnectionNode[]) { return serviceNodes; } +function getServiceAnomalyData( + anomalies: MaxAnomaliesResponse, + serviceName?: string +): { [SERVICE_NAME]: string; maxAnomaly: MaxAnomaly } | undefined { + if (anomalies.mlJobIds.length === 0 || !serviceName) { + return; + } + + const matchedAnomalyData = anomalies.maxAnomalies.find( + (anomalyData) => anomalyData[SERVICE_NAME] === serviceName + ); + if (matchedAnomalyData) { + return matchedAnomalyData; + } + + // If there is no anomaly data, return a job_id to link to a running job + return { + [SERVICE_NAME]: serviceName, + maxAnomaly: { job_id: anomalies.mlJobIds[0] }, + }; +} + export type ServiceMapResponse = ConnectionsResponse & { services: ServicesResponse; - anomalies: TopAnomaliesResponse; + anomalies: MaxAnomaliesResponse; }; export function transformServiceMapResponses(response: ServiceMapResponse) { @@ -106,9 +129,7 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { (serviceNode) => serviceNode[SERVICE_NAME] === serviceName ); - const matchedAnomalies = anomalies.find( - (anomalyData) => anomalyData[SERVICE_NAME] === serviceName - ); + const serviceAnomalyData = getServiceAnomalyData(anomalies, serviceName); if (matchedServiceNodes.length) { return { @@ -120,7 +141,7 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { ...matchedServiceNodes.map((serviceNode) => pickBy(serviceNode, identity) ), - matchedAnomalies + serviceAnomalyData ), }; } From 13a95fd4b6bd52aaa883d61a2da3f40e135eb7f7 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 00:11:15 -0700 Subject: [PATCH 03/13] fixes some CI errors --- .../ServiceMap/Popover/Popover.stories.tsx | 37 +++++++++---------- .../MachineLearningLinks/MLJobLink.test.tsx | 2 +- .../transform_service_map_responses.test.ts | 19 ++++++++++ 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index 2edd36f0d1380..8deb75a8a9f14 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -11,41 +11,40 @@ import { ServiceMetricList } from './ServiceMetricList'; storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) .add('example', () => ( - )) - .add('loading', () => ( - )) .add('some null values', () => ( )) .add('all null values', () => ( )); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index b4cf3a65fea35..97fb16ba55333 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -19,7 +19,7 @@ describe('MLJobLink', () => { ); expect(href).toEqual( - `/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))` + `/basepath/app/ml#/timeseriesexplorer?_g=(ml%3A(jobIds%3A!(myservicename-mytransactiontype-high_mean_response_time))%2CrefreshInterval%3A(pause%3Atrue%2Cvalue%3A'0')%2Ctime%3A(from%3Anow%252Fw%2Cto%3Anow-4h))` ); }); }); diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 1e26634bdf0f1..d3fd1f748d260 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -35,6 +35,21 @@ const javaService = { [AGENT_NAME]: 'java', }; +const anomalies = { + mlJobIds: ['apm-test-1234-ml-module-name'], + maxAnomalies: [ + { + 'service.name': 'opbeans-test', + maxAnomaly: { + 'transaction.type': 'request', + actual_value: 10000, + anomaly_score: 50, + job_id: 'apm-test-1234-ml-module-name', + }, + }, + ], +}; + describe('transformServiceMapResponses', () => { it('maps external destinations to internal services', () => { const response: ServiceMapResponse = { @@ -51,6 +66,7 @@ describe('transformServiceMapResponses', () => { destination: nodejsExternal, }, ], + anomalies, }; const { elements } = transformServiceMapResponses(response); @@ -89,6 +105,7 @@ describe('transformServiceMapResponses', () => { }, }, ], + anomalies, }; const { elements } = transformServiceMapResponses(response); @@ -126,6 +143,7 @@ describe('transformServiceMapResponses', () => { }, }, ], + anomalies, }; const { elements } = transformServiceMapResponses(response); @@ -150,6 +168,7 @@ describe('transformServiceMapResponses', () => { destination: nodejsService, }, ], + anomalies, }; const { elements } = transformServiceMapResponses(response); From 55b1e911228f55aa778c749d74999990d8339aa8 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 00:57:25 -0700 Subject: [PATCH 04/13] Simplified messaging for service popop with not data in the current environment --- x-pack/plugins/apm/common/service_map.ts | 1 - .../ServiceMap/Popover/Popover.stories.tsx | 3 --- .../Popover/ServiceMetricFetcher.tsx | 22 ++----------------- .../get_service_map_service_node_info.ts | 1 - 4 files changed, 2 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 8de530be04a63..fd2bba16d3fce 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -36,7 +36,6 @@ export interface Connection { export interface ServiceNodeMetrics { hasEnvironmentData: boolean; - environmentsWithData: string[]; avgMemoryUsage: number | null; avgCpuUsage: number | null; transactionKPIs: { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index 8deb75a8a9f14..e888399b243e9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -12,7 +12,6 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) .add('example', () => ( ( ( @@ -107,12 +98,3 @@ function LoadingSpinner() { ); } - -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/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 50814bce2988b..83ea835b1050d 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 @@ -80,7 +80,6 @@ export async function getServiceMapServiceNodeInfo({ return { hasEnvironmentData, - environmentsWithData, ...errorMetrics, transactionKPIs, ...cpuMetrics, From e1cbde6b4a6c876ebc8d8d796ad90fb126eff384 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 13:15:28 -0700 Subject: [PATCH 05/13] PR feedback, renamed max anomalies -> service anomalies including the file name --- .../plugins/apm/common/anomaly_detection.ts | 15 +-- x-pack/plugins/apm/common/service_map.ts | 5 +- .../ServiceMap/Popover/AnomalyDetection.tsx | 18 ++-- .../app/ServiceMap/Popover/Contents.tsx | 2 +- .../ServiceMap/Popover/Popover.stories.tsx | 9 +- .../Popover/ServiceMetricFetcher.tsx | 35 +++++-- .../ServiceMap/Popover/ServiceMetricList.tsx | 10 +- .../app/ServiceMap/cytoscapeOptions.ts | 10 +- .../create_anomaly_detection_jobs.ts | 1 + ..._anomalies.ts => get_service_anomalies.ts} | 96 +++++++------------ .../server/lib/service_map/get_service_map.ts | 10 +- .../get_service_map_service_node_info.ts | 19 +--- .../transform_service_map_responses.test.ts | 14 +-- .../transform_service_map_responses.ts | 27 +++--- 14 files changed, 130 insertions(+), 141 deletions(-) rename x-pack/plugins/apm/server/lib/service_map/{get_max_anomalies.ts => get_service_anomalies.ts} (59%) diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 7b48230075dca..f9f7e6afeaabe 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TRANSACTION_TYPE } from './elasticsearch_fieldnames'; +export interface ServiceAnomalyStats { + transactionType?: string; + anomalyScore?: number; + actualValue?: number; + jobId?: string; +} -export interface MaxAnomaly { - [TRANSACTION_TYPE]?: string; - anomaly_score?: number; - actual_value?: number; - job_id?: string; +export interface ServiceAnomalies { + serviceName: string; + serviceAnomalyStats: ServiceAnomalyStats; } diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index fd2bba16d3fce..b50db270ef544 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -15,11 +15,13 @@ import { SPAN_SUBTYPE, SPAN_TYPE, } from './elasticsearch_fieldnames'; +import { ServiceAnomalyStats } from './anomaly_detection'; export interface ServiceConnectionNode extends cytoscape.NodeDataDefinition { [SERVICE_NAME]: string; [SERVICE_ENVIRONMENT]: string | null; [AGENT_NAME]: string; + serviceAnomalyStats?: ServiceAnomalyStats; } export interface ExternalConnectionNode extends cytoscape.NodeDataDefinition { [SPAN_DESTINATION_SERVICE_RESOURCE]: string; @@ -35,10 +37,9 @@ export interface Connection { } export interface ServiceNodeMetrics { - hasEnvironmentData: boolean; avgMemoryUsage: number | null; avgCpuUsage: number | null; - transactionKPIs: { + transactionStats: { avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }; 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 b2ffa8f55372a..410ba8b5027fb 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 @@ -20,9 +20,8 @@ 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_TYPE } from '../../../../../common/elasticsearch_fieldnames'; import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; -import { MaxAnomaly } from '../../../../../common/anomaly_detection'; +import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; const HealthStatusTitle = styled(EuiTitle)` display: inline; @@ -51,16 +50,17 @@ export const ContentLine = styled.section` interface Props { serviceName: string; - maxAnomaly: MaxAnomaly | undefined; + serviceAnomalyStats: ServiceAnomalyStats | undefined; } -export function AnomalyDetection({ serviceName, maxAnomaly }: Props) { +export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) { const theme = useTheme(); - const anomalyScore = maxAnomaly?.anomaly_score; + const anomalyScore = serviceAnomalyStats?.anomalyScore; const anomalySeverity = getSeverity(anomalyScore); - const actualValue = maxAnomaly?.actual_value; - const mlJobId = maxAnomaly?.job_id; - const transactionType = maxAnomaly?.[TRANSACTION_TYPE]; + const actualValue = serviceAnomalyStats?.actualValue; + const mlJobId = serviceAnomalyStats?.jobId; + const transactionType = + serviceAnomalyStats?.transactionType ?? TRANSACTION_REQUEST; const hasAnomalyDetectionScore = anomalyScore !== undefined; return ( @@ -102,7 +102,7 @@ export function AnomalyDetection({ serviceName, maxAnomaly }: Props) { external jobId={mlJobId} serviceName={serviceName} - transactionType={transactionType || TRANSACTION_REQUEST} + transactionType={transactionType} > {ANOMALY_DETECTION_LINK} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 3a6057f26550d..c696a93773ceb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -72,7 +72,7 @@ export function Contents({ {isService ? ( ) : ( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index e888399b243e9..ccf147ed1d90d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -11,9 +11,8 @@ import { ServiceMetricList } from './ServiceMetricList'; storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) .add('example', () => ( ( ( { @@ -57,13 +58,28 @@ export function ServiceMetricFetcher({ return ; } - if (environment && !data.hasEnvironmentData) { + const { + avgCpuUsage, + avgErrorsPerMinute, + avgMemoryUsage, + transactionStats: { avgRequestsPerMinute, avgTransactionDuration }, + } = data; + + const hasNoServiceData = [ + avgCpuUsage, + avgErrorsPerMinute, + avgMemoryUsage, + avgRequestsPerMinute, + avgTransactionDuration, + ].every((stat) => !isNumber(stat)); + + if (environment && !hasNoServiceData) { return ( @@ -80,7 +96,10 @@ export function ServiceMetricFetcher({ } return ( <> - + diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index bb77e7606b1f4..f82f434e7ded1 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -30,7 +30,7 @@ export function ServiceMetricList({ avgErrorsPerMinute, avgCpuUsage, avgMemoryUsage, - transactionKPIs, + transactionStats, }: ServiceMetricListProps) { const listItems = [ { @@ -40,8 +40,8 @@ export function ServiceMetricList({ defaultMessage: 'Trans. duration (avg.)', } ), - description: isNumber(transactionKPIs.avgTransactionDuration) - ? asDuration(transactionKPIs.avgTransactionDuration) + description: isNumber(transactionStats.avgTransactionDuration) + ? asDuration(transactionStats.avgTransactionDuration) : null, }, { @@ -51,8 +51,8 @@ export function ServiceMetricList({ defaultMessage: 'Req. per minute (avg.)', } ), - description: isNumber(transactionKPIs.avgRequestsPerMinute) - ? `${transactionKPIs.avgRequestsPerMinute.toFixed(2)} ${tpmUnit( + description: isNumber(transactionStats.avgRequestsPerMinute) + ? `${transactionStats.avgRequestsPerMinute.toFixed(2)} ${tpmUnit( 'request' )}` : null, 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 ba0fef7dfb443..dfcfbee1806a4 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -12,7 +12,7 @@ import { import { EuiTheme } from '../../../../../observability/public'; import { severity, getSeverity } from '../../../../common/ml_job_constants'; import { defaultIcon, iconForNode } from './icons'; -import { MaxAnomaly } from '../../../../common/anomaly_detection'; +import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; export const popoverWidth = 280; @@ -31,15 +31,17 @@ export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) { } function getNodeSeverity(el: cytoscape.NodeSingular) { - const maxAnomaly: MaxAnomaly | undefined = el.data('maxAnomaly'); - return getSeverity(maxAnomaly?.anomaly_score); + const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data( + 'serviceAnomalyStats' + ); + return getSeverity(serviceAnomalyStats?.anomalyScore); } function getBorderColorFn( theme: EuiTheme ): cytoscape.Css.MapperFunction { return (el: cytoscape.NodeSingular) => { - const hasAnomalyDetectionJob = el.data('maxAnomaly') !== undefined; + const hasAnomalyDetectionJob = el.data('serviceAnomalyStats') !== undefined; const nodeSeverity = getNodeSeverity(el); if (hasAnomalyDetectionJob) { return ( 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 71afc5733becb..5449a51cd02aa 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 @@ -83,6 +83,7 @@ async function createAnomalyDetectionJob({ return ml.modules.setup({ moduleId: ML_MODULE_ID_APM_TRANSACTION, + // removes all non-alphanumeric characters form environment prefix: `${ML_GROUP_NAME_APM}-${snakeCase(environment)}-${randomToken}-`, groups: [ML_GROUP_NAME_APM], indexPatternName, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_max_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts similarity index 59% rename from x-pack/plugins/apm/server/lib/service_map/get_max_anomalies.ts rename to x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index b481d7649b39b..6407f8f37ae88 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_max_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -11,20 +11,15 @@ import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; -import { - TRANSACTION_TYPE, - SERVICE_NAME, -} from '../../../common/elasticsearch_fieldnames'; -import { MaxAnomaly } from '../../../common/anomaly_detection'; +import { ServiceAnomalies } from '../../../common/anomaly_detection'; -const DEFAULT_VALUE = { - mlJobIds: [], - maxAnomalies: [], -}; +export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: [] }; -export type MaxAnomaliesResponse = PromiseReturnType; +export type ServiceAnomaliesResponse = PromiseReturnType< + typeof getServiceAnomalies +>; -export async function getMaxAnomalies({ +export async function getServiceAnomalies({ setup, logger, environment, @@ -37,18 +32,18 @@ export async function getMaxAnomalies({ if (!ml) { logger.warn('Anomaly detection plugin is not available.'); - return DEFAULT_VALUE; + return DEFAULT_ANOMALIES; } const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { logger.warn('Anomaly detection feature is not enabled for the space.'); - return DEFAULT_VALUE; + return DEFAULT_ANOMALIES; } if (!mlCapabilities.isPlatinumOrTrialLicense) { logger.warn( 'Unable to create anomaly detection jobs due to insufficient license.' ); - return DEFAULT_VALUE; + return DEFAULT_ANOMALIES; } let mlJobIds: string[] = []; @@ -56,7 +51,7 @@ export async function getMaxAnomalies({ mlJobIds = await getMLJobIds(ml, environment); } catch (error) { logger.error(error); - return DEFAULT_VALUE; + return DEFAULT_ANOMALIES; } const params = { @@ -65,27 +60,16 @@ export async function getMaxAnomalies({ query: { bool: { filter: [ - { - term: { - result_type: 'record', - }, - }, - { - terms: { - job_id: mlJobIds, - }, - }, + { term: { result_type: 'record' } }, + { terms: { job_id: mlJobIds } }, { range: { - timestamp: { - gte: start, - lte: end, - format: 'epoch_millis', - }, + timestamp: { gte: start, lte: end, format: 'epoch_millis' }, }, }, { terms: { + // Only retrieving anomalies for transaction types "request" and "page-load" by_field_value: [TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD], }, }, @@ -93,19 +77,13 @@ export async function getMaxAnomalies({ }, }, aggs: { - service_name: { - terms: { - field: 'partition_field_value', - }, + services: { + terms: { field: 'partition_field_value' }, aggs: { top_score: { top_hits: { - sort: { - record_score: 'desc', - }, - _source: { - includes: ['actual', 'job_id', 'by_field_value'], - }, + sort: { record_score: 'desc' }, + _source: { includes: ['actual', 'job_id', 'by_field_value'] }, size: 1, }, }, @@ -117,15 +95,15 @@ export async function getMaxAnomalies({ const response = await ml.mlSystem.mlAnomalySearch(params); return { mlJobIds, - maxAnomalies: transformResponseToMaxAnomalies( - response as MaxAnomaliesAggResponse + serviceAnomalies: transformResponseToServiceAnomalies( + response as ServiceAnomaliesAggResponse ), }; } -interface MaxAnomaliesAggResponse { +interface ServiceAnomaliesAggResponse { aggregations: { - service_name: { + services: { buckets: Array<{ key: string; top_score: { @@ -145,21 +123,18 @@ interface MaxAnomaliesAggResponse { }; } -function transformResponseToMaxAnomalies( - response: MaxAnomaliesAggResponse -): Array<{ - [SERVICE_NAME]: string; - maxAnomaly: MaxAnomaly; -}> { - const services = response.aggregations.service_name.buckets.map( +function transformResponseToServiceAnomalies( + response: ServiceAnomaliesAggResponse +): ServiceAnomalies[] { + const services = response.aggregations.services.buckets.map( ({ key: serviceName, top_score: topScoreAgg }) => { return { - [SERVICE_NAME]: serviceName, - maxAnomaly: { - [TRANSACTION_TYPE]: topScoreAgg.hits.hits[0]?._source?.by_field_value, - anomaly_score: topScoreAgg.hits.hits[0]?.sort?.[0], - actual_value: topScoreAgg.hits.hits[0]?._source?.actual?.[0], - job_id: topScoreAgg.hits.hits[0]?._source?.job_id, + serviceName, + serviceAnomalyStats: { + transactionType: topScoreAgg.hits.hits[0]?._source?.by_field_value, + anomalyScore: topScoreAgg.hits.hits[0]?.sort?.[0], + actualValue: topScoreAgg.hits.hits[0]?._source?.actual?.[0], + jobId: topScoreAgg.hits.hits[0]?._source?.job_id, }, }; } @@ -172,11 +147,12 @@ export async function getMLJobIds( environment?: string ) { const response = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM); - const apmAnomalyDetectionMLJobs = response.jobs.filter( + // to filter out legacy jobs we are filtering by the existence of `environment` in `custom_settings` + const mlJobs = response.jobs.filter( (job) => job.custom_settings?.job_tags?.environment ); if (environment) { - const matchingMLJob = apmAnomalyDetectionMLJobs.find( + const matchingMLJob = mlJobs.find( (job) => job.custom_settings?.job_tags?.environment === environment ); if (!matchingMLJob) { @@ -184,5 +160,5 @@ export async function getMLJobIds( } return [matchingMLJob.job_id]; } - return apmAnomalyDetectionMLJobs.map((job) => job.job_id); + return mlJobs.map((job) => job.job_id); } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 090c0eba1c044..ea2bb14efdfc7 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -17,7 +17,11 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { transformServiceMapResponses } from './transform_service_map_responses'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; -import { getMaxAnomalies, MaxAnomaliesResponse } from './get_max_anomalies'; +import { + getServiceAnomalies, + ServiceAnomaliesResponse, + DEFAULT_ANOMALIES, +} from './get_service_anomalies'; export interface IEnvOptions { setup: Setup & SetupTimeRange; @@ -136,12 +140,12 @@ export type ServiceMapAPIResponse = PromiseReturnType; export async function getServiceMap(options: IEnvOptions) { const { logger } = options; - const anomaliesPromise: Promise = getMaxAnomalies( + const anomaliesPromise: Promise = getServiceAnomalies( options ).catch((error) => { logger.warn(`Unable to retrieve anomalies for service maps.`); logger.error(error); - return { mlJobIds: [], maxAnomalies: [] }; + return DEFAULT_ANOMALIES; }); const [connectionData, servicesData, anomalies] = await Promise.all([ getConnectionData(options), 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 83ea835b1050d..0f48fb5c07dd3 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 @@ -22,7 +22,6 @@ import { TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD, } from '../../../common/transaction_types'; -import { getAllEnvironments } from '../environments/get_all_environments'; interface Options { setup: Setup & SetupTimeRange; @@ -58,30 +57,20 @@ export async function getServiceMapServiceNodeInfo({ }; const [ - environmentsWithData, errorMetrics, - transactionKPIs, + transactionStats, cpuMetrics, memoryMetrics, ] = await Promise.all([ - getAllEnvironments({ - serviceName, - setup, - includeMissing: true, - }), getErrorMetrics(taskParams), - getTransactionKPIs(taskParams), + getTransactionStats(taskParams), getCpuMetrics(taskParams), getMemoryMetrics(taskParams), ]); - const hasEnvironmentData = - !environment || environmentsWithData.includes(environment); - return { - hasEnvironmentData, ...errorMetrics, - transactionKPIs, + transactionStats, ...cpuMetrics, ...memoryMetrics, }; @@ -115,7 +104,7 @@ async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) { }; } -async function getTransactionKPIs({ +async function getTransactionStats({ setup, filter, minutes, diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index d3fd1f748d260..fdc252f66cb71 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -37,14 +37,14 @@ const javaService = { const anomalies = { mlJobIds: ['apm-test-1234-ml-module-name'], - maxAnomalies: [ + serviceAnomalies: [ { - 'service.name': 'opbeans-test', - maxAnomaly: { - 'transaction.type': 'request', - actual_value: 10000, - anomaly_score: 50, - job_id: 'apm-test-1234-ml-module-name', + serviceName: 'opbeans-test', + serviceAnomalyStats: { + transactionType: 'request', + actualValue: 10000, + anomalyScore: 50, + jobId: 'apm-test-1234-ml-module-name', }, }, ], diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 0a374e11861d1..319710ee4be9b 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -18,8 +18,8 @@ import { ExternalConnectionNode, } from '../../../common/service_map'; import { ConnectionsResponse, ServicesResponse } from './get_service_map'; -import { MaxAnomaliesResponse } from './get_max_anomalies'; -import { MaxAnomaly } from '../../../common/anomaly_detection'; +import { ServiceAnomaliesResponse } from './get_service_anomalies'; +import { ServiceAnomalyStats } from '../../../common/anomaly_detection'; function getConnectionNodeId(node: ConnectionNode): string { if ('span.destination.service.resource' in node) { @@ -63,31 +63,28 @@ export function getServiceNodes(allNodes: ConnectionNode[]) { return serviceNodes; } -function getServiceAnomalyData( - anomalies: MaxAnomaliesResponse, +function getServiceAnomalyStats( + anomalies: ServiceAnomaliesResponse, serviceName?: string -): { [SERVICE_NAME]: string; maxAnomaly: MaxAnomaly } | undefined { +): ServiceAnomalyStats | undefined { if (anomalies.mlJobIds.length === 0 || !serviceName) { return; } - const matchedAnomalyData = anomalies.maxAnomalies.find( - (anomalyData) => anomalyData[SERVICE_NAME] === serviceName + const matchedAnomalyData = anomalies.serviceAnomalies.find( + (anomalyData) => anomalyData.serviceName === serviceName ); if (matchedAnomalyData) { - return matchedAnomalyData; + return matchedAnomalyData.serviceAnomalyStats; } // If there is no anomaly data, return a job_id to link to a running job - return { - [SERVICE_NAME]: serviceName, - maxAnomaly: { job_id: anomalies.mlJobIds[0] }, - }; + return { jobId: anomalies.mlJobIds[0] }; } export type ServiceMapResponse = ConnectionsResponse & { services: ServicesResponse; - anomalies: MaxAnomaliesResponse; + anomalies: ServiceAnomaliesResponse; }; export function transformServiceMapResponses(response: ServiceMapResponse) { @@ -129,7 +126,7 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { (serviceNode) => serviceNode[SERVICE_NAME] === serviceName ); - const serviceAnomalyData = getServiceAnomalyData(anomalies, serviceName); + const serviceAnomalyStats = getServiceAnomalyStats(anomalies, serviceName); if (matchedServiceNodes.length) { return { @@ -141,7 +138,7 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { ...matchedServiceNodes.map((serviceNode) => pickBy(serviceNode, identity) ), - serviceAnomalyData + serviceAnomalyStats ? { serviceAnomalyStats } : null ), }; } From fe62454147692412acaec500f9870f94e3940ba5 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 14:38:00 -0700 Subject: [PATCH 06/13] - defines custom_settings.job_tags.apm_ml_version in ML job creation, then filters for it when returing valid APM ML jobs --- .../app/ServiceMap/Popover/ServiceMetricFetcher.tsx | 6 +++--- .../lib/anomaly_detection/create_anomaly_detection_jobs.ts | 6 +++++- .../apm/server/lib/service_map/get_service_anomalies.ts | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index c7dd265e1e9bc..9f5115c0c70f4 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -65,15 +65,15 @@ export function ServiceMetricFetcher({ transactionStats: { avgRequestsPerMinute, avgTransactionDuration }, } = data; - const hasNoServiceData = [ + const hasServiceData = [ avgCpuUsage, avgErrorsPerMinute, avgMemoryUsage, avgRequestsPerMinute, avgTransactionDuration, - ].every((stat) => !isNumber(stat)); + ].some((stat) => isNumber(stat)); - if (environment && !hasNoServiceData) { + if (environment && !hasServiceData) { return ( job.custom_settings?.job_tags?.environment + (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2 ); if (environment) { const matchingMLJob = mlJobs.find( From f21b6fbbcaf7f111c4dd7c0d01af3cda39843624 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 15:34:47 -0700 Subject: [PATCH 07/13] changes shape of of service anomalies from an array to a object keyed by serviceName --- .../plugins/apm/common/anomaly_detection.ts | 5 ----- .../lib/service_map/get_service_anomalies.ts | 19 ++++++++++--------- .../transform_service_map_responses.test.ts | 17 +++++++---------- .../transform_service_map_responses.ts | 13 ++++--------- 4 files changed, 21 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index f9f7e6afeaabe..1fd927d82f186 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -10,8 +10,3 @@ export interface ServiceAnomalyStats { actualValue?: number; jobId?: string; } - -export interface ServiceAnomalies { - serviceName: string; - serviceAnomalyStats: ServiceAnomalyStats; -} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 83cabc91a7884..fb4bcffc1cfc3 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -11,9 +11,9 @@ import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; -import { ServiceAnomalies } from '../../../common/anomaly_detection'; +import { ServiceAnomalyStats } from '../../../common/anomaly_detection'; -export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: [] }; +export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} }; export type ServiceAnomaliesResponse = PromiseReturnType< typeof getServiceAnomalies @@ -125,21 +125,22 @@ interface ServiceAnomaliesAggResponse { function transformResponseToServiceAnomalies( response: ServiceAnomaliesAggResponse -): ServiceAnomalies[] { - const services = response.aggregations.services.buckets.map( - ({ key: serviceName, top_score: topScoreAgg }) => { +): Record { + const serviceAnomaliesMap = response.aggregations.services.buckets.reduce( + (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => { return { - serviceName, - serviceAnomalyStats: { + ...statsByServiceName, + [serviceName]: { transactionType: topScoreAgg.hits.hits[0]?._source?.by_field_value, anomalyScore: topScoreAgg.hits.hits[0]?.sort?.[0], actualValue: topScoreAgg.hits.hits[0]?._source?.actual?.[0], jobId: topScoreAgg.hits.hits[0]?._source?.job_id, }, }; - } + }, + {} ); - return services; + return serviceAnomaliesMap; } export async function getMLJobIds( diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index fdc252f66cb71..7e4bcfdda7382 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -37,17 +37,14 @@ const javaService = { const anomalies = { mlJobIds: ['apm-test-1234-ml-module-name'], - serviceAnomalies: [ - { - serviceName: 'opbeans-test', - serviceAnomalyStats: { - transactionType: 'request', - actualValue: 10000, - anomalyScore: 50, - jobId: 'apm-test-1234-ml-module-name', - }, + serviceAnomalies: { + 'opbeans-test': { + transactionType: 'request', + actualValue: 10000, + anomalyScore: 50, + jobId: 'apm-test-1234-ml-module-name', }, - ], + }, }; describe('transformServiceMapResponses', () => { diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 319710ee4be9b..86c2f862821bf 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -68,18 +68,13 @@ function getServiceAnomalyStats( serviceName?: string ): ServiceAnomalyStats | undefined { if (anomalies.mlJobIds.length === 0 || !serviceName) { + // Don't return data when there are no anomaly jobs return; } - - const matchedAnomalyData = anomalies.serviceAnomalies.find( - (anomalyData) => anomalyData.serviceName === serviceName - ); - if (matchedAnomalyData) { - return matchedAnomalyData.serviceAnomalyStats; - } - // If there is no anomaly data, return a job_id to link to a running job - return { jobId: anomalies.mlJobIds[0] }; + return ( + anomalies.serviceAnomalies[serviceName] || { jobId: anomalies.mlJobIds[0] } + ); } export type ServiceMapResponse = ConnectionsResponse & { From 5f0c386ce01c86f7a2fadb21d7a7139c0776cb40 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 15:35:47 -0700 Subject: [PATCH 08/13] removes the url encoding from ML job link href to how it was previously. --- .../MachineLearningLinks/MLJobLink.test.tsx | 20 ++++++- .../useTimeSeriesExplorerHref.ts | 53 +++++++++++-------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index 97fb16ba55333..c832d3ded6175 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -18,8 +18,24 @@ describe('MLJobLink', () => { { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location ); - expect(href).toEqual( - `/basepath/app/ml#/timeseriesexplorer?_g=(ml%3A(jobIds%3A!(myservicename-mytransactiontype-high_mean_response_time))%2CrefreshInterval%3A(pause%3Atrue%2Cvalue%3A'0')%2Ctime%3A(from%3Anow%252Fw%2Cto%3Anow-4h))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))"` + ); + }); + it('should produce the correct URL with jobId, serviceName, and transactionType', async () => { + const href = await getRenderedHref( + () => ( + + ), + { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location + ); + + expect(href).toMatchInlineSnapshot( + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts index 8aefd2ca7bbb7..625b9205b6ce0 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts @@ -5,6 +5,7 @@ */ import url from 'url'; +import querystring from 'querystring'; import rison from 'rison-node'; import { useLocation } from '../../../../hooks/useLocation'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; @@ -22,30 +23,36 @@ export function useTimeSeriesExplorerHref({ const { core } = useApmPluginContext(); const location = useLocation(); - return url.format({ - pathname: core.http.basePath.prepend('/app/ml'), - hash: url.format({ - pathname: '/timeseriesexplorer', - query: { - _g: rison.encode({ - ml: { - jobIds: [jobId], - }, - ...getTimepickerRisonData(location.search), - }), - ...(serviceName && transactionType - ? { - _a: rison.encode({ - mlTimeSeriesExplorer: { - entities: { - 'service.name': serviceName, - 'transaction.type': transactionType, - }, + const search = querystring.stringify( + { + _g: rison.encode({ + ml: { jobIds: [jobId] }, + ...getTimepickerRisonData(location.search), + }), + ...(serviceName && transactionType + ? { + _a: rison.encode({ + mlTimeSeriesExplorer: { + entities: { + 'service.name': serviceName, + 'transaction.type': transactionType, }, - }), - } - : null), + }, + }), + } + : null), + }, + undefined, + undefined, + { + encodeURIComponent(str: string) { + return str; }, - }), + } + ); + + return url.format({ + pathname: core.http.basePath.prepend('/app/ml'), + hash: url.format({ pathname: '/timeseriesexplorer', search }), }); } From 4ca96d5715cdd0671527e8918ff66f1a6a10cf0e Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 16:01:27 -0700 Subject: [PATCH 09/13] PR feedback --- .../transform_service_map_responses.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 86c2f862821bf..648d846c290a9 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -117,24 +117,21 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { serviceName = node[SERVICE_NAME]; } - const matchedServiceNodes = serviceNodes.filter( - (serviceNode) => serviceNode[SERVICE_NAME] === serviceName - ); + const matchedServiceNodes = serviceNodes + .filter((serviceNode) => serviceNode[SERVICE_NAME] === serviceName) + .map((serviceNode) => pickBy(serviceNode, identity)); + const mergedServiceNode = Object.assign({}, ...matchedServiceNodes); const serviceAnomalyStats = getServiceAnomalyStats(anomalies, serviceName); if (matchedServiceNodes.length) { return { ...map, - [node.id]: Object.assign( - { - id: matchedServiceNodes[0][SERVICE_NAME], - }, - ...matchedServiceNodes.map((serviceNode) => - pickBy(serviceNode, identity) - ), - serviceAnomalyStats ? { serviceAnomalyStats } : null - ), + [node.id]: { + id: matchedServiceNodes[0][SERVICE_NAME], + ...mergedServiceNode, + ...(serviceAnomalyStats ? { serviceAnomalyStats } : null), + }, }; } From 7e1b4d71efbb9401508aa2d0a4b0dd6545adbf15 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 16:52:37 -0700 Subject: [PATCH 10/13] Popover no data state simplified: - renders the "no data" message as plain text instead of in a callout - hides the 'Anomaly detection' section if there is not anomaly data. --- .../Popover/ServiceMetricFetcher.tsx | 38 ++++++++----------- .../transform_service_map_responses.ts | 19 ++-------- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index 9f5115c0c70f4..957678877a134 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -9,7 +9,7 @@ import { EuiLoadingSpinner, EuiFlexGroup, EuiHorizontalRule, - EuiCallOut, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; @@ -75,32 +75,24 @@ export function ServiceMetricFetcher({ if (environment && !hasServiceData) { return ( - - {i18n.translate( - 'xpack.apm.serviceMap.popoverMetrics.noEnvironmentDataCallout.text', - { - defaultMessage: `Try switching to another environment.`, - } - )} - + + {i18n.translate('xpack.apm.serviceMap.popoverMetrics.noDataText', { + defaultMessage: `No data for selected environment. Try switching to another environment.`, + })} + ); } return ( <> - - + {serviceAnomalyStats && ( + <> + + + + )} ); diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 648d846c290a9..7f5e34f68f922 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -19,7 +19,6 @@ import { } from '../../../common/service_map'; import { ConnectionsResponse, ServicesResponse } from './get_service_map'; import { ServiceAnomaliesResponse } from './get_service_anomalies'; -import { ServiceAnomalyStats } from '../../../common/anomaly_detection'; function getConnectionNodeId(node: ConnectionNode): string { if ('span.destination.service.resource' in node) { @@ -63,20 +62,6 @@ export function getServiceNodes(allNodes: ConnectionNode[]) { return serviceNodes; } -function getServiceAnomalyStats( - anomalies: ServiceAnomaliesResponse, - serviceName?: string -): ServiceAnomalyStats | undefined { - if (anomalies.mlJobIds.length === 0 || !serviceName) { - // Don't return data when there are no anomaly jobs - return; - } - // If there is no anomaly data, return a job_id to link to a running job - return ( - anomalies.serviceAnomalies[serviceName] || { jobId: anomalies.mlJobIds[0] } - ); -} - export type ServiceMapResponse = ConnectionsResponse & { services: ServicesResponse; anomalies: ServiceAnomaliesResponse; @@ -122,7 +107,9 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { .map((serviceNode) => pickBy(serviceNode, identity)); const mergedServiceNode = Object.assign({}, ...matchedServiceNodes); - const serviceAnomalyStats = getServiceAnomalyStats(anomalies, serviceName); + const serviceAnomalyStats = serviceName + ? anomalies.serviceAnomalies[serviceName] + : null; if (matchedServiceNodes.length) { return { From 5dcd97557a5abd185982ef20f3c226c3c310fc7b Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 17:10:06 -0700 Subject: [PATCH 11/13] Fixes filtering bug when user selects 'Environment: Not defined'. Now filters properly by filtering for docs where service.environment does not exist --- .../get_service_map_service_node_info.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 0f48fb5c07dd3..be92bfe5a0099 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 @@ -22,6 +22,7 @@ import { TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD, } from '../../../common/transaction_types'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; interface Options { setup: Setup & SetupTimeRange; @@ -42,12 +43,23 @@ 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 } }, - ...(environment ? [{ term: { [SERVICE_ENVIRONMENT]: environment } }] : []), ]; + if (environment) { + filter.push( + environment === ENVIRONMENT_NOT_DEFINED + ? environmentNotDefinedFilter + : { term: { [SERVICE_ENVIRONMENT]: environment } } + ); + } + const minutes = Math.abs((end - start) / (1000 * 60)); const taskParams = { From 258a45644991c882efe2beb83d960cced5addf9c Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 8 Jul 2020 17:58:35 -0700 Subject: [PATCH 12/13] filters jobs fetched in the settings page by `job.custom_settings.job_tags.apm_ml_version` --- .../lib/anomaly_detection/get_anomaly_detection_jobs.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 252c87e9263db..b59187dc0133f 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 @@ -42,14 +42,16 @@ export async function getAnomalyDetectionJobs( try { const { jobs } = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM); return jobs + .filter( + (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2 + ) .map((job) => { const environment = job.custom_settings?.job_tags?.environment ?? ''; return { job_id: job.job_id, environment, }; - }) - .filter((job) => job.environment); + }); } catch (error) { if (error.statusCode !== 404) { logger.warn('Unable to get APM ML jobs.'); From 0457514f35231560038f0cccd241445251d7647e Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 9 Jul 2020 00:16:00 -0700 Subject: [PATCH 13/13] Fixed bad import from last upstream merge --- .../apm/server/lib/service_map/get_service_anomalies.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index fb4bcffc1cfc3..3e5ef5eb37b02 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -6,12 +6,12 @@ import { Logger } from 'kibana/server'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseReturnType } from '../../../typings/common'; -import { ML_GROUP_NAME_APM } from '../anomaly_detection/create_anomaly_detection_jobs'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; import { ServiceAnomalyStats } from '../../../common/anomaly_detection'; +import { APM_ML_JOB_GROUP } from '../anomaly_detection/constants'; export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} }; @@ -147,7 +147,7 @@ export async function getMLJobIds( ml: Required['ml'], environment?: string ) { - const response = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM); + const response = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP); // to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings` // and checking that it is compatable. const mlJobs = response.jobs.filter(