diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 75c1c945c5d261..2ff30a61499b6b 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -9,7 +9,6 @@ import { ILicense } from '../../licensing/public'; import { AGENT_NAME, SERVICE_ENVIRONMENT, - SERVICE_FRAMEWORK_NAME, SERVICE_NAME, SPAN_SUBTYPE, SPAN_TYPE, @@ -19,7 +18,6 @@ import { export interface ServiceConnectionNode { [SERVICE_NAME]: string; [SERVICE_ENVIRONMENT]: string | null; - [SERVICE_FRAMEWORK_NAME]: string | null; [AGENT_NAME]: string; } export interface ExternalConnectionNode { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index ad77434bca9f43..797145368b4b75 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -134,6 +134,11 @@ export function Cytoscape({ ); cy.remove(absentElements); cy.add(elements); + // ensure all elements get latest data properties + elements.forEach(elementDefinition => { + const el = cy.getElementById(elementDefinition.data.id as string); + el.data(elementDefinition.data); + }); cy.trigger('data'); } }, [cy, elements]); 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 bc3434f277d1c0..7e15d0116b84d1 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 @@ -8,14 +8,23 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, - EuiTitle + EuiTitle, + EuiIconTip, + EuiHealth } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; import React from 'react'; -import { SERVICE_FRAMEWORK_NAME } from '../../../../../common/elasticsearch_fieldnames'; +import styled from 'styled-components'; +import { fontSize, px } from '../../../../style/variables'; import { Buttons } from './Buttons'; import { Info } from './Info'; import { ServiceMetricFetcher } from './ServiceMetricFetcher'; +import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; +import { getSeverityColor } from '../cytoscapeOptions'; +import { asInteger } from '../../../../utils/formatters'; +import { getMetricChangeDescription } from '../../../../../../ml/public'; const popoverMinWidth = 280; @@ -27,6 +36,31 @@ interface ContentsProps { selectedNodeServiceName: string; } +const HealthStatusTitle = styled(EuiTitle)` + display: inline; + text-transform: uppercase; +`; + +const VerticallyCentered = styled.div` + display: flex; + align-items: center; +`; + +const SubduedText = styled.span` + color: ${theme.euiTextSubduedColor}; +`; + +const EnableText = styled.section` + color: ${theme.euiTextSubduedColor}; + line-height: 1.4; + font-size: ${fontSize}; + width: ${px(popoverMinWidth)}; +`; + +export const ContentLine = styled.section` + line-height: 2; +`; + // IE 11 does not handle flex properties as expected. With browser detection, // we can use regular div elements to render contents that are almost identical. // @@ -51,6 +85,37 @@ const FlexColumnGroup = (props: { const FlexColumnItem = (props: { children: React.ReactNode }) => isIE11 ?
: ; +const ANOMALY_DETECTION_TITLE = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverTitle', + { defaultMessage: 'Anomaly Detection' } +); + +const ANOMALY_DETECTION_INFO = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverInfo', + { + defaultMessage: + 'Display the health of your service by enabling 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_ENABLE_TEXT = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverEnable', + { + defaultMessage: + 'Enable anomaly detection from the Integrations menu in the Service details view.' + } +); + export function Contents({ selectedNodeData, isService, @@ -58,7 +123,23 @@ export function Contents({ onFocusClick, selectedNodeServiceName }: ContentsProps) { - const frameworkName = selectedNodeData[SERVICE_FRAMEWORK_NAME]; + // Anomaly Detection + const severity = selectedNodeData.severity; + const maxScore = selectedNodeData.max_score; + const actualValue = selectedNodeData.actual_value; + const typicalValue = selectedNodeData.typical_value; + const jobId = selectedNodeData.job_id; + const hasAnomalyDetection = [ + severity, + maxScore, + actualValue, + typicalValue, + jobId + ].every(value => value !== undefined); + const anomalyDescription = hasAnomalyDetection + ? getMetricChangeDescription(actualValue, typicalValue).message + : null; + return ( + {isService && ( + +
+ +

{ANOMALY_DETECTION_TITLE}

+
+   + +
+ {hasAnomalyDetection ? ( + <> + + + + + + + {ANOMALY_DETECTION_SCORE_METRIC} + + + + +
+ {asInteger(maxScore)} +  ({anomalyDescription}) +
+
+
+
+ + + {ANOMALY_DETECTION_LINK} + + + + ) : ( + {ANOMALY_DETECTION_ENABLE_TEXT} + )} + +
+ )} {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 23e9e737be9a65..e5962afd76eb8d 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 @@ -16,7 +16,6 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) avgRequestsPerMinute={164.47222031860858} avgCpuUsage={0.32809666568309237} avgMemoryUsage={0.5504868173242986} - frameworkName="Spring" numInstances={2} isLoading={false} /> 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 5e6412333a2e18..6f67f7a4bed7af 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 @@ -11,12 +11,10 @@ import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ServiceMetricList } from './ServiceMetricList'; interface ServiceMetricFetcherProps { - frameworkName?: string; serviceName: string; } export function ServiceMetricFetcher({ - frameworkName, serviceName }: ServiceMetricFetcherProps) { const { @@ -39,11 +37,5 @@ export function ServiceMetricFetcher({ ); const isLoading = status === 'loading'; - return ( - - ); + 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 3cee986261a68b..5c28fc0a5a7d0e 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 @@ -34,21 +34,20 @@ const BadgeRow = styled(EuiFlexItem)` padding-bottom: ${lightTheme.gutterTypes.gutterSmall}; `; -const ItemRow = styled('tr')` +export const ItemRow = styled('tr')` line-height: 2; `; -const ItemTitle = styled('td')` +export const ItemTitle = styled('td')` color: ${lightTheme.textColors.subdued}; padding-right: 1rem; `; -const ItemDescription = styled('td')` +export const ItemDescription = styled('td')` text-align: right; `; interface ServiceMetricListProps extends ServiceNodeMetrics { - frameworkName?: string; isLoading: boolean; } @@ -58,7 +57,6 @@ export function ServiceMetricList({ avgErrorsPerMinute, avgCpuUsage, avgMemoryUsage, - frameworkName, numInstances, isLoading }: ServiceMetricListProps) { @@ -112,7 +110,7 @@ export function ServiceMetricList({ : null } ]; - const showBadgeRow = frameworkName || numInstances > 1; + const showBadgeRow = numInstances > 1; return isLoading ? ( @@ -121,7 +119,6 @@ export function ServiceMetricList({ {showBadgeRow && ( - {frameworkName && {frameworkName}} {numInstances > 1 && ( {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json index 4b4b5c2ed802e8..e55ba65bcbcb91 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json @@ -3,38 +3,19 @@ { "data": { "source": "apm-server", - "target": ">172.17.0.1", - "id": "apm-server~>172.17.0.1", + "target": ">elasticsearch", + "id": "apm-server~>elasticsearch", "sourceData": { - "service.environment": null, + "id": "apm-server", "service.name": "apm-server", - "agent.name": "go", - "id": "apm-server" - }, - "targetData": { - "destination.address": "172.17.0.1", - "span.subtype": "http", - "span.type": "external", - "id": ">172.17.0.1" - } - } - }, - { - "data": { - "source": "client", - "target": ">opbeans-node", - "id": "client~>opbeans-node", - "sourceData": { - "service.environment": null, - "service.name": "client", - "agent.name": "js-base", - "id": "client" + "agent.name": "go" }, "targetData": { - "destination.address": "opbeans-node", - "span.subtype": null, - "span.type": "resource", - "id": ">opbeans-node" + "span.subtype": "elasticsearch", + "span.destination.service.resource": "elasticsearch", + "span.type": "db", + "id": ">elasticsearch", + "label": "elasticsearch" } } }, @@ -44,135 +25,39 @@ "target": "opbeans-node", "id": "client~opbeans-node", "sourceData": { - "service.environment": null, + "id": "client", "service.name": "client", - "agent.name": "js-base", - "id": "client" + "agent.name": "rum-js" }, "targetData": { + "id": "opbeans-node", "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "id": "opbeans-node" + "service.framework.name": "express" } } }, - { - "data": { - "source": "opbeans-dotnet", - "target": "opbeans-go", - "id": "opbeans-dotnet~opbeans-go", - "sourceData": { - "service.environment": "production", - "service.name": "opbeans-dotnet", - "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "targetData": { - "service.environment": "production", - "service.name": "opbeans-go", - "agent.name": "go", - "id": "opbeans-go" - }, - "bidirectional": true - } - }, - { - "data": { - "source": "opbeans-dotnet", - "target": "opbeans-java", - "id": "opbeans-dotnet~opbeans-java", - "sourceData": { - "service.environment": "production", - "service.name": "opbeans-dotnet", - "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "targetData": { - "service.environment": "production", - "service.name": "opbeans-java", - "agent.name": "java", - "id": "opbeans-java" - }, - "bidirectional": true - } - }, - { - "data": { - "source": "opbeans-dotnet", - "target": "opbeans-node", - "id": "opbeans-dotnet~opbeans-node", - "sourceData": { - "service.environment": "production", - "service.name": "opbeans-dotnet", - "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "targetData": { - "service.environment": "production", - "service.name": "opbeans-node", - "agent.name": "nodejs", - "id": "opbeans-node" - }, - "bidirectional": true - } - }, - { - "data": { - "source": "opbeans-dotnet", - "target": "opbeans-python", - "id": "opbeans-dotnet~opbeans-python", - "sourceData": { - "service.environment": "production", - "service.name": "opbeans-dotnet", - "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "targetData": { - "service.environment": "production", - "service.name": "opbeans-python", - "agent.name": "python", - "id": "opbeans-python" - }, - "bidirectional": true - } - }, - { - "data": { - "source": "opbeans-dotnet", - "target": "opbeans-ruby", - "id": "opbeans-dotnet~opbeans-ruby", - "sourceData": { - "service.environment": "production", - "service.name": "opbeans-dotnet", - "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "targetData": { - "service.environment": "production", - "service.name": "opbeans-ruby", - "agent.name": "ruby", - "id": "opbeans-ruby" - }, - "bidirectional": true - } - }, { "data": { "source": "opbeans-go", - "target": ">postgres", - "id": "opbeans-go~>postgres", + "target": ">postgresql", + "id": "opbeans-go~>postgresql", "sourceData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "targetData": { - "destination.address": "postgres", "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", "span.type": "db", - "id": ">postgres" + "id": ">postgresql", + "label": "postgresql" } } }, @@ -182,18 +67,22 @@ "target": "opbeans-dotnet", "id": "opbeans-go~opbeans-dotnet", "sourceData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "targetData": { - "service.environment": "production", + "id": "opbeans-dotnet", "service.name": "opbeans-dotnet", "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "isInverseEdge": true + "service.framework.name": "ASP.NET Core", + "max_score": 43.63972429875661, + "severity": "minor" + } } }, { @@ -202,16 +91,21 @@ "target": "opbeans-java", "id": "opbeans-go~opbeans-java", "sourceData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "targetData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "bidirectional": true } @@ -222,16 +116,20 @@ "target": "opbeans-node", "id": "opbeans-go~opbeans-node", "sourceData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "targetData": { + "id": "opbeans-node", "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "id": "opbeans-node" + "service.framework.name": "express" }, "bidirectional": true } @@ -242,16 +140,22 @@ "target": "opbeans-python", "id": "opbeans-go~opbeans-python", "sourceData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "targetData": { + "id": "opbeans-python", "service.environment": "production", "service.name": "opbeans-python", "agent.name": "python", - "id": "opbeans-python" + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" }, "bidirectional": true } @@ -262,16 +166,20 @@ "target": "opbeans-ruby", "id": "opbeans-go~opbeans-ruby", "sourceData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "targetData": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" + "service.framework.name": "Ruby on Rails" }, "bidirectional": true } @@ -279,19 +187,22 @@ { "data": { "source": "opbeans-java", - "target": ">postgres", - "id": "opbeans-java~>postgres", + "target": ">postgresql", + "id": "opbeans-java~>postgresql", "sourceData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "targetData": { - "destination.address": "postgres", "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", "span.type": "db", - "id": ">postgres" + "id": ">postgresql", + "label": "postgresql" } } }, @@ -301,18 +212,21 @@ "target": "opbeans-dotnet", "id": "opbeans-java~opbeans-dotnet", "sourceData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "targetData": { - "service.environment": "production", + "id": "opbeans-dotnet", "service.name": "opbeans-dotnet", "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "isInverseEdge": true + "service.framework.name": "ASP.NET Core", + "max_score": 43.63972429875661, + "severity": "minor" + } } }, { @@ -321,16 +235,21 @@ "target": "opbeans-go", "id": "opbeans-java~opbeans-go", "sourceData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "targetData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "isInverseEdge": true } @@ -341,16 +260,19 @@ "target": "opbeans-node", "id": "opbeans-java~opbeans-node", "sourceData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "targetData": { + "id": "opbeans-node", "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "id": "opbeans-node" + "service.framework.name": "express" }, "bidirectional": true } @@ -361,16 +283,21 @@ "target": "opbeans-python", "id": "opbeans-java~opbeans-python", "sourceData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "targetData": { + "id": "opbeans-python", "service.environment": "production", "service.name": "opbeans-python", "agent.name": "python", - "id": "opbeans-python" + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" }, "bidirectional": true } @@ -381,56 +308,43 @@ "target": "opbeans-ruby", "id": "opbeans-java~opbeans-ruby", "sourceData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "targetData": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" + "service.framework.name": "Ruby on Rails" }, "bidirectional": true } }, - { - "data": { - "source": "opbeans-node", - "target": "opbeans-dotnet", - "id": "opbeans-node~opbeans-dotnet", - "sourceData": { - "service.environment": "production", - "service.name": "opbeans-node", - "agent.name": "nodejs", - "id": "opbeans-node" - }, - "targetData": { - "service.environment": "production", - "service.name": "opbeans-dotnet", - "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "isInverseEdge": true - } - }, { "data": { "source": "opbeans-node", "target": "opbeans-go", "id": "opbeans-node~opbeans-go", "sourceData": { + "id": "opbeans-node", "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "id": "opbeans-node" + "service.framework.name": "express" }, "targetData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "isInverseEdge": true } @@ -441,16 +355,19 @@ "target": "opbeans-java", "id": "opbeans-node~opbeans-java", "sourceData": { + "id": "opbeans-node", "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "id": "opbeans-node" + "service.framework.name": "express" }, "targetData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "isInverseEdge": true } @@ -461,16 +378,20 @@ "target": "opbeans-python", "id": "opbeans-node~opbeans-python", "sourceData": { + "id": "opbeans-node", "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "id": "opbeans-node" + "service.framework.name": "express" }, "targetData": { + "id": "opbeans-python", "service.environment": "production", "service.name": "opbeans-python", "agent.name": "python", - "id": "opbeans-python" + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" }, "bidirectional": true } @@ -481,38 +402,113 @@ "target": "opbeans-ruby", "id": "opbeans-node~opbeans-ruby", "sourceData": { + "id": "opbeans-node", "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "id": "opbeans-node" + "service.framework.name": "express" }, "targetData": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" + "service.framework.name": "Ruby on Rails" }, "bidirectional": true } }, + { + "data": { + "source": "opbeans-python", + "target": ">elasticsearch", + "id": "opbeans-python~>elasticsearch", + "sourceData": { + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python", + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" + }, + "targetData": { + "span.subtype": "elasticsearch", + "span.destination.service.resource": "elasticsearch", + "span.type": "db", + "id": ">elasticsearch", + "label": "elasticsearch" + } + } + }, + { + "data": { + "source": "opbeans-python", + "target": ">postgresql", + "id": "opbeans-python~>postgresql", + "sourceData": { + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python", + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" + }, + "targetData": { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db", + "id": ">postgresql", + "label": "postgresql" + } + } + }, + { + "data": { + "source": "opbeans-python", + "target": ">redis", + "id": "opbeans-python~>redis", + "sourceData": { + "id": "opbeans-python", + "service.environment": "production", + "service.name": "opbeans-python", + "agent.name": "python", + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" + }, + "targetData": { + "span.subtype": "redis", + "span.destination.service.resource": "redis", + "span.type": "db", + "id": ">redis", + "label": "redis" + } + } + }, { "data": { "source": "opbeans-python", "target": "opbeans-dotnet", "id": "opbeans-python~opbeans-dotnet", "sourceData": { + "id": "opbeans-python", "service.environment": "production", "service.name": "opbeans-python", "agent.name": "python", - "id": "opbeans-python" + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" }, "targetData": { - "service.environment": "production", + "id": "opbeans-dotnet", "service.name": "opbeans-dotnet", "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "isInverseEdge": true + "service.framework.name": "ASP.NET Core", + "max_score": 43.63972429875661, + "severity": "minor" + } } }, { @@ -521,16 +517,22 @@ "target": "opbeans-go", "id": "opbeans-python~opbeans-go", "sourceData": { + "id": "opbeans-python", "service.environment": "production", "service.name": "opbeans-python", "agent.name": "python", - "id": "opbeans-python" + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" }, "targetData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "isInverseEdge": true } @@ -541,16 +543,21 @@ "target": "opbeans-java", "id": "opbeans-python~opbeans-java", "sourceData": { + "id": "opbeans-python", "service.environment": "production", "service.name": "opbeans-python", "agent.name": "python", - "id": "opbeans-python" + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" }, "targetData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "isInverseEdge": true } @@ -561,16 +568,20 @@ "target": "opbeans-node", "id": "opbeans-python~opbeans-node", "sourceData": { + "id": "opbeans-python", "service.environment": "production", "service.name": "opbeans-python", "agent.name": "python", - "id": "opbeans-python" + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" }, "targetData": { + "id": "opbeans-node", "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "id": "opbeans-node" + "service.framework.name": "express" }, "isInverseEdge": true } @@ -581,38 +592,65 @@ "target": "opbeans-ruby", "id": "opbeans-python~opbeans-ruby", "sourceData": { + "id": "opbeans-python", "service.environment": "production", "service.name": "opbeans-python", "agent.name": "python", - "id": "opbeans-python" + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" }, "targetData": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" + "service.framework.name": "Ruby on Rails" }, "bidirectional": true } }, + { + "data": { + "source": "opbeans-ruby", + "target": ">postgresql", + "id": "opbeans-ruby~>postgresql", + "sourceData": { + "id": "opbeans-ruby", + "service.environment": "production", + "service.name": "opbeans-ruby", + "agent.name": "ruby", + "service.framework.name": "Ruby on Rails" + }, + "targetData": { + "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", + "span.type": "db", + "id": ">postgresql", + "label": "postgresql" + } + } + }, { "data": { "source": "opbeans-ruby", "target": "opbeans-dotnet", "id": "opbeans-ruby~opbeans-dotnet", "sourceData": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" + "service.framework.name": "Ruby on Rails" }, "targetData": { - "service.environment": "production", + "id": "opbeans-dotnet", "service.name": "opbeans-dotnet", "agent.name": "dotnet", - "id": "opbeans-dotnet" - }, - "isInverseEdge": true + "service.framework.name": "ASP.NET Core", + "max_score": 43.63972429875661, + "severity": "minor" + } } }, { @@ -621,16 +659,20 @@ "target": "opbeans-go", "id": "opbeans-ruby~opbeans-go", "sourceData": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" + "service.framework.name": "Ruby on Rails" }, "targetData": { + "id": "opbeans-go", "service.environment": "production", "service.name": "opbeans-go", "agent.name": "go", - "id": "opbeans-go" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" }, "isInverseEdge": true } @@ -641,16 +683,19 @@ "target": "opbeans-java", "id": "opbeans-ruby~opbeans-java", "sourceData": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" + "service.framework.name": "Ruby on Rails" }, "targetData": { + "id": "opbeans-java", "service.environment": "production", "service.name": "opbeans-java", "agent.name": "java", - "id": "opbeans-java" + "max_score": 31.374423806075157, + "severity": "minor" }, "isInverseEdge": true } @@ -661,16 +706,18 @@ "target": "opbeans-node", "id": "opbeans-ruby~opbeans-node", "sourceData": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" + "service.framework.name": "Ruby on Rails" }, "targetData": { + "id": "opbeans-node", "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "id": "opbeans-node" + "service.framework.name": "express" }, "isInverseEdge": true } @@ -681,178 +728,123 @@ "target": "opbeans-python", "id": "opbeans-ruby~opbeans-python", "sourceData": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" + "service.framework.name": "Ruby on Rails" }, "targetData": { + "id": "opbeans-python", "service.environment": "production", "service.name": "opbeans-python", "agent.name": "python", - "id": "opbeans-python" + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" }, "isInverseEdge": true } }, { "data": { - "service.environment": null, - "service.name": "client", - "agent.name": "js-base", - "id": "client" - } - }, - { - "data": { - "service.environment": "production", - "service.name": "opbeans-node", - "agent.name": "nodejs", - "id": "opbeans-node" - } - }, - { - "data": { + "id": "opbeans-java", "service.environment": "production", - "service.name": "opbeans-go", - "agent.name": "go", - "id": "opbeans-go" + "service.name": "opbeans-java", + "agent.name": "java", + "max_score": 31.374423806075157, + "severity": "minor" } }, { "data": { + "id": "opbeans-python", "service.environment": "production", - "service.name": "opbeans-java", - "agent.name": "java", - "id": "opbeans-java" + "service.name": "opbeans-python", + "agent.name": "python", + "service.framework.name": "django", + "max_score": 92.48735, + "severity": "critical" } }, { "data": { - "destination.address": "postgres", "span.subtype": "postgresql", + "span.destination.service.resource": "postgresql", "span.type": "db", - "id": ">postgres" + "id": ">postgresql", + "label": "postgresql" } }, { "data": { + "id": "opbeans-ruby", "service.environment": "production", "service.name": "opbeans-ruby", "agent.name": "ruby", - "id": "opbeans-ruby" - } - }, - { - "data": { - "service.environment": "production", - "service.name": "opbeans-dotnet", - "agent.name": "dotnet", - "id": "opbeans-dotnet" + "service.framework.name": "Ruby on Rails" } }, { "data": { + "id": "opbeans-go", "service.environment": "production", - "service.name": "opbeans-python", - "agent.name": "python", - "id": "opbeans-python" - } - }, - { - "data": { - "destination.address": "opbeans-node", - "span.subtype": null, - "span.type": "resource", - "id": ">opbeans-node" - } - }, - { - "data": { - "service.environment": null, - "service.name": "apm-server", + "service.name": "opbeans-go", "agent.name": "go", - "id": "apm-server" - } - }, - { - "data": { - "destination.address": "172.17.0.1", - "span.subtype": "http", - "span.type": "external", - "id": ">172.17.0.1" + "service.framework.name": "gin", + "max_score": 92.40731, + "severity": "critical" } }, { "data": { + "id": "apm-server", "service.name": "apm-server", - "agent.name": "go", - "service.environment": null, - "service.framework.name": null, - "id": "apm-server" - } - }, - { - "data": { - "service.name": "opbeans-python", - "agent.name": "python", - "service.environment": null, - "service.framework.name": "django", - "id": "opbeans-python" + "agent.name": "go" } }, { "data": { - "service.name": "opbeans-ruby", - "agent.name": "ruby", - "service.environment": null, - "service.framework.name": "Ruby on Rails", - "id": "opbeans-ruby" + "span.subtype": "elasticsearch", + "span.destination.service.resource": "elasticsearch", + "span.type": "db", + "id": ">elasticsearch", + "label": "elasticsearch" } }, { "data": { + "id": "opbeans-node", + "service.environment": "production", "service.name": "opbeans-node", "agent.name": "nodejs", - "service.environment": null, - "service.framework.name": "express", - "id": "opbeans-node" + "service.framework.name": "express" } }, { "data": { - "service.name": "opbeans-go", - "agent.name": "go", - "service.environment": null, - "service.framework.name": "gin", - "id": "opbeans-go" - } - }, - { - "data": { - "service.name": "opbeans-java", - "agent.name": "java", - "service.environment": null, - "service.framework.name": null, - "id": "opbeans-java" + "id": "opbeans-dotnet", + "service.name": "opbeans-dotnet", + "agent.name": "dotnet", + "service.framework.name": "ASP.NET Core", + "max_score": 43.63972429875661, + "severity": "minor" } }, { "data": { - "service.name": "opbeans-dotnet", - "agent.name": "dotnet", - "service.environment": null, - "service.framework.name": "ASP.NET Core", - "id": "opbeans-dotnet" + "span.subtype": "redis", + "span.destination.service.resource": "redis", + "span.type": "db", + "id": ">redis", + "label": "redis" } }, { "data": { + "id": "client", "service.name": "client", - "agent.name": "js-base", - "service.environment": null, - "service.framework.name": null, - "id": "client" + "agent.name": "rum-js" } } ] 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 3bb4319d0722dd..0cdc7c4eb124d5 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -13,9 +13,7 @@ import { import { severity } from '../../../../common/ml_job_constants'; import { defaultIcon, iconForNode } from './icons'; -const getBorderColor = (el: cytoscape.NodeSingular) => { - const nodeSeverity = el.data('severity'); - +export const getSeverityColor = (nodeSeverity: string) => { switch (nodeSeverity) { case severity.warning: return theme.euiColorVis0; @@ -24,11 +22,20 @@ const getBorderColor = (el: cytoscape.NodeSingular) => { case severity.critical: return theme.euiColorVis9; default: - if (el.hasClass('primary') || el.selected()) { - return theme.euiColorPrimary; - } else { - return theme.euiColorMediumShade; - } + return; + } +}; + +const getBorderColor = (el: cytoscape.NodeSingular) => { + const nodeSeverity = el.data('severity'); + const severityColor = getSeverityColor(nodeSeverity); + if (severityColor) { + return severityColor; + } + if (el.hasClass('primary') || el.selected()) { + return theme.euiColorPrimary; + } else { + return theme.euiColorMediumShade; } }; 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 75a247a1aae40b..9065f20ae600e6 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 @@ -10,7 +10,7 @@ import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLJobLink } from './MLJobLink'; describe('MLJobLink', () => { - it('should produce the correct URL', async () => { + it('should produce the correct URL with serviceName', async () => { const href = await getRenderedHref( () => ( { { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location ); + 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))` + ); + }); + it('should produce the correct URL with jobId', async () => { + const href = await getRenderedHref( + () => ( + + ), + { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location + ); + 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))` ); 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 81c5d17d491c06..b085fab2b7ed66 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 @@ -8,22 +8,33 @@ import React from 'react'; import { getMlJobId } from '../../../../../common/ml_job_constants'; import { MLLink } from './MLLink'; -interface Props { +interface PropsServiceName { serviceName: string; transactionType?: string; } +interface PropsJobId { + jobId: string; +} + +type Props = (PropsServiceName | PropsJobId) & { + external?: boolean; +}; -export const MLJobLink: React.FC = ({ - serviceName, - transactionType, - children -}) => { - const jobId = getMlJobId(serviceName, transactionType); +export const MLJobLink: React.FC = props => { + const jobId = + 'jobId' in props + ? props.jobId + : getMlJobId(props.serviceName, props.transactionType); const query = { ml: { jobIds: [jobId] } }; return ( - + ); }; diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx index 3671a0089fd6e2..7b57c193d896d2 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx @@ -22,9 +22,10 @@ interface Props { query?: MlRisonData; path?: string; children?: React.ReactNode; + external?: boolean; } -export function MLLink({ children, path = '', query = {} }: Props) { +export function MLLink({ children, path = '', query = {}, external }: Props) { const { core } = useApmPluginContext(); const location = useLocation(); @@ -41,5 +42,12 @@ export function MLLink({ children, path = '', query = {} }: Props) { hash: `${path}?_g=${rison.encode(risonQuery as RisonValue)}` }); - return ; + return ( + + ); } diff --git a/x-pack/plugins/apm/server/lib/helpers/range_filter.ts b/x-pack/plugins/apm/server/lib/helpers/range_filter.ts index 39581687f04f2b..0647144a19c1dc 100644 --- a/x-pack/plugins/apm/server/lib/helpers/range_filter.ts +++ b/x-pack/plugins/apm/server/lib/helpers/range_filter.ts @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export function rangeFilter(start: number, end: number) { +export function rangeFilter( + start: number, + end: number, + timestampField = '@timestamp' +) { return { - '@timestamp': { + [timestampField]: { gte: start, lte: end, format: 'epoch_millis' diff --git a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts b/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts index 572d73e368c7ad..4af8a541392042 100644 --- a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts @@ -9,7 +9,6 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, SERVICE_NAME, SERVICE_ENVIRONMENT, - SERVICE_FRAMEWORK_NAME, AGENT_NAME, SPAN_TYPE, SPAN_SUBTYPE @@ -19,7 +18,6 @@ import { dedupeConnections } from './'; const nodejsService = { [SERVICE_NAME]: 'opbeans-node', [SERVICE_ENVIRONMENT]: 'production', - [SERVICE_FRAMEWORK_NAME]: null, [AGENT_NAME]: 'nodejs' }; @@ -32,7 +30,6 @@ const nodejsExternal = { const javaService = { [SERVICE_NAME]: 'opbeans-java', [SERVICE_ENVIRONMENT]: 'production', - [SERVICE_FRAMEWORK_NAME]: null, [AGENT_NAME]: 'java' }; 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 adb2c9b7cb084c..7d5f0a75d2208c 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 @@ -7,7 +7,6 @@ import { chunk } from 'lodash'; import { AGENT_NAME, SERVICE_ENVIRONMENT, - SERVICE_FRAMEWORK_NAME, SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { getServicesProjection } from '../../../common/projections/services'; @@ -19,6 +18,7 @@ import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; import { addAnomaliesToServicesData } from './ml_helpers'; import { getMlIndex } from '../../../common/ml_job_constants'; +import { rangeFilter } from '../helpers/range_filter'; export interface IEnvOptions { setup: Setup & SetupTimeRange; @@ -107,11 +107,6 @@ async function getServicesData(options: IEnvOptions) { terms: { field: AGENT_NAME } - }, - service_framework_name: { - terms: { - field: SERVICE_FRAMEWORK_NAME - } } } } @@ -129,38 +124,32 @@ async function getServicesData(options: IEnvOptions) { [SERVICE_NAME]: bucket.key as string, [AGENT_NAME]: (bucket.agent_name.buckets[0]?.key as string | undefined) || '', - [SERVICE_ENVIRONMENT]: options.environment || null, - [SERVICE_FRAMEWORK_NAME]: - (bucket.service_framework_name.buckets[0]?.key as - | string - | undefined) || null + [SERVICE_ENVIRONMENT]: options.environment || null }; }) || [] ); } function getAnomaliesData(options: IEnvOptions) { - const { client } = options.setup; + const { start, end, client } = options.setup; + const rangeQuery = { range: rangeFilter(start, end, 'timestamp') }; const params = { index: getMlIndex('*'), body: { size: 0, query: { - exists: { - field: 'bucket_span' - } + bool: { filter: [{ term: { result_type: 'record' } }, rangeQuery] } }, aggs: { jobs: { - terms: { - field: 'job_id', - size: 10 - }, + terms: { field: 'job_id', size: 10 }, aggs: { - max_score: { - max: { - field: 'anomaly_score' + top_score_hits: { + top_hits: { + sort: [{ record_score: { order: 'desc' as const } }], + _source: ['job_id', 'record_score', 'typical', 'actual'], + size: 1 } } } @@ -178,7 +167,13 @@ export type ServicesResponse = PromiseReturnType; export type ServiceMapAPIResponse = PromiseReturnType; export async function getServiceMap(options: IEnvOptions) { - const [connectionData, servicesData, anomaliesData] = await Promise.all([ + const [connectionData, servicesData, anomaliesData]: [ + // explicit types to avoid TS "excessively deep" error + ConnectionsResponse, + ServicesResponse, + AnomaliesResponse + // @ts-ignore + ] = await Promise.all([ getConnectionData(options), getServicesData(options), getAnomaliesData(options) diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts index c6680ecd6375bf..c80ba8dba01eaa 100644 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts @@ -13,14 +13,12 @@ describe('addAnomaliesToServicesData', () => { { 'service.name': 'opbeans-ruby', 'agent.name': 'ruby', - 'service.environment': null, - 'service.framework.name': 'Ruby on Rails' + 'service.environment': null }, { 'service.name': 'opbeans-java', 'agent.name': 'java', - 'service.environment': null, - 'service.framework.name': null + 'service.environment': null } ]; @@ -30,11 +28,37 @@ describe('addAnomaliesToServicesData', () => { buckets: [ { key: 'opbeans-ruby-request-high_mean_response_time', - max_score: { value: 50 } + top_score_hits: { + hits: { + hits: [ + { + _source: { + record_score: 50, + actual: [2000], + typical: [1000], + job_id: 'opbeans-ruby-request-high_mean_response_time' + } + } + ] + } + } }, { key: 'opbeans-java-request-high_mean_response_time', - max_score: { value: 100 } + top_score_hits: { + hits: { + hits: [ + { + _source: { + record_score: 100, + actual: [9000], + typical: [3000], + job_id: 'opbeans-java-request-high_mean_response_time' + } + } + ] + } + } } ] } @@ -46,17 +70,21 @@ describe('addAnomaliesToServicesData', () => { 'service.name': 'opbeans-ruby', 'agent.name': 'ruby', 'service.environment': null, - 'service.framework.name': 'Ruby on Rails', max_score: 50, - severity: 'major' + severity: 'major', + actual_value: 2000, + typical_value: 1000, + job_id: 'opbeans-ruby-request-high_mean_response_time' }, { 'service.name': 'opbeans-java', 'agent.name': 'java', 'service.environment': null, - 'service.framework.name': null, max_score: 100, - severity: 'critical' + severity: 'critical', + actual_value: 9000, + typical_value: 3000, + job_id: 'opbeans-java-request-high_mean_response_time' } ]; diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts index 26a964bfb4dd21..9789911660bd03 100644 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts +++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts @@ -18,29 +18,54 @@ export function addAnomaliesToServicesData( const anomaliesMap = ( anomaliesResponse.aggregations?.jobs.buckets ?? [] ).reduce<{ - [key: string]: { max_score?: number }; + [key: string]: { + max_score?: number; + actual_value?: number; + typical_value?: number; + job_id?: string; + }; }>((previousValue, currentValue) => { const key = getMlJobServiceName(currentValue.key.toString()); + const hitSource = currentValue.top_score_hits.hits.hits[0]._source as { + record_score: number; + actual: [number]; + typical: [number]; + job_id: string; + }; + const maxScore = hitSource.record_score; + const actualValue = hitSource.actual[0]; + const typicalValue = hitSource.typical[0]; + const jobId = hitSource.job_id; + + if ((previousValue[key]?.max_score ?? 0) > maxScore) { + return previousValue; + } return { ...previousValue, [key]: { - max_score: Math.max( - previousValue[key]?.max_score ?? 0, - currentValue.max_score.value ?? 0 - ) + max_score: maxScore, + actual_value: actualValue, + typical_value: typicalValue, + job_id: jobId } }; }, {}); const servicesDataWithAnomalies = servicesData.map(service => { - const score = anomaliesMap[service[SERVICE_NAME]]?.max_score; - - return { - ...service, - max_score: score, - severity: getSeverity(score) - }; + const serviceAnomalies = anomaliesMap[service[SERVICE_NAME]]; + if (serviceAnomalies) { + const maxScore = serviceAnomalies.max_score; + return { + ...service, + max_score: maxScore, + severity: getSeverity(maxScore), + actual_value: serviceAnomalies.actual_value, + typical_value: serviceAnomalies.typical_value, + job_id: serviceAnomalies.job_id + }; + } + return service; }); return servicesDataWithAnomalies; diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 8070f94a1264dd..c23d042822816d 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -13,6 +13,7 @@ import { MlSetupDependencies, MlStartDependencies, } from './plugin'; +import { getMetricChangeDescription } from './application/formatters/metric_change_description'; export const plugin: PluginInitializer< MlPluginSetup, @@ -21,4 +22,4 @@ export const plugin: PluginInitializer< MlStartDependencies > = () => new MlPlugin(); -export { MlPluginSetup, MlPluginStart }; +export { MlPluginSetup, MlPluginStart, getMetricChangeDescription };