Skip to content

Commit

Permalink
[Metrics UI] Add anomalies to timeline (#78602) (#78887)
Browse files Browse the repository at this point in the history
* Add ability to fetch anomalies by metric

* Add ability to fetch anomalies to timeline

* Show anomaly annotation on timeline

* Fix type check

* Fix typos

* Add influencers to tooltip, add legend

* Remove unused variable

* Only show anomalies with a score greater than 50

Co-authored-by: Elastic Machine <[email protected]>

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
phillipb and elasticmachine authored Sep 30, 2020
1 parent b646db8 commit 4f15a84
Show file tree
Hide file tree
Showing 16 changed files with 262 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ export const sortRT = rt.type({
});

export type Sort = rt.TypeOf<typeof sortRT>;

export const metricRT = rt.keyof({
memory_usage: null,
network_in: null,
network_out: null,
});

export type Metric = rt.TypeOf<typeof metricRT>;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import * as rt from 'io-ts';

import { timeRangeRT, routeTimingMetadataRT } from '../../shared';
import { anomalyTypeRT, paginationCursorRT, sortRT, paginationRT } from './common';
import { anomalyTypeRT, paginationCursorRT, sortRT, paginationRT, metricRT } from './common';

export const INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH =
'/api/infra/infra_ml/results/metrics_hosts_anomalies';
Expand All @@ -18,6 +18,7 @@ const metricsHostAnomalyCommonFieldsRT = rt.type({
typical: rt.number,
actual: rt.number,
type: anomalyTypeRT,
influencers: rt.array(rt.string),
duration: rt.number,
startTime: rt.number,
jobId: rt.string,
Expand Down Expand Up @@ -64,12 +65,11 @@ export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({
timeRange: timeRangeRT,
}),
rt.partial({
metric: metricRT,
// Pagination properties
pagination: paginationRT,
// Sort properties
sort: sortRT,
// // Dataset filters
// datasets: rt.array(rt.string),
}),
]),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import * as rt from 'io-ts';

import { timeRangeRT, routeTimingMetadataRT } from '../../shared';
import { paginationCursorRT, anomalyTypeRT, sortRT, paginationRT } from './common';
import { paginationCursorRT, anomalyTypeRT, sortRT, paginationRT, metricRT } from './common';

export const INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH =
'/api/infra/infra_ml/results/metrics_k8s_anomalies';
Expand All @@ -18,6 +18,7 @@ const metricsK8sAnomalyCommonFieldsRT = rt.type({
typical: rt.number,
actual: rt.number,
type: anomalyTypeRT,
influencers: rt.array(rt.string),
duration: rt.number,
startTime: rt.number,
jobId: rt.string,
Expand Down Expand Up @@ -64,6 +65,7 @@ export const getMetricsK8sAnomaliesRequestPayloadRT = rt.type({
timeRange: timeRangeRT,
}),
rt.partial({
metric: metricRT,
// Pagination properties
pagination: paginationRT,
// Sort properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const FlyoutHome = (props: Props) => {
return (
<LoadingPage
message={i18n.translate('xpack.infra.ml.anomalyFlyout.jobStatusLoadingMessage', {
defaultMessage: 'Checking status of metris jobs...',
defaultMessage: 'Checking status of metrics jobs...',
})}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export const JobSetupScreen = (props: Props) => {
label={
<FormattedMessage
id="xpack.infra.ml.steps.setupProcess.partition.label"
defaultMessage="Partition filed"
defaultMessage="Partition field"
/>
}
compressed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useMemo, useCallback } from 'react';
import React, { useMemo, useCallback, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
Expand All @@ -18,7 +18,12 @@ import {
TooltipValue,
niceTimeFormatter,
ElementClickListener,
RectAnnotation,
RectAnnotationDatum,
} from '@elastic/charts';
import { EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup } from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public';
import { toMetricOpt } from '../../../../../../common/snapshot_metric_i18n';
import { MetricsExplorerAggregation } from '../../../../../../common/http_api';
Expand All @@ -35,6 +40,8 @@ import { calculateDomain } from '../../../metrics_explorer/components/helpers/ca

import { euiStyled } from '../../../../../../../observability/public';
import { InfraFormatter } from '../../../../../lib/lib';
import { useMetricsHostsAnomaliesResults } from '../../hooks/use_metrics_hosts_anomalies';
import { useMetricsK8sAnomaliesResults } from '../../hooks/use_metrics_k8s_anomalies';

interface Props {
interval: string;
Expand All @@ -47,7 +54,8 @@ export const Timeline: React.FC<Props> = ({ interval, yAxisFormatter, isVisible
const { metric, nodeType, accountId, region } = useWaffleOptionsContext();
const { currentTime, jumpToTime, stopAutoReload } = useWaffleTimeContext();
const { filterQueryAsJson } = useWaffleFiltersContext();
const { loading, error, timeseries, reload } = useTimeline(

const { loading, error, startTime, endTime, timeseries, reload } = useTimeline(
filterQueryAsJson,
[metric],
nodeType,
Expand All @@ -59,6 +67,40 @@ export const Timeline: React.FC<Props> = ({ interval, yAxisFormatter, isVisible
isVisible
);

const anomalyParams = {
sourceId: 'default',
startTime,
endTime,
defaultSortOptions: {
direction: 'desc' as const,
field: 'anomalyScore' as const,
},
defaultPaginationOptions: { pageSize: 100 },
};

const { metricsHostsAnomalies, getMetricsHostsAnomalies } = useMetricsHostsAnomaliesResults(
anomalyParams
);
const { metricsK8sAnomalies, getMetricsK8sAnomalies } = useMetricsK8sAnomaliesResults(
anomalyParams
);

const getAnomalies = useMemo(() => {
if (nodeType === 'host') {
return getMetricsHostsAnomalies;
} else if (nodeType === 'pod') {
return getMetricsK8sAnomalies;
}
}, [nodeType, getMetricsK8sAnomalies, getMetricsHostsAnomalies]);

const anomalies = useMemo(() => {
if (nodeType === 'host') {
return metricsHostsAnomalies;
} else if (nodeType === 'pod') {
return metricsK8sAnomalies;
}
}, [nodeType, metricsHostsAnomalies, metricsK8sAnomalies]);

const metricLabel = toMetricOpt(metric.type)?.textLC;

const chartMetric = {
Expand Down Expand Up @@ -104,6 +146,25 @@ export const Timeline: React.FC<Props> = ({ interval, yAxisFormatter, isVisible
[jumpToTime, stopAutoReload]
);

const anomalyMetricName = useMemo(() => {
const metricType = metric.type;
if (metricType === 'memory') {
return 'memory_usage';
}
if (metricType === 'rx') {
return 'network_in';
}
if (metricType === 'tx') {
return 'network_out';
}
}, [metric]);

useEffect(() => {
if (getAnomalies && anomalyMetricName) {
getAnomalies(anomalyMetricName);
}
}, [getAnomalies, anomalyMetricName]);

if (loading) {
return (
<TimelineContainer>
Expand All @@ -130,21 +191,86 @@ export const Timeline: React.FC<Props> = ({ interval, yAxisFormatter, isVisible
);
}

function generateAnnotationData(results: Array<[number, string[]]>): RectAnnotationDatum[] {
return results.map((anomaly) => {
const [val, influencers] = anomaly;
return {
coordinates: {
x0: val,
x1: moment(val).add(15, 'minutes').valueOf(),
y0: dataDomain?.min,
y1: dataDomain?.max,
},
details: influencers.join(','),
};
});
}

return (
<TimelineContainer>
<TimelineHeader>
<EuiText>
<strong>
<FormattedMessage
id="xpack.infra.inventoryTimeline.header"
defaultMessage="Average {metricLabel}"
values={{ metricLabel }}
/>
</strong>
</EuiText>
<EuiFlexItem grow={true}>
<EuiText>
<strong>
<FormattedMessage
id="xpack.infra.inventoryTimeline.header"
defaultMessage="Average {metricLabel}"
values={{ metricLabel }}
/>
</strong>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems={'center'}>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize={'s'} alignItems={'center'}>
<EuiFlexItem grow={false}>
<EuiIcon
color={getTimelineChartTheme(isDarkMode).crosshair.band.fill}
type={'dot'}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size={'xs'}>
<FormattedMessage
id="xpack.infra.inventoryTimeline.header"
defaultMessage="Average {metricLabel}"
values={{ metricLabel }}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize={'s'} alignItems={'center'}>
<EuiFlexItem
grow={false}
style={{ backgroundColor: '#D36086', height: 5, width: 10 }}
/>
<EuiFlexItem>
<EuiText size={'xs'}>
<FormattedMessage
id="xpack.infra.inventoryTimeline.legend.anomalyLabel"
defaultMessage="Anomaly detected"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</TimelineHeader>
<TimelineChartContainer>
<Chart>
{anomalies && (
<RectAnnotation
id={'anomalies'}
dataValues={generateAnnotationData(
anomalies.map((a) => [a.startTime, a.influencers])
)}
style={{ fill: '#D36086' }}
/>
)}
<MetricExplorerSeriesChart
type={MetricsExplorerChartType.area}
metric={chartMetric}
Expand Down Expand Up @@ -196,7 +322,7 @@ const TimelineHeader = euiStyled.div`
`;

const TimelineChartContainer = euiStyled.div`
padding-left: ${(props) => props.theme.eui.paddingSizes.xs};
padding-left: ${(props) => props.theme.eui.paddingSizes.xs};
width: 100%;
height: 100%;
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { useMemo, useState, useCallback, useEffect, useReducer } from 'react';
import {
INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH,
Metric,
Sort,
Pagination,
PaginationCursor,
Expand Down Expand Up @@ -168,7 +169,7 @@ export const useMetricsHostsAnomaliesResults = ({
const [getMetricsHostsAnomaliesRequest, getMetricsHostsAnomalies] = useTrackedPromise(
{
cancelPreviousOn: 'creation',
createPromise: async () => {
createPromise: async (metric: Metric) => {
const {
timeRange: { start: queryStartTime, end: queryEndTime },
sortOptions,
Expand All @@ -179,6 +180,7 @@ export const useMetricsHostsAnomaliesResults = ({
sourceId,
queryStartTime,
queryEndTime,
metric,
sortOptions,
{
...paginationOptions,
Expand Down Expand Up @@ -249,10 +251,6 @@ export const useMetricsHostsAnomaliesResults = ({
});
}, [filteredDatasets]);

useEffect(() => {
getMetricsHostsAnomalies();
}, [getMetricsHostsAnomalies]); // TODO: FIgure out the deps here.

const handleFetchNextPage = useCallback(() => {
if (reducerState.lastReceivedCursors) {
dispatch({ type: 'fetchNextPage' });
Expand Down Expand Up @@ -294,6 +292,7 @@ export const callGetMetricHostsAnomaliesAPI = async (
sourceId: string,
startTime: number,
endTime: number,
metric: Metric,
sort: Sort,
pagination: Pagination
) => {
Expand All @@ -307,6 +306,7 @@ export const callGetMetricHostsAnomaliesAPI = async (
startTime,
endTime,
},
metric,
sort,
pagination,
},
Expand Down
Loading

0 comments on commit 4f15a84

Please sign in to comment.