[ML] Fix Single Metric Viewer and Anomaly Explorer charts still loading even after failure (#98490) (#98790)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Quynh Nguyen <43350163+qn895@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-04-29 14:34:51 -04:00 committed by GitHub
parent d16d17b955
commit e695a5bd04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 205 additions and 72 deletions

View file

@ -207,7 +207,7 @@ export function getSingleMetricViewerJobErrorMessage(job: CombinedJob): string |
return i18n.translate(
'xpack.ml.timeSeriesJob.jobWithUnsupportedCompositeAggregationMessage',
{
defaultMessage: 'Disabled because the datafeed contains unsupported composite sources.',
defaultMessage: 'the datafeed contains unsupported composite sources',
}
);
}
@ -223,7 +223,7 @@ export function getSingleMetricViewerJobErrorMessage(job: CombinedJob): string |
if (isChartableTimeSeriesViewJob === false) {
return i18n.translate('xpack.ml.timeSeriesJob.notViewableTimeSeriesJobMessage', {
defaultMessage: 'Disabled because not a viewable time series job.',
defaultMessage: 'not a viewable time series job',
});
}
}

View file

@ -39,6 +39,15 @@ export function ResultLinks({ jobs }) {
const singleMetricDisabledMessage =
jobs.length === 1 && jobs[0].isNotSingleMetricViewerJobMessage;
const singleMetricDisabledMessageText =
singleMetricDisabledMessage !== undefined
? i18n.translate('xpack.ml.jobsList.resultActions.singleMetricDisabledMessageText', {
defaultMessage: 'Disabled because {reason}.',
values: {
reason: singleMetricDisabledMessage,
},
})
: undefined;
const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true;
const { createLinkWithUserDefaults } = useCreateADLinks();
const timeSeriesExplorerLink = useMemo(
@ -60,7 +69,7 @@ export function ResultLinks({ jobs }) {
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={singleMetricDisabledMessage ?? openJobsInSingleMetricViewerText}
content={singleMetricDisabledMessageText ?? openJobsInSingleMetricViewerText}
>
<EuiButtonIcon
href={timeSeriesExplorerLink}

View file

@ -8,7 +8,7 @@
import { each, find, get, map, reduce, sortBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { Observable, of } from 'rxjs';
import { map as mapObservable } from 'rxjs/operators';
import { catchError, map as mapObservable } from 'rxjs/operators';
import { RecordForInfluencer } from './results_service/results_service';
import {
isMappableJob,
@ -29,7 +29,11 @@ import { CriteriaField, MlResultsService } from './results_service';
import { TimefilterContract, TimeRange } from '../../../../../../src/plugins/data/public';
import { CHART_TYPE, ChartType } from '../explorer/explorer_constants';
import type { ChartRecord } from '../explorer/explorer_utils';
import { RecordsForCriteria, ScheduledEventsByBucket } from './results_service/result_service_rx';
import {
RecordsForCriteria,
ResultResponse,
ScheduledEventsByBucket,
} from './results_service/result_service_rx';
import { isPopulatedObject } from '../../../common/util/object_utils';
import { AnomalyRecordDoc } from '../../../common/types/anomalies';
import {
@ -60,9 +64,8 @@ interface ChartPoint {
numberOfCauses?: number;
scheduledEvents?: any[];
}
interface MetricData {
interface MetricData extends ResultResponse {
results: Record<string, number>;
success: boolean;
}
interface SeriesConfig {
jobId: JobId;
@ -91,6 +94,8 @@ export interface SeriesConfigWithMetadata extends SeriesConfig {
loading?: boolean;
chartData?: ChartPoint[] | null;
mapData?: Array<ChartRecord | undefined>;
plotEarliest?: number;
plotLatest?: number;
}
export const isSeriesConfigWithMetadata = (arg: unknown): arg is SeriesConfigWithMetadata => {
@ -545,6 +550,19 @@ export class AnomalyExplorerChartsService {
return data;
}
function handleError(errorMsg: string, jobId: string): void {
// Group the jobIds by the type of error message
if (!data.errorMessages) {
data.errorMessages = {};
}
if (data.errorMessages[errorMsg]) {
data.errorMessages[errorMsg].add(jobId);
} else {
data.errorMessages[errorMsg] = new Set([jobId]);
}
}
// Query 1 - load the raw metric data.
function getMetricData(
mlResultsService: MlResultsService,
@ -577,6 +595,17 @@ export class AnomalyExplorerChartsService {
bucketSpanSeconds * 1000,
config.datafeedConfig
)
.pipe(
catchError((error) => {
handleError(
i18n.translate('xpack.ml.timeSeriesJob.metricDataErrorMessage', {
defaultMessage: 'an error occurred while retrieving metric data',
}),
job.job_id
);
return of({ success: false, results: {}, error });
})
)
.toPromise();
} else {
// Extract the partition, by, over fields on which to filter.
@ -638,8 +667,15 @@ export class AnomalyExplorerChartsService {
});
resolve(obj);
})
.catch((resp) => {
reject(resp);
.catch((error) => {
handleError(
i18n.translate('xpack.ml.timeSeriesJob.modelPlotDataErrorMessage', {
defaultMessage: 'an error occurred while retrieving model plot data',
}),
job.job_id
);
reject(error);
});
});
}
@ -665,6 +701,17 @@ export class AnomalyExplorerChartsService {
range.max,
ANOMALIES_MAX_RESULTS
)
.pipe(
catchError((error) => {
handleError(
i18n.translate('xpack.ml.timeSeriesJob.recordsForCriteriaErrorMessage', {
defaultMessage: 'an error occurred while retrieving anomaly records',
}),
config.jobId
);
return of({ success: false, records: [], error });
})
)
.toPromise();
}
@ -683,6 +730,17 @@ export class AnomalyExplorerChartsService {
1,
MAX_SCHEDULED_EVENTS
)
.pipe(
catchError((error) => {
handleError(
i18n.translate('xpack.ml.timeSeriesJob.scheduledEventsByBucketErrorMessage', {
defaultMessage: 'an error occurred while retrieving scheduled events',
}),
config.jobId
);
return of({ success: false, events: {}, error });
})
)
.toPromise();
}
@ -707,20 +765,30 @@ export class AnomalyExplorerChartsService {
}
const datafeedQuery = get(config, 'datafeedConfig.query', null);
return mlResultsService.getEventDistributionData(
Array.isArray(config.datafeedConfig.indices)
? config.datafeedConfig.indices[0]
: config.datafeedConfig.indices,
splitField,
filterField,
datafeedQuery,
config.metricFunction,
config.metricFieldName,
config.timeField,
range.min,
range.max,
config.bucketSpanSeconds * 1000
);
return mlResultsService
.getEventDistributionData(
Array.isArray(config.datafeedConfig.indices)
? config.datafeedConfig.indices[0]
: config.datafeedConfig.indices,
splitField,
filterField,
datafeedQuery,
config.metricFunction,
config.metricFieldName,
config.timeField,
range.min,
range.max,
config.bucketSpanSeconds * 1000
)
.catch((err) => {
handleError(
i18n.translate('xpack.ml.timeSeriesJob.eventDistributionDataErrorMessage', {
defaultMessage: 'an error occurred while retrieving data',
}),
config.jobId
);
});
}
// first load and wait for required data,
@ -883,20 +951,23 @@ export class AnomalyExplorerChartsService {
);
const overallChartLimits = chartLimits(allDataPoints);
data.seriesToPlot = response.map((d, i) => {
return {
...seriesConfigsForPromises[i],
loading: false,
chartData: processedData[i],
plotEarliest: chartRange.min,
plotLatest: chartRange.max,
selectedEarliest: selectedEarliestMs,
selectedLatest: selectedLatestMs,
chartLimits: USE_OVERALL_CHART_LIMITS
? overallChartLimits
: chartLimits(processedData[i]),
};
});
data.seriesToPlot = response
// Don't show the charts if there was an issue retrieving metric or anomaly data
.filter((r) => r[0]?.success === true && r[1]?.success === true)
.map((d, i) => {
return {
...seriesConfigsForPromises[i],
loading: false,
chartData: processedData[i],
plotEarliest: chartRange.min,
plotLatest: chartRange.max,
selectedEarliest: selectedEarliestMs,
selectedLatest: selectedLatestMs,
chartLimits: USE_OVERALL_CHART_LIMITS
? overallChartLimits
: chartLimits(processedData[i]),
};
});
if (mapData.length) {
// push map data in if it's available

View file

@ -28,9 +28,11 @@ import { isPopulatedObject } from '../../../../common/util/object_utils';
import { InfluencersFilterQuery } from '../../../../common/types/es_client';
import { RecordForInfluencer } from './results_service';
import { isRuntimeMappings } from '../../../../common';
import { ErrorType } from '../../../../common/util/errors';
interface ResultResponse {
export interface ResultResponse {
success: boolean;
error?: ErrorType;
}
export interface MetricData extends ResultResponse {

View file

@ -31,7 +31,7 @@ export function toastNotificationServiceProvider(toastNotifications: ToastsStart
toastNotifications.addSuccess(toastOrTitle, options);
}
function displayErrorToast(error: ErrorType, title?: string) {
function displayErrorToast(error: ErrorType, title?: string, toastLifeTimeMs?: number) {
const errorObj = extractErrorProperties(error);
toastNotifications.addError(new MLRequestFailure(errorObj, error), {
title:
@ -39,6 +39,7 @@ export function toastNotificationServiceProvider(toastNotifications: ToastsStart
i18n.translate('xpack.ml.toastNotificationService.errorTitle', {
defaultMessage: 'An error has occurred',
}),
...(toastLifeTimeMs ? { toastLifeTimeMs } : {}),
});
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { TimeseriesexplorerChartDataError } from './timeseriesexplorer_chart_data_error';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
export const TimeseriesexplorerChartDataError = ({ errorMsg }: { errorMsg: string }) => {
return <EuiEmptyPrompt iconType="alert" title={<h2>{errorMsg}</h2>} />;
};

View file

@ -34,8 +34,6 @@ import {
EuiAccordion,
EuiBadge,
} from '@elastic/eui';
import { getToastNotifications } from '../util/dependency_cache';
import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public';
import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search';
@ -87,6 +85,7 @@ import { TimeSeriesChartWithTooltips } from './components/timeseries_chart/times
import { aggregationTypeTransform } from '../../../common/util/anomaly_utils';
import { isMetricDetector } from './get_function_description';
import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors';
import { TimeseriesexplorerChartDataError } from './components/timeseriesexplorer_chart_data_error';
// Used to indicate the chart is being plotted across
// all partition field values, where the cardinality of the field cannot be
@ -131,6 +130,7 @@ function getTimeseriesexplorerDefaultState() {
zoomTo: undefined,
zoomFromFocusLoaded: undefined,
zoomToFocusLoaded: undefined,
chartDataError: undefined,
};
}
@ -151,6 +151,7 @@ export class TimeSeriesExplorer extends React.Component {
tableInterval: PropTypes.string,
tableSeverity: PropTypes.number,
zoom: PropTypes.object,
toastNotificationService: PropTypes.object,
};
state = getTimeseriesexplorerDefaultState();
@ -390,6 +391,13 @@ export class TimeSeriesExplorer extends React.Component {
this.props.appStateHandler(APP_STATE_ACTION.SET_FORECAST_ID, forecastId);
};
displayErrorToastMessages = (error, errorMsg) => {
if (this.props.toastNotificationService) {
this.props.toastNotificationService.displayErrorToast(error, errorMsg, 2000);
}
this.setState({ loading: false, chartDataError: errorMsg });
};
loadSingleMetricData = (fullRefresh = true) => {
const {
autoZoomDuration,
@ -426,6 +434,7 @@ export class TimeSeriesExplorer extends React.Component {
fullRefresh,
loadCounter: currentLoadCounter + 1,
loading: true,
chartDataError: undefined,
...(fullRefresh
? {
chartDetails: undefined,
@ -558,11 +567,11 @@ export class TimeSeriesExplorer extends React.Component {
stateUpdate.contextChartData = fullRangeChartData;
finish(counter);
})
.catch((resp) => {
console.log(
'Time series explorer - error getting metric data from elasticsearch:',
resp
);
.catch((err) => {
const errorMsg = i18n.translate('xpack.ml.timeSeriesExplorer.metricDataErrorMessage', {
defaultMessage: 'Error getting metric data',
});
this.displayErrorToastMessages(err, errorMsg);
});
// Query 2 - load max record score at same granularity as context chart
@ -581,11 +590,15 @@ export class TimeSeriesExplorer extends React.Component {
stateUpdate.swimlaneData = fullRangeRecordScoreData;
finish(counter);
})
.catch((resp) => {
console.log(
'Time series explorer - error getting bucket anomaly scores from elasticsearch:',
resp
.catch((err) => {
const errorMsg = i18n.translate(
'xpack.ml.timeSeriesExplorer.bucketAnomalyScoresErrorMessage',
{
defaultMessage: 'Error getting bucket anomaly scores',
}
);
this.displayErrorToastMessages(err, errorMsg);
});
// Query 3 - load details on the chart used in the chart title (charting function and entity(s)).
@ -601,10 +614,12 @@ export class TimeSeriesExplorer extends React.Component {
stateUpdate.chartDetails = resp.results;
finish(counter);
})
.catch((resp) => {
console.log(
'Time series explorer - error getting entity counts from elasticsearch:',
resp
.catch((err) => {
this.displayErrorToastMessages(
err,
i18n.translate('xpack.ml.timeSeriesExplorer.entityCountsErrorMessage', {
defaultMessage: 'Error getting entity counts',
})
);
});
@ -633,10 +648,13 @@ export class TimeSeriesExplorer extends React.Component {
stateUpdate.contextForecastData = processForecastResults(resp.results);
finish(counter);
})
.catch((resp) => {
console.log(
`Time series explorer - error loading data for forecast ID ${selectedForecastId}`,
resp
.catch((err) => {
this.displayErrorToastMessages(
err,
i18n.translate('xpack.ml.timeSeriesExplorer.forecastDataErrorMessage', {
defaultMessage: 'Error loading forecast data for forecast ID {forecastId}',
values: { forecastId: selectedForecastId },
})
);
});
}
@ -695,8 +713,10 @@ export class TimeSeriesExplorer extends React.Component {
},
}
);
const toastNotifications = getToastNotifications();
toastNotifications.addWarning(warningText);
if (this.props.toastNotificationService) {
this.props.toastNotificationService.displayWarningToast(warningText);
}
detectorIndex = detectors[0].index;
}
@ -716,16 +736,17 @@ export class TimeSeriesExplorer extends React.Component {
// perhaps due to user's advanced setting using incorrect date-maths
const { invalidTimeRangeError } = this.props;
if (invalidTimeRangeError) {
const toastNotifications = getToastNotifications();
toastNotifications.addWarning(
i18n.translate('xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout', {
defaultMessage:
'The time filter was changed to the full range for this job due to an invalid default time filter. Check the advanced settings for {field}.',
values: {
field: ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
},
})
);
if (this.props.toastNotificationService) {
this.props.toastNotificationService.displayWarningToast(
i18n.translate('xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout', {
defaultMessage:
'The time filter was changed to the full range for this job due to an invalid default time filter. Check the advanced settings for {field}.',
values: {
field: ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
},
})
);
}
}
// Required to redraw the time series chart when the container is resized.
@ -853,7 +874,8 @@ export class TimeSeriesExplorer extends React.Component {
if (
previousProps === undefined ||
!isEqual(previousProps.bounds, this.props.bounds) ||
!isEqual(previousProps.lastRefresh, this.props.lastRefresh) ||
(!isEqual(previousProps.lastRefresh, this.props.lastRefresh) &&
previousProps.lastRefresh !== 0) ||
!isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
previousProps.selectedForecastId !== this.props.selectedForecastId ||
@ -938,6 +960,7 @@ export class TimeSeriesExplorer extends React.Component {
zoomTo,
zoomFromFocusLoaded,
zoomToFocusLoaded,
chartDataError,
} = this.state;
const chartProps = {
modelPlotEnabled,
@ -1041,10 +1064,15 @@ export class TimeSeriesExplorer extends React.Component {
/>
)}
{loading === false && chartDataError !== undefined && (
<TimeseriesexplorerChartDataError errorMsg={chartDataError} />
)}
{arePartitioningFieldsProvided &&
jobs.length > 0 &&
(fullRefresh === false || loading === false) &&
hasResults === false && (
hasResults === false &&
chartDataError === undefined && (
<TimeseriesexplorerNoChartData
dataNotChartable={dataNotChartable}
entities={entityControls}
@ -1149,6 +1177,7 @@ export class TimeSeriesExplorer extends React.Component {
</EuiFlexItem>
)}
</EuiFlexGroup>
<TimeSeriesChartWithTooltips
chartProps={chartProps}
contextAggregationInterval={contextAggregationInterval}