Skip to content

Commit 524e4d5

Browse files
qn895kibanamachine
andauthored
[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>
1 parent ee8c9be commit 524e4d5

File tree

5 files changed

+247
-39
lines changed

5 files changed

+247
-39
lines changed

x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { TimeBuckets } from '../../services/time_buckets';
6868
import { extractSearchData } from '../../utils/saved_search_utils';
6969
import { DataVisualizerIndexPatternManagement } from '../index_pattern_management';
7070
import { ResultLink } from '../../../common/components/results_links';
71+
import { extractErrorProperties } from '../../utils/error_utils';
7172

7273
interface DataVisualizerPageState {
7374
overallStats: OverallStats;
@@ -371,9 +372,16 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
371372
earliest,
372373
latest
373374
);
375+
// Because load overall stats perform queries in batches
376+
// there could be multiple errors
377+
if (Array.isArray(allStats.errors) && allStats.errors.length > 0) {
378+
allStats.errors.forEach((err: any) => {
379+
dataLoader.displayError(extractErrorProperties(err));
380+
});
381+
}
374382
setOverallStats(allStats);
375383
} catch (err) {
376-
dataLoader.displayError(err);
384+
dataLoader.displayError(err.body ?? err);
377385
}
378386
}
379387

x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,17 @@ export class DataLoader {
109109
'The request may have timed out. Try using a smaller sample size or narrowing the time range.',
110110
values: {
111111
index: this._indexPattern.title,
112-
message: err.message,
112+
message: err.error ?? err.message,
113113
},
114114
}),
115115
});
116116
} else {
117117
this._toastNotifications.addError(err, {
118-
title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage.', {
119-
defaultMessage: 'Error loading data in index {index}. {message}',
118+
title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage', {
119+
defaultMessage: 'Error loading data in index {index}. {message}.',
120120
values: {
121121
index: this._indexPattern.title,
122-
message: err.message,
122+
message: err.error ?? err.message,
123123
},
124124
}),
125125
});
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { HttpFetchError } from 'kibana/public';
9+
import Boom from '@hapi/boom';
10+
import { isPopulatedObject } from '../../../../common/utils/object_utils';
11+
12+
export interface WrappedError {
13+
body: {
14+
attributes: {
15+
body: EsErrorBody;
16+
};
17+
message: Boom.Boom;
18+
};
19+
statusCode: number;
20+
}
21+
22+
export interface EsErrorRootCause {
23+
type: string;
24+
reason: string;
25+
caused_by?: EsErrorRootCause;
26+
script?: string;
27+
}
28+
29+
export interface EsErrorBody {
30+
error: {
31+
root_cause?: EsErrorRootCause[];
32+
caused_by?: EsErrorRootCause;
33+
type: string;
34+
reason: string;
35+
};
36+
status: number;
37+
}
38+
39+
export interface DVResponseError {
40+
statusCode: number;
41+
error: string;
42+
message: string;
43+
attributes?: {
44+
body: EsErrorBody;
45+
};
46+
}
47+
48+
export interface ErrorMessage {
49+
message: string;
50+
}
51+
52+
export interface DVErrorObject {
53+
causedBy?: string;
54+
message: string;
55+
statusCode?: number;
56+
fullError?: EsErrorBody;
57+
}
58+
59+
export interface DVHttpFetchError<T> extends HttpFetchError {
60+
body: T;
61+
}
62+
63+
export type ErrorType =
64+
| WrappedError
65+
| DVHttpFetchError<DVResponseError>
66+
| EsErrorBody
67+
| Boom.Boom
68+
| string
69+
| undefined;
70+
71+
export function isEsErrorBody(error: any): error is EsErrorBody {
72+
return error && error.error?.reason !== undefined;
73+
}
74+
75+
export function isErrorString(error: any): error is string {
76+
return typeof error === 'string';
77+
}
78+
79+
export function isErrorMessage(error: any): error is ErrorMessage {
80+
return error && error.message !== undefined && typeof error.message === 'string';
81+
}
82+
83+
export function isDVResponseError(error: any): error is DVResponseError {
84+
return typeof error.body === 'object' && 'message' in error.body;
85+
}
86+
87+
export function isBoomError(error: any): error is Boom.Boom {
88+
return error.isBoom === true;
89+
}
90+
91+
export function isWrappedError(error: any): error is WrappedError {
92+
return error && isBoomError(error.body?.message) === true;
93+
}
94+
95+
export const extractErrorProperties = (error: ErrorType): DVErrorObject => {
96+
// extract properties of the error object from within the response error
97+
// coming from Kibana, Elasticsearch, and our own DV messages
98+
99+
// some responses contain raw es errors as part of a bulk response
100+
// e.g. if some jobs fail the action in a bulk request
101+
102+
if (isEsErrorBody(error)) {
103+
return {
104+
message: error.error.reason,
105+
statusCode: error.status,
106+
fullError: error,
107+
};
108+
}
109+
110+
if (isErrorString(error)) {
111+
return {
112+
message: error,
113+
};
114+
}
115+
if (isWrappedError(error)) {
116+
return error.body.message?.output?.payload;
117+
}
118+
119+
if (isBoomError(error)) {
120+
return {
121+
message: error.output.payload.message,
122+
statusCode: error.output.payload.statusCode,
123+
};
124+
}
125+
126+
if (error?.body === undefined && !error?.message) {
127+
return {
128+
message: '',
129+
};
130+
}
131+
132+
if (typeof error.body === 'string') {
133+
return {
134+
message: error.body,
135+
};
136+
}
137+
138+
if (isDVResponseError(error)) {
139+
if (
140+
typeof error.body.attributes === 'object' &&
141+
typeof error.body.attributes.body?.error?.reason === 'string'
142+
) {
143+
const errObj: DVErrorObject = {
144+
message: error.body.attributes.body.error.reason,
145+
statusCode: error.body.statusCode,
146+
fullError: error.body.attributes.body,
147+
};
148+
if (
149+
typeof error.body.attributes.body.error.caused_by === 'object' &&
150+
(typeof error.body.attributes.body.error.caused_by?.reason === 'string' ||
151+
typeof error.body.attributes.body.error.caused_by?.caused_by?.reason === 'string')
152+
) {
153+
errObj.causedBy =
154+
error.body.attributes.body.error.caused_by?.caused_by?.reason ||
155+
error.body.attributes.body.error.caused_by?.reason;
156+
}
157+
if (
158+
Array.isArray(error.body.attributes.body.error.root_cause) &&
159+
typeof error.body.attributes.body.error.root_cause[0] === 'object' &&
160+
isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script'])
161+
) {
162+
errObj.causedBy = error.body.attributes.body.error.root_cause[0].script;
163+
errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`;
164+
}
165+
return errObj;
166+
} else {
167+
return {
168+
message: error.body.message,
169+
statusCode: error.body.statusCode,
170+
};
171+
}
172+
}
173+
174+
if (isErrorMessage(error)) {
175+
return {
176+
message: error.message,
177+
};
178+
}
179+
180+
// If all else fail return an empty message instead of JSON.stringify
181+
return {
182+
message: '',
183+
};
184+
};

x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
getNumericFieldsStats,
3232
getStringFieldsStats,
3333
} from './get_fields_stats';
34+
import { wrapError } from '../../utils/error_wrapper';
3435

3536
export class DataVisualizer {
3637
private _client: IScopedClusterClient;
@@ -60,6 +61,7 @@ export class DataVisualizer {
6061
aggregatableNotExistsFields: [] as FieldData[],
6162
nonAggregatableExistsFields: [] as FieldData[],
6263
nonAggregatableNotExistsFields: [] as FieldData[],
64+
errors: [] as any[],
6365
};
6466

6567
// To avoid checking for the existence of too many aggregatable fields in one request,
@@ -76,49 +78,61 @@ export class DataVisualizer {
7678

7779
await Promise.all(
7880
batches.map(async (fields) => {
79-
const batchStats = await this.checkAggregatableFieldsExist(
80-
indexPatternTitle,
81-
query,
82-
fields,
83-
samplerShardSize,
84-
timeFieldName,
85-
earliestMs,
86-
latestMs,
87-
undefined,
88-
runtimeMappings
89-
);
81+
try {
82+
const batchStats = await this.checkAggregatableFieldsExist(
83+
indexPatternTitle,
84+
query,
85+
fields,
86+
samplerShardSize,
87+
timeFieldName,
88+
earliestMs,
89+
latestMs,
90+
undefined,
91+
runtimeMappings
92+
);
9093

91-
// Total count will be returned with each batch of fields. Just overwrite.
92-
stats.totalCount = batchStats.totalCount;
94+
// Total count will be returned with each batch of fields. Just overwrite.
95+
stats.totalCount = batchStats.totalCount;
9396

94-
// Add to the lists of fields which do and do not exist.
95-
stats.aggregatableExistsFields.push(...batchStats.aggregatableExistsFields);
96-
stats.aggregatableNotExistsFields.push(...batchStats.aggregatableNotExistsFields);
97+
// Add to the lists of fields which do and do not exist.
98+
stats.aggregatableExistsFields.push(...batchStats.aggregatableExistsFields);
99+
stats.aggregatableNotExistsFields.push(...batchStats.aggregatableNotExistsFields);
100+
} catch (e) {
101+
// If index not found, no need to proceed with other batches
102+
if (e.statusCode === 404) {
103+
throw e;
104+
}
105+
stats.errors.push(wrapError(e));
106+
}
97107
})
98108
);
99109

100110
await Promise.all(
101111
nonAggregatableFields.map(async (field) => {
102-
const existsInDocs = await this.checkNonAggregatableFieldExists(
103-
indexPatternTitle,
104-
query,
105-
field,
106-
timeFieldName,
107-
earliestMs,
108-
latestMs,
109-
runtimeMappings
110-
);
112+
try {
113+
const existsInDocs = await this.checkNonAggregatableFieldExists(
114+
indexPatternTitle,
115+
query,
116+
field,
117+
timeFieldName,
118+
earliestMs,
119+
latestMs,
120+
runtimeMappings
121+
);
111122

112-
const fieldData: FieldData = {
113-
fieldName: field,
114-
existsInDocs,
115-
stats: {},
116-
};
123+
const fieldData: FieldData = {
124+
fieldName: field,
125+
existsInDocs,
126+
stats: {},
127+
};
117128

118-
if (existsInDocs === true) {
119-
stats.nonAggregatableExistsFields.push(fieldData);
120-
} else {
121-
stats.nonAggregatableNotExistsFields.push(fieldData);
129+
if (existsInDocs === true) {
130+
stats.nonAggregatableExistsFields.push(fieldData);
131+
} else {
132+
stats.nonAggregatableNotExistsFields.push(fieldData);
133+
}
134+
} catch (e) {
135+
stats.errors.push(wrapError(e));
122136
}
123137
})
124138
);

x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export default ({ getService }: FtrProviderContext) => {
5252
aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }],
5353
nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }],
5454
nonAggregatableNotExistsFields: [],
55+
errors: [],
5556
},
5657
},
5758
},
@@ -98,6 +99,7 @@ export default ({ getService }: FtrProviderContext) => {
9899
aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }],
99100
nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }],
100101
nonAggregatableNotExistsFields: [],
102+
errors: [],
101103
},
102104
},
103105
},

0 commit comments

Comments
 (0)