mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[APM] Service maps anomaly detection status in popover (#65217)
* Adds ML status to the service maps popover. Modifies the query a bit to get the actual vs typical values necessary to generate a proper anomaly description * fixed failures and updated tests * component clean up * makes the ML link open in a new window * - Closes #64278 by removing the framework badge. - changes anomaly score display to integer formatting - update link test to 'View anomalies' * - Closes #65244 by displaying a message for services without anomalies detected - removes unecessary service framework name in service map queries - adds date range filter for anomaly detection
This commit is contained in:
parent
bec09fde87
commit
4896b65c98
17 changed files with 624 additions and 434 deletions
|
@ -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 {
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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 ? <div {...props} /> : <EuiFlexItem {...props} />;
|
||||
|
||||
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 (
|
||||
<FlexColumnGroup
|
||||
direction="column"
|
||||
|
@ -71,12 +152,50 @@ export function Contents({
|
|||
</EuiTitle>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
</FlexColumnItem>
|
||||
{isService && (
|
||||
<FlexColumnItem>
|
||||
<section>
|
||||
<HealthStatusTitle size="xxs">
|
||||
<h3>{ANOMALY_DETECTION_TITLE}</h3>
|
||||
</HealthStatusTitle>
|
||||
|
||||
<EuiIconTip type="iInCircle" content={ANOMALY_DETECTION_INFO} />
|
||||
</section>
|
||||
{hasAnomalyDetection ? (
|
||||
<>
|
||||
<ContentLine>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<VerticallyCentered>
|
||||
<EuiHealth color={getSeverityColor(severity)} />
|
||||
<SubduedText>
|
||||
{ANOMALY_DETECTION_SCORE_METRIC}
|
||||
</SubduedText>
|
||||
</VerticallyCentered>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div>
|
||||
{asInteger(maxScore)}
|
||||
<SubduedText> ({anomalyDescription})</SubduedText>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</ContentLine>
|
||||
<ContentLine>
|
||||
<MLJobLink external jobId={jobId}>
|
||||
{ANOMALY_DETECTION_LINK}
|
||||
</MLJobLink>
|
||||
</ContentLine>
|
||||
</>
|
||||
) : (
|
||||
<EnableText>{ANOMALY_DETECTION_ENABLE_TEXT}</EnableText>
|
||||
)}
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
</FlexColumnItem>
|
||||
)}
|
||||
<FlexColumnItem>
|
||||
{isService ? (
|
||||
<ServiceMetricFetcher
|
||||
frameworkName={frameworkName}
|
||||
serviceName={selectedNodeServiceName}
|
||||
/>
|
||||
<ServiceMetricFetcher serviceName={selectedNodeServiceName} />
|
||||
) : (
|
||||
<Info {...selectedNodeData} />
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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 (
|
||||
<ServiceMetricList
|
||||
{...data}
|
||||
frameworkName={frameworkName}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
return <ServiceMetricList {...data} isLoading={isLoading} />;
|
||||
}
|
||||
|
|
|
@ -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 ? (
|
||||
<LoadingSpinner />
|
||||
|
@ -121,7 +119,6 @@ export function ServiceMetricList({
|
|||
{showBadgeRow && (
|
||||
<BadgeRow>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
{frameworkName && <EuiBadge>{frameworkName}</EuiBadge>}
|
||||
{numInstances > 1 && (
|
||||
<EuiBadge iconType="apps" color="hollow">
|
||||
{i18n.translate('xpack.apm.serviceMap.numInstancesMetric', {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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(
|
||||
() => (
|
||||
<MLJobLink
|
||||
|
@ -21,6 +21,18 @@ describe('MLJobLink', () => {
|
|||
{ 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(
|
||||
() => (
|
||||
<MLJobLink jobId="myservicename-mytransactiontype-high_mean_response_time" />
|
||||
),
|
||||
{ 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))`
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
export const MLJobLink: React.FC<Props> = ({
|
||||
serviceName,
|
||||
transactionType,
|
||||
children
|
||||
}) => {
|
||||
const jobId = getMlJobId(serviceName, transactionType);
|
||||
type Props = (PropsServiceName | PropsJobId) & {
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
export const MLJobLink: React.FC<Props> = props => {
|
||||
const jobId =
|
||||
'jobId' in props
|
||||
? props.jobId
|
||||
: getMlJobId(props.serviceName, props.transactionType);
|
||||
const query = {
|
||||
ml: { jobIds: [jobId] }
|
||||
};
|
||||
|
||||
return (
|
||||
<MLLink children={children} query={query} path="/timeseriesexplorer" />
|
||||
<MLLink
|
||||
children={props.children}
|
||||
query={query}
|
||||
path="/timeseriesexplorer"
|
||||
external={props.external}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 <EuiLink children={children} href={href} />;
|
||||
return (
|
||||
<EuiLink
|
||||
children={children}
|
||||
href={href}
|
||||
external={external}
|
||||
target={external ? '_blank' : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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<typeof getServicesData>;
|
|||
export type ServiceMapAPIResponse = PromiseReturnType<typeof getServiceMap>;
|
||||
|
||||
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)
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue