mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
ee8c9be778
commit
524e4d5481
5 changed files with 247 additions and 39 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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: '',
|
||||
};
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue