Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] Service maps - Fix missing ML status for services with jobs but no anomalies #68486

Merged
Merged
2 changes: 2 additions & 0 deletions x-pack/plugins/apm/common/ml_job_constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export enum severity {
warning = 'warning',
}

export const APM_ML_JOB_GROUP_NAME = 'apm';

export function getMlPrefix(serviceName: string, transactionType?: string) {
const maybeTransactionType = transactionType ? `${transactionType}-` : '';
return encodeForMlApi(`${serviceName}-${maybeTransactionType}`);
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/apm/common/service_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ export interface Connection {
destination: ConnectionNode;
}

export interface ServiceAnomaly {
anomaly_score: number;
anomaly_severity: string;
actual_value: number;
typical_value: number;
ml_job_id: string;
}

export type ServiceNode = ConnectionNode & Partial<ServiceAnomaly>;

export interface ServiceNodeMetrics {
avgMemoryUsage: number | null;
avgCpuUsage: number | null;
Expand Down
21 changes: 21 additions & 0 deletions x-pack/plugins/apm/common/utils/left_join.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 { Assign, Omit } from 'utility-types';

export function leftJoin<
TL extends object,
K extends keyof TL,
TR extends Pick<TL, K>
>(leftRecords: TL[], matchKey: K, rightRecords: TR[]) {
const rightLookup = new Map(
rightRecords.map((record) => [record[matchKey], record])
);
return leftRecords.map((record) => {
const matchProp = (record[matchKey] as unknown) as TR[K];
const matchingRightRecord = rightLookup.get(matchProp);
return { ...record, ...matchingRightRecord };
}) as Array<Assign<TL, Partial<Omit<TR, K>>>>;
}
3 changes: 2 additions & 1 deletion x-pack/plugins/apm/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"actions",
"alerts",
"observability",
"security"
"security",
"ml"
],
"server": true,
"ui": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,15 @@ import {
EuiFlexItem,
EuiHorizontalRule,
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, { MouseEvent } from 'react';
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;
import { AnomalyDetection } from './anomaly_detection';
import { ServiceNode } from '../../../../../common/service_map';
import { popoverMinWidth } from '../cytoscapeOptions';

interface ContentsProps {
isService: boolean;
Expand All @@ -36,31 +27,6 @@ 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.
//
Expand All @@ -85,61 +51,13 @@ const FlexColumnGroup = (props: {
const FlexColumnItem = (props: { children: React.ReactNode }) =>
isIE11 ? <div {...props} /> : <EuiFlexItem {...props} />;

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 from the Integrations menu in the Service details view.',
}
);

export function Contents({
selectedNodeData,
isService,
label,
onFocusClick,
selectedNodeServiceName,
}: ContentsProps) {
// 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 (
<FlexColumnGroup
direction="column"
Expand All @@ -154,50 +72,7 @@ export function Contents({
</FlexColumnItem>
{isService && (
<FlexColumnItem>
{hasAnomalyDetection ? (
<>
<section>
<HealthStatusTitle size="xxs">
<h3>{ANOMALY_DETECTION_TITLE}</h3>
</HealthStatusTitle>
&nbsp;
<EuiIconTip
type="iInCircle"
content={ANOMALY_DETECTION_TOOLTIP}
/>
</section>
<ContentLine>
<EuiFlexGroup>
<EuiFlexItem>
<VerticallyCentered>
<EuiHealth color={getSeverityColor(severity)} />
<SubduedText>
{ANOMALY_DETECTION_SCORE_METRIC}
</SubduedText>
</VerticallyCentered>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div>
{asInteger(maxScore)}
<SubduedText>&nbsp;({anomalyDescription})</SubduedText>
</div>
</EuiFlexItem>
</EuiFlexGroup>
</ContentLine>
<ContentLine>
<MLJobLink external jobId={jobId}>
{ANOMALY_DETECTION_LINK}
</MLJobLink>
</ContentLine>
</>
) : (
<>
<HealthStatusTitle size="xxs">
<h3>{ANOMALY_DETECTION_TITLE}</h3>
</HealthStatusTitle>
<EnableText>{ANOMALY_DETECTION_DISABLED_TEXT}</EnableText>
</>
)}
<AnomalyDetection serviceNodeData={selectedNodeData as ServiceNode} />
<EuiHorizontalRule margin="xs" />
</FlexColumnItem>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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 theme from '@elastic/eui/dist/eui_theme_light.json';
import {
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiIconTip,
EuiHealth,
} from '@elastic/eui';
import { fontSize, px } from '../../../../style/variables';
import { asInteger } from '../../../../utils/formatters';
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
import { getSeverityColor, popoverMinWidth } from '../cytoscapeOptions';
import { getMetricChangeDescription } from '../../../../../../ml/public';
import { ServiceNode } from '../../../../../common/service_map';

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

interface AnomalyDetectionProps {
serviceNodeData: cytoscape.NodeDataDefinition & ServiceNode;
}

export function AnomalyDetection({ serviceNodeData }: AnomalyDetectionProps) {
const anomalySeverity = serviceNodeData.anomaly_severity;
const anomalyScore = serviceNodeData.anomaly_score;
const actualValue = serviceNodeData.actual_value;
const typicalValue = serviceNodeData.typical_value;
const mlJobId = serviceNodeData.ml_job_id;
const hasAnomalyDetectionScore =
anomalySeverity !== undefined && anomalyScore !== undefined;
const anomalyDescription =
hasAnomalyDetectionScore &&
actualValue !== undefined &&
typicalValue !== undefined
? getMetricChangeDescription(actualValue, typicalValue).message
: null;

return (
<>
<section>
<HealthStatusTitle size="xxs">
<h3>{ANOMALY_DETECTION_TITLE}</h3>
</HealthStatusTitle>
&nbsp;
<EuiIconTip type="iInCircle" content={ANOMALY_DETECTION_TOOLTIP} />
{!mlJobId && <EnableText>{ANOMALY_DETECTION_DISABLED_TEXT}</EnableText>}
</section>
{hasAnomalyDetectionScore && (
<ContentLine>
<EuiFlexGroup>
<EuiFlexItem>
<VerticallyCentered>
<EuiHealth color={getSeverityColor(anomalySeverity)} />
<SubduedText>{ANOMALY_DETECTION_SCORE_METRIC}</SubduedText>
</VerticallyCentered>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div>
{getDisplayedAnomalyScore(anomalyScore as number)}
{anomalyDescription && (
<SubduedText>&nbsp;({anomalyDescription})</SubduedText>
)}
</div>
</EuiFlexItem>
</EuiFlexGroup>
</ContentLine>
)}
{mlJobId && !hasAnomalyDetectionScore && (
<EnableText>{ANOMALY_DETECTION_NO_DATA_TEXT}</EnableText>
)}
{mlJobId && (
<ContentLine>
<MLJobLink external jobId={mlJobId}>
{ANOMALY_DETECTION_LINK}
</MLJobLink>
</ContentLine>
)}
</>
);
}

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 from the Integrations menu in the Service details view.',
}
);

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.`,
}
);
Loading