[ML] Fix Index Data Visualizer not gracefully handling error (#104567)

* Fix to show better error message

* Handle batch errors by still showing as much data as possible

* Fix i18n

* Fix errors

* Fix 404 error, add extractErrorProperties

* Fix missing histogram

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen 2021-07-13 11:35:39 -05:00 committed by GitHub
parent ee8c9be778
commit 524e4d5481
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 247 additions and 39 deletions

View file

@ -68,6 +68,7 @@ import { TimeBuckets } from '../../services/time_buckets';
import { extractSearchData } from '../../utils/saved_search_utils';
import { DataVisualizerIndexPatternManagement } from '../index_pattern_management';
import { ResultLink } from '../../../common/components/results_links';
import { extractErrorProperties } from '../../utils/error_utils';
interface DataVisualizerPageState {
overallStats: OverallStats;
@ -371,9 +372,16 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
earliest,
latest
);
// Because load overall stats perform queries in batches
// there could be multiple errors
if (Array.isArray(allStats.errors) && allStats.errors.length > 0) {
allStats.errors.forEach((err: any) => {
dataLoader.displayError(extractErrorProperties(err));
});
}
setOverallStats(allStats);
} catch (err) {
dataLoader.displayError(err);
dataLoader.displayError(err.body ?? err);
}
}

View file

@ -109,17 +109,17 @@ export class DataLoader {
'The request may have timed out. Try using a smaller sample size or narrowing the time range.',
values: {
index: this._indexPattern.title,
message: err.message,
message: err.error ?? err.message,
},
}),
});
} else {
this._toastNotifications.addError(err, {
title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage.', {
defaultMessage: 'Error loading data in index {index}. {message}',
title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage', {
defaultMessage: 'Error loading data in index {index}. {message}.',
values: {
index: this._indexPattern.title,
message: err.message,
message: err.error ?? err.message,
},
}),
});

View file

@ -0,0 +1,184 @@
/*
* 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 { HttpFetchError } from 'kibana/public';
import Boom from '@hapi/boom';
import { isPopulatedObject } from '../../../../common/utils/object_utils';
export interface WrappedError {
body: {
attributes: {
body: EsErrorBody;
};
message: Boom.Boom;
};
statusCode: number;
}
export interface EsErrorRootCause {
type: string;
reason: string;
caused_by?: EsErrorRootCause;
script?: string;
}
export interface EsErrorBody {
error: {
root_cause?: EsErrorRootCause[];
caused_by?: EsErrorRootCause;
type: string;
reason: string;
};
status: number;
}
export interface DVResponseError {
statusCode: number;
error: string;
message: string;
attributes?: {
body: EsErrorBody;
};
}
export interface ErrorMessage {
message: string;
}
export interface DVErrorObject {
causedBy?: string;
message: string;
statusCode?: number;
fullError?: EsErrorBody;
}
export interface DVHttpFetchError<T> extends HttpFetchError {
body: T;
}
export type ErrorType =
| WrappedError
| DVHttpFetchError<DVResponseError>
| EsErrorBody
| Boom.Boom
| string
| undefined;
export function isEsErrorBody(error: any): error is EsErrorBody {
return error && error.error?.reason !== undefined;
}
export function isErrorString(error: any): error is string {
return typeof error === 'string';
}
export function isErrorMessage(error: any): error is ErrorMessage {
return error && error.message !== undefined && typeof error.message === 'string';
}
export function isDVResponseError(error: any): error is DVResponseError {
return typeof error.body === 'object' && 'message' in error.body;
}
export function isBoomError(error: any): error is Boom.Boom {
return error.isBoom === true;
}
export function isWrappedError(error: any): error is WrappedError {
return error && isBoomError(error.body?.message) === true;
}
export const extractErrorProperties = (error: ErrorType): DVErrorObject => {
// extract properties of the error object from within the response error
// coming from Kibana, Elasticsearch, and our own DV messages
// some responses contain raw es errors as part of a bulk response
// e.g. if some jobs fail the action in a bulk request
if (isEsErrorBody(error)) {
return {
message: error.error.reason,
statusCode: error.status,
fullError: error,
};
}
if (isErrorString(error)) {
return {
message: error,
};
}
if (isWrappedError(error)) {
return error.body.message?.output?.payload;
}
if (isBoomError(error)) {
return {
message: error.output.payload.message,
statusCode: error.output.payload.statusCode,
};
}
if (error?.body === undefined && !error?.message) {
return {
message: '',
};
}
if (typeof error.body === 'string') {
return {
message: error.body,
};
}
if (isDVResponseError(error)) {
if (
typeof error.body.attributes === 'object' &&
typeof error.body.attributes.body?.error?.reason === 'string'
) {
const errObj: DVErrorObject = {
message: error.body.attributes.body.error.reason,
statusCode: error.body.statusCode,
fullError: error.body.attributes.body,
};
if (
typeof error.body.attributes.body.error.caused_by === 'object' &&
(typeof error.body.attributes.body.error.caused_by?.reason === 'string' ||
typeof error.body.attributes.body.error.caused_by?.caused_by?.reason === 'string')
) {
errObj.causedBy =
error.body.attributes.body.error.caused_by?.caused_by?.reason ||
error.body.attributes.body.error.caused_by?.reason;
}
if (
Array.isArray(error.body.attributes.body.error.root_cause) &&
typeof error.body.attributes.body.error.root_cause[0] === 'object' &&
isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script'])
) {
errObj.causedBy = error.body.attributes.body.error.root_cause[0].script;
errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`;
}
return errObj;
} else {
return {
message: error.body.message,
statusCode: error.body.statusCode,
};
}
}
if (isErrorMessage(error)) {
return {
message: error.message,
};
}
// If all else fail return an empty message instead of JSON.stringify
return {
message: '',
};
};

View file

@ -31,6 +31,7 @@ import {
getNumericFieldsStats,
getStringFieldsStats,
} from './get_fields_stats';
import { wrapError } from '../../utils/error_wrapper';
export class DataVisualizer {
private _client: IScopedClusterClient;
@ -60,6 +61,7 @@ export class DataVisualizer {
aggregatableNotExistsFields: [] as FieldData[],
nonAggregatableExistsFields: [] as FieldData[],
nonAggregatableNotExistsFields: [] as FieldData[],
errors: [] as any[],
};
// To avoid checking for the existence of too many aggregatable fields in one request,
@ -76,49 +78,61 @@ export class DataVisualizer {
await Promise.all(
batches.map(async (fields) => {
const batchStats = await this.checkAggregatableFieldsExist(
indexPatternTitle,
query,
fields,
samplerShardSize,
timeFieldName,
earliestMs,
latestMs,
undefined,
runtimeMappings
);
try {
const batchStats = await this.checkAggregatableFieldsExist(
indexPatternTitle,
query,
fields,
samplerShardSize,
timeFieldName,
earliestMs,
latestMs,
undefined,
runtimeMappings
);
// Total count will be returned with each batch of fields. Just overwrite.
stats.totalCount = batchStats.totalCount;
// Total count will be returned with each batch of fields. Just overwrite.
stats.totalCount = batchStats.totalCount;
// Add to the lists of fields which do and do not exist.
stats.aggregatableExistsFields.push(...batchStats.aggregatableExistsFields);
stats.aggregatableNotExistsFields.push(...batchStats.aggregatableNotExistsFields);
// Add to the lists of fields which do and do not exist.
stats.aggregatableExistsFields.push(...batchStats.aggregatableExistsFields);
stats.aggregatableNotExistsFields.push(...batchStats.aggregatableNotExistsFields);
} catch (e) {
// If index not found, no need to proceed with other batches
if (e.statusCode === 404) {
throw e;
}
stats.errors.push(wrapError(e));
}
})
);
await Promise.all(
nonAggregatableFields.map(async (field) => {
const existsInDocs = await this.checkNonAggregatableFieldExists(
indexPatternTitle,
query,
field,
timeFieldName,
earliestMs,
latestMs,
runtimeMappings
);
try {
const existsInDocs = await this.checkNonAggregatableFieldExists(
indexPatternTitle,
query,
field,
timeFieldName,
earliestMs,
latestMs,
runtimeMappings
);
const fieldData: FieldData = {
fieldName: field,
existsInDocs,
stats: {},
};
const fieldData: FieldData = {
fieldName: field,
existsInDocs,
stats: {},
};
if (existsInDocs === true) {
stats.nonAggregatableExistsFields.push(fieldData);
} else {
stats.nonAggregatableNotExistsFields.push(fieldData);
if (existsInDocs === true) {
stats.nonAggregatableExistsFields.push(fieldData);
} else {
stats.nonAggregatableNotExistsFields.push(fieldData);
}
} catch (e) {
stats.errors.push(wrapError(e));
}
})
);