Skip to content

Commit 4f15a84

Browse files
[Metrics UI] Add anomalies to timeline (#78602) (#78887)
* 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 <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent b646db8 commit 4f15a84

File tree

16 files changed

+262
-159
lines changed

16 files changed

+262
-159
lines changed

x-pack/plugins/infra/common/http_api/infra_ml/results/common.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,11 @@ export const sortRT = rt.type({
5757
});
5858

5959
export type Sort = rt.TypeOf<typeof sortRT>;
60+
61+
export const metricRT = rt.keyof({
62+
memory_usage: null,
63+
network_in: null,
64+
network_out: null,
65+
});
66+
67+
export type Metric = rt.TypeOf<typeof metricRT>;

x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import * as rt from 'io-ts';
88

99
import { timeRangeRT, routeTimingMetadataRT } from '../../shared';
10-
import { anomalyTypeRT, paginationCursorRT, sortRT, paginationRT } from './common';
10+
import { anomalyTypeRT, paginationCursorRT, sortRT, paginationRT, metricRT } from './common';
1111

1212
export const INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH =
1313
'/api/infra/infra_ml/results/metrics_hosts_anomalies';
@@ -18,6 +18,7 @@ const metricsHostAnomalyCommonFieldsRT = rt.type({
1818
typical: rt.number,
1919
actual: rt.number,
2020
type: anomalyTypeRT,
21+
influencers: rt.array(rt.string),
2122
duration: rt.number,
2223
startTime: rt.number,
2324
jobId: rt.string,
@@ -64,12 +65,11 @@ export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({
6465
timeRange: timeRangeRT,
6566
}),
6667
rt.partial({
68+
metric: metricRT,
6769
// Pagination properties
6870
pagination: paginationRT,
6971
// Sort properties
7072
sort: sortRT,
71-
// // Dataset filters
72-
// datasets: rt.array(rt.string),
7373
}),
7474
]),
7575
});

x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import * as rt from 'io-ts';
88

99
import { timeRangeRT, routeTimingMetadataRT } from '../../shared';
10-
import { paginationCursorRT, anomalyTypeRT, sortRT, paginationRT } from './common';
10+
import { paginationCursorRT, anomalyTypeRT, sortRT, paginationRT, metricRT } from './common';
1111

