Skip to content

Commit

Permalink
[Uptime] Ml detection of duration anomalies (#59785) (#61051)
Browse files Browse the repository at this point in the history
* add flyout

* add state

* update state

* ad job

* update

* updat

* add ml analyze button

* update api

* use differential colors for duration chart

* remove duration chart gql

* update type

* type fix

* fix tyoe

* update translation

* update test

* update conflicts

* update anomaly record

* chart

* added annotations

* update error handling

* update

* update types

* fixed types

* fix types

* update types

* update

* update

* remove unnecessary change

* remove unnecessary change

* fix type

* update

* save

* update pr

* update tets

* update job deletion

* update

* update tets

* upadte tests

* fix types

* update title text

* update types

* fixed tests

* update tests and types

* updated types

* fix PR feedback

* unit test

* update more types

* update test and manage  job

* resolve conflicts

* types

* remove unnecessary change

* revert ml code

* revert ml code

* fixed formatting issues pointed by pr feedback
  • Loading branch information
shahzad31 authored Mar 24, 2020
1 parent 4789f47 commit 7a7939a
Show file tree
Hide file tree
Showing 69 changed files with 2,522 additions and 180 deletions.
1 change: 1 addition & 0 deletions src/plugins/kibana_react/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export * from './field_icon';
export * from './table_list_view';
export * from './split_panel';
export { ValidatedDualRange } from './validated_range';
export * from './notifications';
export { Markdown, MarkdownSimple } from './markdown';
export { reactToUiComponent, uiToReactComponent } from './adapters';
export { useUrlTracker } from './use_url_tracker';
Expand Down
6 changes: 6 additions & 0 deletions x-pack/legacy/plugins/uptime/common/constants/rest_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ export enum API_URLS {
PING_HISTOGRAM = `/api/uptime/ping/histogram`,
SNAPSHOT_COUNT = `/api/uptime/snapshot/count`,
FILTERS = `/api/uptime/filters`,

ML_MODULE_JOBS = `/api/ml/modules/jobs_exist/`,
ML_SETUP_MODULE = '/api/ml/modules/setup/',
ML_DELETE_JOB = `/api/ml/jobs/delete_jobs`,
ML_CAPABILITIES = '/api/ml/ml_capabilities',
ML_ANOMALIES_RESULT = `/api/ml/results/anomalies_table_data`,
}
4 changes: 4 additions & 0 deletions x-pack/legacy/plugins/uptime/common/constants/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export enum STATUS {
DOWN = 'down',
}

export const ML_JOB_ID = 'high_latency_by_geo';

export const ML_MODULE_ID = 'uptime_heartbeat';

export const UNNAMED_LOCATION = 'Unnamed-location';

export const SHORT_TS_LOCALE = 'en-short-locale';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,80 @@
import React, { useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useUrlParams } from '../../../hooks';
import { getMonitorDurationAction } from '../../../state/actions';
import {
getAnomalyRecordsAction,
getMLCapabilitiesAction,
getMonitorDurationAction,
} from '../../../state/actions';
import { DurationChartComponent } from '../../functional/charts';
import { selectDurationLines } from '../../../state/selectors';
import {
anomaliesSelector,
hasMLFeatureAvailable,
hasMLJobSelector,
selectDurationLines,
} from '../../../state/selectors';
import { UptimeRefreshContext } from '../../../contexts';
import { getMLJobId } from '../../../state/api/ml_anomaly';
import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer';

interface Props {
monitorId: string;
}

export const DurationChart: React.FC<Props> = ({ monitorId }: Props) => {
const [getUrlParams] = useUrlParams();
const { dateRangeStart, dateRangeEnd } = getUrlParams();
const {
dateRangeStart,
dateRangeEnd,
absoluteDateRangeStart,
absoluteDateRangeEnd,
} = getUrlParams();

const { monitor_duration, loading } = useSelector(selectDurationLines);
const { durationLines, loading } = useSelector(selectDurationLines);

const isMLAvailable = useSelector(hasMLFeatureAvailable);

const { data: mlJobs, loading: jobsLoading } = useSelector(hasMLJobSelector);

const hasMLJob =
!!mlJobs?.jobsExist &&
!!mlJobs.jobs.find((job: JobStat) => job.id === getMLJobId(monitorId as string));

const anomalies = useSelector(anomaliesSelector);

const dispatch = useDispatch();

const { lastRefresh } = useContext(UptimeRefreshContext);

useEffect(() => {
dispatch(
getMonitorDurationAction({ monitorId, dateStart: dateRangeStart, dateEnd: dateRangeEnd })
);
if (isMLAvailable) {
const anomalyParams = {
listOfMonitorIds: [monitorId],
dateStart: absoluteDateRangeStart,
dateEnd: absoluteDateRangeEnd,
};

dispatch(getAnomalyRecordsAction.get(anomalyParams));
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId, isMLAvailable]);

useEffect(() => {
const params = { monitorId, dateStart: dateRangeStart, dateEnd: dateRangeEnd };
dispatch(getMonitorDurationAction(params));
}, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId]);

useEffect(() => {
dispatch(getMLCapabilitiesAction.get());
}, [dispatch]);

return (
<DurationChartComponent
locationDurationLines={monitor_duration?.locationDurationLines ?? []}
loading={loading}
anomalies={anomalies}
hasMLJob={hasMLJob}
loading={loading || jobsLoading}
locationDurationLines={durationLines?.locationDurationLines ?? []}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { indexStatusSelector } from '../../../state/selectors';
import { EmptyStateComponent } from '../../functional/empty_state/empty_state';

export const EmptyState: React.FC = ({ children }) => {
const { data, loading, errors } = useSelector(indexStatusSelector);
const { data, loading, error } = useSelector(indexStatusSelector);

const dispatch = useDispatch();

Expand All @@ -23,7 +23,7 @@ export const EmptyState: React.FC = ({ children }) => {
<EmptyStateComponent
statesIndexStatus={data}
loading={loading}
errors={errors}
errors={error ? [error] : undefined}
children={children as React.ReactElement}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ interface StateProps {
}

interface DispatchProps {
loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => void;
loadMonitorStatus: typeof getMonitorStatusAction;
loadSelectedMonitor: typeof getSelectedMonitorAction;
}

interface OwnProps {
Expand All @@ -33,6 +34,7 @@ type Props = OwnProps & StateProps & DispatchProps;

const Container: React.FC<Props> = ({
loadMonitorStatus,
loadSelectedMonitor,
monitorId,
monitorStatus,
monitorLocations,
Expand All @@ -43,8 +45,9 @@ const Container: React.FC<Props> = ({
const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams();

useEffect(() => {
loadMonitorStatus(dateStart, dateEnd, monitorId);
}, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh]);
loadMonitorStatus({ dateStart, dateEnd, monitorId });
loadSelectedMonitor({ monitorId });
}, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh, loadSelectedMonitor]);

return (
<MonitorStatusBarComponent
Expand All @@ -61,20 +64,8 @@ const mapStateToProps = (state: AppState, ownProps: OwnProps) => ({
});

const mapDispatchToProps = (dispatch: Dispatch<any>): DispatchProps => ({
loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => {
dispatch(
getMonitorStatusAction({
monitorId,
dateStart,
dateEnd,
})
);
dispatch(
getSelectedMonitorAction({
monitorId,
})
);
},
loadSelectedMonitor: params => dispatch(getSelectedMonitorAction(params)),
loadMonitorStatus: params => dispatch(getMonitorStatusAction(params)),
});

// @ts-ignore TODO: Investigate typescript issues here
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ describe('MonitorCharts component', () => {
it('renders the component without errors', () => {
const component = shallowWithRouter(
<DurationChartComponent
locationDurationLines={chartResponse.monitorChartsData.locationDurationLines}
loading={false}
hasMLJob={false}
anomalies={null}
locationDurationLines={chartResponse.monitorChartsData.locationDurationLines}
/>
);
expect(component).toMatchSnapshot();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 React from 'react';
import moment from 'moment';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';

const Header = styled.div`
font-weight: bold;
padding-left: 4px;
`;

const RecordSeverity = styled.div`
font-weight: bold;
border-left: 4px solid ${props => props.color};
padding-left: 2px;
`;

const TimeDiv = styled.div`
font-weight: 500;
border-bottom: 1px solid gray;
padding-bottom: 2px;
`;

export const AnnotationTooltip = ({ details }: { details: string }) => {
const data = JSON.parse(details);

function capitalizeFirstLetter(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}

return (
<>
<TimeDiv>{moment(data.time).format('lll')}</TimeDiv>
<Header>
<FormattedMessage
id="xpack.uptime.charts.mlAnnotation.header"
defaultMessage="Score: {score}"
values={{ score: data.score.toFixed(2) }}
/>
</Header>
<RecordSeverity color={data.color}>
<FormattedMessage
id="xpack.uptime.charts.mlAnnotation.severity"
defaultMessage="Severity: {severity}"
values={{ severity: capitalizeFirstLetter(data.severity) }}
/>
</RecordSeverity>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Axis, Chart, Position, timeFormatter, Settings } from '@elastic/charts';
import { EuiPanel, EuiTitle } from '@elastic/eui';
import React from 'react';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { Axis, Chart, Position, timeFormatter, Settings } from '@elastic/charts';
import { SeriesIdentifier } from '@elastic/charts/dist/chart_types/xy_chart/utils/series';
import { getChartDateLabel } from '../../../lib/helper';
import { LocationDurationLine } from '../../../../common/types';
import { DurationLineSeriesList } from './duration_line_series_list';
import { ChartWrapper } from './chart_wrapper';
import { useUrlParams } from '../../../hooks';
import { getTickFormat } from './get_tick_format';
import { ChartEmptyState } from './chart_empty_state';
import { DurationAnomaliesBar } from './duration_line_bar_list';
import { MLIntegrationComponent } from '../../monitor_details/ml/ml_integeration';
import { AnomalyRecords } from '../../../state/actions';

interface DurationChartProps {
/**
Expand All @@ -29,6 +33,10 @@ interface DurationChartProps {
* To represent the loading spinner on chart
*/
loading: boolean;

hasMLJob: boolean;

anomalies: AnomalyRecords | null;
}

/**
Expand All @@ -37,29 +45,64 @@ interface DurationChartProps {
* milliseconds.
* @param props The props required for this component to render properly
*/
export const DurationChartComponent = ({ locationDurationLines, loading }: DurationChartProps) => {
export const DurationChartComponent = ({
locationDurationLines,
anomalies,
loading,
hasMLJob,
}: DurationChartProps) => {
const hasLines = locationDurationLines.length > 0;
const [getUrlParams, updateUrlParams] = useUrlParams();
const { absoluteDateRangeStart: min, absoluteDateRangeEnd: max } = getUrlParams();

const [hiddenLegends, setHiddenLegends] = useState<string[]>([]);

const onBrushEnd = (minX: number, maxX: number) => {
updateUrlParams({
dateRangeStart: moment(minX).toISOString(),
dateRangeEnd: moment(maxX).toISOString(),
});
};

const legendToggleVisibility = (legendItem: SeriesIdentifier | null) => {
if (legendItem) {
setHiddenLegends(prevState => {
if (prevState.includes(legendItem.specId)) {
return [...prevState.filter(item => item !== legendItem.specId)];
} else {
return [...prevState, legendItem.specId];
}
});
}
};

return (
<>
<EuiPanel paddingSize="m">
<EuiTitle size="xs">
<h4>
<FormattedMessage
id="xpack.uptime.monitorCharts.monitorDuration.titleLabel"
defaultMessage="Monitor duration"
description="The 'ms' is an abbreviation for milliseconds."
/>
</h4>
</EuiTitle>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h4>
{hasMLJob ? (
<FormattedMessage
id="xpack.uptime.monitorCharts.monitorDuration.titleLabelWithAnomaly"
defaultMessage="Monitor duration (Anomalies: {noOfAnomalies})"
values={{ noOfAnomalies: anomalies?.anomalies?.length ?? 0 }}
/>
) : (
<FormattedMessage
id="xpack.uptime.monitorCharts.monitorDuration.titleLabel"
defaultMessage="Monitor duration"
/>
)}
</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<MLIntegrationComponent />
</EuiFlexItem>
</EuiFlexGroup>

<ChartWrapper height="400px" loading={loading}>
{hasLines ? (
<Chart>
Expand All @@ -69,6 +112,7 @@ export const DurationChartComponent = ({ locationDurationLines, loading }: Durat
showLegendExtra
legendPosition={Position.Bottom}
onBrushEnd={onBrushEnd}
onLegendItemClick={legendToggleVisibility}
/>
<Axis
id="bottom"
Expand All @@ -89,6 +133,7 @@ export const DurationChartComponent = ({ locationDurationLines, loading }: Durat
})}
/>
<DurationLineSeriesList lines={locationDurationLines} />
<DurationAnomaliesBar anomalies={anomalies} hiddenLegends={hiddenLegends} />
</Chart>
) : (
<ChartEmptyState
Expand Down
Loading

0 comments on commit 7a7939a

Please sign in to comment.