1212
export const INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH =
1313
'/api/infra/infra_ml/results/metrics_k8s_anomalies';
@@ -18,6 +18,7 @@ const metricsK8sAnomalyCommonFieldsRT = rt.type({
1818
typical: rt.number,
1919
actual: rt.number,
2020
type: anomalyTypeRT,
21+
influencers: rt.array(rt.string),
2122
duration: rt.number,
2223
startTime: rt.number,
2324
jobId: rt.string,
@@ -64,6 +65,7 @@ export const getMetricsK8sAnomaliesRequestPayloadRT = rt.type({
6465
timeRange: timeRangeRT,
6566
}),
6667
rt.partial({
68+
metric: metricRT,
6769
// Pagination properties
6870
pagination: paginationRT,
6971
// Sort properties

x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const FlyoutHome = (props: Props) => {
8484
return (
8585
<LoadingPage
8686
message={i18n.translate('xpack.infra.ml.anomalyFlyout.jobStatusLoadingMessage', {
87-
defaultMessage: 'Checking status of metris jobs...',
87+
defaultMessage: 'Checking status of metrics jobs...',
8888
})}
8989
/>
9090
);

x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export const JobSetupScreen = (props: Props) => {
223223
label={
224224
<FormattedMessage
225225
id="xpack.infra.ml.steps.setupProcess.partition.label"
226-
defaultMessage="Partition filed"
226+
defaultMessage="Partition field"
227227
/>
228228
}
229229
compressed

x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx

Lines changed: 138 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import React, { useMemo, useCallback } from 'react';
7+
import React, { useMemo, useCallback, useEffect } from 'react';
88
import { i18n } from '@kbn/i18n';
99
import { FormattedMessage } from '@kbn/i18n/react';
1010
import moment from 'moment';
@@ -18,7 +18,12 @@ import {
1818
TooltipValue,
1919
niceTimeFormatter,
2020
ElementClickListener,
21+
RectAnnotation,
22+
RectAnnotationDatum,
2123
} from '@elastic/charts';
24+
import { EuiFlexItem } from '@elastic/eui';
25+
import { EuiFlexGroup } from '@elastic/eui';
26+
import { EuiIcon } from '@elastic/eui';
2227
import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public';
2328
import { toMetricOpt } from '../../../../../../common/snapshot_metric_i18n';
2429
import { MetricsExplorerAggregation } from '../../../../../../common/http_api';
@@ -35,6 +40,8 @@ import { calculateDomain } from '../../../metrics_explorer/components/helpers/ca
3540

3641
import { euiStyled } from '../../../../../../../observability/public';
3742
import { InfraFormatter } from '../../../../../lib/lib';
43+
import { useMetricsHostsAnomaliesResults } from '../../hooks/use_metrics_hosts_anomalies';
44+
import { useMetricsK8sAnomaliesResults } from '../../hooks/use_metrics_k8s_anomalies';
3845

3946
interface Props {
4047
interval: string;
@@ -47,7 +54,8 @@ export const Timeline: React.FC<Props> = ({ interval, yAxisFormatter, isVisible
4754
const { metric, nodeType, accountId, region } = useWaffleOptionsContext();
4855
const { currentTime, jumpToTime, stopAutoReload } = useWaffleTimeContext();
4956
const { filterQueryAsJson } = useWaffleFiltersContext();
50-
const { loading, error, timeseries, reload } = useTimeline(
57+
58+
const { loading, error, startTime, endTime, timeseries, reload } = useTimeline(
5159
filterQueryAsJson,
5260
[metric],
5361
nodeType,
@@ -59,6 +67,40 @@ export const Timeline: React.FC<Props> = ({ interval, yAxisFormatter, isVisible
5967
isVisible
6068
);
6169

70+
const anomalyParams = {
71+
sourceId: 'default',
72+
startTime,
73+
endTime,
74+
defaultSortOptions: {
75+
direction: 'desc' as const,
76+
field: 'anomalyScore' as const,
77+
},
78+
defaultPaginationOptions: { pageSize: 100 },
79+
};
80+
81+
const { metricsHostsAnomalies, getMetricsHostsAnomalies } = useMetricsHostsAnomaliesResults(
82+
anomalyParams
83+
);
84+
const { metricsK8sAnomalies, getMetricsK8sAnomalies } = useMetricsK8sAnomaliesResults(
85+
anomalyParams
86+
);
87+
88+
const getAnomalies = useMemo(() => {
89+
if (nodeType === 'host') {
90+
return getMetricsHostsAnomalies;
91+
} else if (nodeType === 'pod') {
92+
return getMetricsK8sAnomalies;
93+
}
94+
}, [nodeType, getMetricsK8sAnomalies, getMetricsHostsAnomalies]);
95+
96+
const anomalies = useMemo(() => {
97+
if (nodeType === 'host') {
98+
return metricsHostsAnomalies;
99+
} else if (nodeType === 'pod') {
100+
return metricsK8sAnomalies;
101+
}
102+
}, [nodeType, metricsHostsAnomalies, metricsK8sAnomalies]);
103+
62104
const metricLabel = toMetricOpt(metric.type)?.textLC;
63105

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

149+
const anomalyMetricName = useMemo(() => {
150+
const metricType = metric.type;
151+
if (metricType === 'memory') {
152+
return 'memory_usage';
153+
}
154+
if (metricType === 'rx') {
155+
return 'network_in';
156+
}
157+
if (metricType === 'tx') {
158+
return 'network_out';
159+
}
160+
}, [metric]);
161+
162+
useEffect(() => {
163+
if (getAnomalies && anomalyMetricName) {
164+
getAnomalies(anomalyMetricName);
165+
}
166+
}, [getAnomalies, anomalyMetricName]);
167+
107168
if (loading) {
108169
return (
109170
<TimelineContainer>
@@ -130,21 +191,86 @@ export const Timeline: React.FC<Props> = ({ interval, yAxisFormatter, isVisible
130191
);
131192
}
132193

194+
function generateAnnotationData(results: Array<[number, string[]]>): RectAnnotationDatum[] {
195+
return results.map((anomaly) => {
196+
const [val, influencers] = anomaly;
197+
return {
198+
coordinates: {
199+
x0: val,
200+
x1: moment(val).add(15, 'minutes').valueOf(),
201+
y0: dataDomain?.min,
202+
y1: dataDomain?.max,
203+
},
204+
details: influencers.join(','),
205+
};
206+
});
207+
}
208+
133209
return (
134210
<TimelineContainer>
135211
<TimelineHeader>
136-
<EuiText>
137-
<strong>
138-
<FormattedMessage
139-
id="xpack.infra.inventoryTimeline.header"
140-
defaultMessage="Average {metricLabel}"
141-
values={{ metricLabel }}
142-
/>
143-
</strong>
144-
</EuiText>
212+
<EuiFlexItem grow={true}>
213+
<EuiText>
214+
<strong>
215+
<FormattedMessage
216+
id="xpack.infra.inventoryTimeline.header"
217+
defaultMessage="Average {metricLabel}"
218+
values={{ metricLabel }}
219+
/>
220+
</strong>
221+
</EuiText>
222+
</EuiFlexItem>
223+
<EuiFlexItem grow={false}>
224+
<EuiFlexGroup alignItems={'center'}>
225+
<EuiFlexItem grow={false}>
226+
<EuiFlexGroup gutterSize={'s'} alignItems={'center'}>
227+
<EuiFlexItem grow={false}>
228+
<EuiIcon
229+
color={getTimelineChartTheme(isDarkMode).crosshair.band.fill}
230+
type={'dot'}
231+
/>
232+
</EuiFlexItem>
233+
<EuiFlexItem grow={false}>
234+
<EuiText size={'xs'}>
235+
<FormattedMessage
236+
id="xpack.infra.inventoryTimeline.header"
237+
defaultMessage="Average {metricLabel}"
238+
values={{ metricLabel }}
239+
/>
240+
</EuiText>
241+
</EuiFlexItem>
242+
</EuiFlexGroup>
243+
</EuiFlexItem>
244+
<EuiFlexItem grow={false}>
245+
<EuiFlexGroup gutterSize={'s'} alignItems={'center'}>
246+
<EuiFlexItem
247+
grow={false}
248+
style={{ backgroundColor: '#D36086', height: 5, width: 10 }}
249+
/>
250+
<EuiFlexItem>
251+
<EuiText size={'xs'}>
252+
<FormattedMessage
253+
id="xpack.infra.inventoryTimeline.legend.anomalyLabel"
254+
defaultMessage="Anomaly detected"
255+
/>
256+
</EuiText>
257+
</EuiFlexItem>
258+
</EuiFlexGroup>
259+
</EuiFlexItem>
260+
</EuiFlexGroup>
261+
</EuiFlexItem>
145262
</TimelineHeader>
146263
<TimelineChartContainer>
147264
<Chart>
265+
{anomalies && (
266+
<RectAnnotation
267+
id={'anomalies'}
268+
dataValues={generateAnnotationData(
269+
anomalies.map((a) => [a.startTime, a.influencers])
270+
)}
271+
style={{ fill: '#D36086' }}
272+
/>
273+
)}
148274
<MetricExplorerSeriesChart
149275
type={MetricsExplorerChartType.area}
150276
metric={chartMetric}
@@ -196,7 +322,7 @@ const TimelineHeader = euiStyled.div`
196322
`;
197323

198324
const TimelineChartContainer = euiStyled.div`
199-
padding-left: ${(props) => props.theme.eui.paddingSizes.xs};
325+
padding-left: ${(props) => props.theme.eui.paddingSizes.xs};
200326
width: 100%;
201327
height: 100%;
202328
`;

x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { useMemo, useState, useCallback, useEffect, useReducer } from 'react';
88
import {
99
INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH,
10+
Metric,
1011
Sort,
1112
Pagination,
1213
PaginationCursor,
@@ -168,7 +169,7 @@ export const useMetricsHostsAnomaliesResults = ({
168169
const [getMetricsHostsAnomaliesRequest, getMetricsHostsAnomalies] = useTrackedPromise(
169170
{
170171
cancelPreviousOn: 'creation',
171-
createPromise: async () => {
172+
createPromise: async (metric: Metric) => {
172173
const {
173174
timeRange: { start: queryStartTime, end: queryEndTime },
174175
sortOptions,
@@ -179,6 +180,7 @@ export const useMetricsHostsAnomaliesResults = ({
179180
sourceId,
180181
queryStartTime,
181182
queryEndTime,
183+
metric,
182184
sortOptions,
183185
{
184186
...paginationOptions,
@@ -249,10 +251,6 @@ export const useMetricsHostsAnomaliesResults = ({
249251
});
250252
}, [filteredDatasets]);
251253

252-
useEffect(() => {
253-
getMetricsHostsAnomalies();
254-
}, [getMetricsHostsAnomalies]); // TODO: FIgure out the deps here.
255-
256254
const handleFetchNextPage = useCallback(() => {
257255
if (reducerState.lastReceivedCursors) {
258256
dispatch({ type: 'fetchNextPage' });
@@ -294,6 +292,7 @@ export const callGetMetricHostsAnomaliesAPI = async (
294292
sourceId: string,
295293
startTime: number,
296294
endTime: number,
295+
metric: Metric,
297296
sort: Sort,
298297
pagination: Pagination
299298
) => {
@@ -307,6 +306,7 @@ export const callGetMetricHostsAnomaliesAPI = async (
307306
startTime,
308307
endTime,
309308
},
309+
metric,
310310
sort,
311311
pagination,
312312
},

0 commit comments

Comments
 (0)