[TSVB] Fix TSVB is not reporting all categories of Elasticsearch error (#102926)

* [TSVB] Fix TSVB is not reporting all categories of Elasticsearch error

Closes: #94182

* move validation to server side

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2021-06-30 10:54:06 +03:00 committed by GitHub
parent b92d955b56
commit 790bd35ea7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 285 additions and 142 deletions

View file

@ -25,7 +25,7 @@ export class FieldNotFoundError extends Error {
return this.constructor.name;
}
public get body() {
public get errBody() {
return this.message;
}
}

View file

@ -7,20 +7,29 @@
*/
import { i18n } from '@kbn/i18n';
import { GTE_INTERVAL_RE } from '../../../common/interval_regexp';
import { search } from '../../../../../plugins/data/public';
import { GTE_INTERVAL_RE } from './interval_regexp';
import { parseInterval, TimeRangeBounds } from '../../data/common';
import type { TimeRangeBounds } from '../../../../data/common';
import type { TimeseriesVisParams } from '../../types';
export class ValidateIntervalError extends Error {
constructor() {
super(
i18n.translate('visTypeTimeseries.validateInterval.notifier.maxBucketsExceededErrorMessage', {
defaultMessage:
'Your query attempted to fetch too much data. Reducing the time range or changing the interval used usually fixes the issue.',
})
);
}
const { parseInterval } = search.aggs;
public get name() {
return this.constructor.name;
}
export function validateInterval(
bounds: TimeRangeBounds,
panel: TimeseriesVisParams,
maxBuckets: number
) {
const { interval } = panel;
public get errBody() {
return this.message;
}
}
export function validateInterval(bounds: TimeRangeBounds, interval: string, maxBuckets: number) {
const { min, max } = bounds;
// No need to check auto it will return around 100
if (!interval) return;
@ -33,15 +42,7 @@ export function validateInterval(
const span = max!.valueOf() - min!.valueOf();
const buckets = Math.floor(span / duration.asMilliseconds());
if (buckets > maxBuckets) {
throw new Error(
i18n.translate(
'visTypeTimeseries.validateInterval.notifier.maxBucketsExceededErrorMessage',
{
defaultMessage:
'Your query attempted to fetch too much data. Reducing the time range or changing the interval used usually fixes the issue.',
}
)
);
throw new ValidateIntervalError();
}
}
}

View file

@ -18,8 +18,6 @@ import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { PersistedState } from 'src/plugins/visualizations/public';
import { PaletteRegistry } from 'src/plugins/charts/public';
// @ts-expect-error
import { ErrorComponent } from './error';
import { TimeseriesVisTypes } from './vis_types';
import type { TimeseriesVisData, PanelData } from '../../../common/types';
import { isVisSeriesData } from '../../../common/vis_data_utils';
@ -147,16 +145,6 @@ function TimeseriesVisualization({
handlers.done();
});
// Show the error panel
const error = isVisSeriesData(visData) && visData[model.id]?.error;
if (error) {
return (
<div className={className}>
<ErrorComponent error={error} />
</div>
);
}
const VisComponent = TimeseriesVisTypes[model.type];
const isLastValueMode =

View file

@ -159,8 +159,8 @@ export class VisEditor extends Component<TimeseriesEditorProps, TimeseriesEditor
this.setState({ autoApply: event.target.checked });
};
onDataChange = ({ visData }: { visData: TimeseriesVisData }) => {
this.visDataSubject.next(visData);
onDataChange = (data: { visData?: TimeseriesVisData }) => {
this.visDataSubject.next(data?.visData);
};
render() {

View file

@ -6,14 +6,13 @@
* Side Public License, v 1.
*/
import { KibanaContext } from '../../data/public';
import { getTimezone } from './application/lib/get_timezone';
import { validateInterval } from './application/lib/validate_interval';
import { getUISettings, getDataStart, getCoreStart } from './services';
import { MAX_BUCKETS_SETTING, ROUTES } from '../common/constants';
import { TimeseriesVisParams } from './types';
import { ROUTES } from '../common/constants';
import type { TimeseriesVisParams } from './types';
import type { TimeseriesVisData } from '../common/types';
import type { KibanaContext } from '../../data/public';
interface MetricsRequestHandlerParams {
input: KibanaContext | null;
@ -37,10 +36,6 @@ export const metricsRequestHandler = async ({
const parsedTimeRange = data.query.timefilter.timefilter.calculateBounds(input?.timeRange!);
if (visParams && visParams.id && !visParams.isModelInvalid) {
const maxBuckets = config.get<number>(MAX_BUCKETS_SETTING);
validateInterval(parsedTimeRange, visParams, maxBuckets);
const untrackSearch =
dataSearch.session.isCurrentSession(searchSessionId) &&
dataSearch.session.trackSearch({

View file

@ -9,7 +9,7 @@
import _ from 'lodash';
import { Framework } from '../plugin';
import type { TimeseriesVisData } from '../../common/types';
import type { TimeseriesVisData, FetchedIndexPattern, Series } from '../../common/types';
import { PANEL_TYPES } from '../../common/enums';
import type {
VisTypeTimeseriesVisDataRequest,
@ -20,6 +20,8 @@ import { getSeriesData } from './vis_data/get_series_data';
import { getTableData } from './vis_data/get_table_data';
import { getEsQueryConfig } from './vis_data/helpers/get_es_query_uisettings';
import { getCachedIndexPatternFetcher } from './search_strategies/lib/cached_index_pattern_fetcher';
import { MAX_BUCKETS_SETTING } from '../../common/constants';
import { getIntervalAndTimefield } from './vis_data/get_interval_and_timefield';
export async function getVisData(
requestContext: VisTypeTimeseriesRequestHandlerContext,
@ -32,15 +34,41 @@ export async function getVisData(
const esQueryConfig = await getEsQueryConfig(uiSettings);
const promises = request.body.panels.map((panel) => {
const cachedIndexPatternFetcher = getCachedIndexPatternFetcher(indexPatternsService, {
fetchKibanaIndexForStringIndexes: Boolean(panel.use_kibana_indexes),
});
const services: VisTypeTimeseriesRequestServices = {
esQueryConfig,
esShardTimeout,
indexPatternsService,
uiSettings,
cachedIndexPatternFetcher,
searchStrategyRegistry: framework.searchStrategyRegistry,
cachedIndexPatternFetcher: getCachedIndexPatternFetcher(indexPatternsService, {
fetchKibanaIndexForStringIndexes: Boolean(panel.use_kibana_indexes),
}),
buildSeriesMetaParams: async (
index: FetchedIndexPattern,
useKibanaIndexes: boolean,
series?: Series
) => {
/** This part of code is required to try to get the default timefield for string indices.
* The rest of the functionality available for Kibana indexes should not be active **/
if (!useKibanaIndexes && index.indexPatternString) {
index = await cachedIndexPatternFetcher(index.indexPatternString, true);
}
const maxBuckets = await uiSettings.get<number>(MAX_BUCKETS_SETTING);
const { min, max } = request.body.timerange;
return getIntervalAndTimefield(
panel,
index,
{
min,
max,
maxBuckets,
},
series
);
},
};
return panel.type === PANEL_TYPES.TABLE

View file

@ -11,12 +11,17 @@ import { FetchedIndexPattern, Panel, Series } from '../../../common/types';
describe('getIntervalAndTimefield(panel, series)', () => {
const index: FetchedIndexPattern = {} as FetchedIndexPattern;
const params = {
min: '2017-01-01T00:00:00Z',
max: '2017-01-01T01:00:00Z',
maxBuckets: 1000,
};
test('returns the panel interval and timefield', () => {
const panel = { time_field: '@timestamp', interval: 'auto' } as Panel;
const series = {} as Series;
expect(getIntervalAndTimefield(panel, index, series)).toEqual({
expect(getIntervalAndTimefield(panel, index, params, series)).toEqual({
timeField: '@timestamp',
interval: 'auto',
});
@ -30,7 +35,7 @@ describe('getIntervalAndTimefield(panel, series)', () => {
series_time_field: 'time',
} as unknown) as Series;
expect(getIntervalAndTimefield(panel, index, series)).toEqual({
expect(getIntervalAndTimefield(panel, index, params, series)).toEqual({
timeField: 'time',
interval: '1m',
});

View file

@ -5,13 +5,25 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import moment from 'moment';
import { AUTO_INTERVAL } from '../../../common/constants';
import { validateField } from '../../../common/fields_utils';
import { validateInterval } from '../../../common/validate_interval';
import type { FetchedIndexPattern, Panel, Series } from '../../../common/types';
export function getIntervalAndTimefield(panel: Panel, index: FetchedIndexPattern, series?: Series) {
interface IntervalParams {
min: string;
max: string;
maxBuckets: number;
}
export function getIntervalAndTimefield(
panel: Panel,
index: FetchedIndexPattern,
{ min, max, maxBuckets }: IntervalParams,
series?: Series
) {
const timeField =
(series?.override_index_pattern ? series.series_time_field : panel.time_field) ||
index.indexPattern?.timeFieldName;
@ -28,6 +40,15 @@ export function getIntervalAndTimefield(panel: Panel, index: FetchedIndexPattern
maxBars = series.series_max_bars;
}
validateInterval(
{
min: moment.utc(min),
max: moment.utc(max),
},
interval,
maxBuckets
);
return {
maxBars,
timeField,

View file

@ -8,8 +8,6 @@
import { i18n } from '@kbn/i18n';
// not typed yet
// @ts-expect-error
import { handleErrorResponse } from './handle_error_response';
import { getAnnotations } from './get_annotations';
import { handleResponseBody } from './series/handle_response_body';
@ -51,6 +49,8 @@ export async function getSeriesData(
uiRestrictions: capabilities.uiRestrictions,
};
const handleError = handleErrorResponse(panel);
try {
const bodiesPromises = getActiveSeries(panel).map((series) =>
getSeriesRequestParams(req, panel, panelIndex, series, capabilities, services)
@ -97,14 +97,9 @@ export async function getSeriesData(
},
};
} catch (err) {
if (err.body) {
err.response = err.body;
return {
...meta,
...handleErrorResponse(panel)(err),
};
}
return meta;
return {
...meta,
...handleError(err),
};
}
}

View file

@ -12,20 +12,19 @@ import { get } from 'lodash';
// not typed yet
// @ts-expect-error
import { buildRequestBody } from './table/build_request_body';
// @ts-expect-error
import { handleErrorResponse } from './handle_error_response';
// @ts-expect-error
import { processBucket } from './table/process_bucket';
import { createFieldsFetcher } from '../search_strategies/lib/fields_fetcher';
import { extractFieldLabel } from '../../../common/fields_utils';
import type {
VisTypeTimeseriesRequestHandlerContext,
VisTypeTimeseriesRequestServices,
VisTypeTimeseriesVisDataRequest,
} from '../../types';
import type { Panel } from '../../../common/types';
import { getIntervalAndTimefield } from './get_interval_and_timefield';
export async function getTableData(
requestContext: VisTypeTimeseriesRequestHandlerContext,
@ -67,23 +66,13 @@ export async function getTableData(
return panel.pivot_id;
};
const buildSeriesMetaParams = async () => {
let index = panelIndex;
/** This part of code is required to try to get the default timefield for string indices.
* The rest of the functionality available for Kibana indexes should not be active **/
if (!panel.use_kibana_indexes && index.indexPatternString) {
index = await services.cachedIndexPatternFetcher(index.indexPatternString, true);
}
return getIntervalAndTimefield(panel, index);
};
const meta = {
type: panel.type,
uiRestrictions: capabilities.uiRestrictions,
};
const handleError = handleErrorResponse(panel);
try {
const body = await buildRequestBody(
req,
@ -92,7 +81,7 @@ export async function getTableData(
panelIndex,
capabilities,
services.uiSettings,
buildSeriesMetaParams
() => services.buildSeriesMetaParams(panelIndex, Boolean(panel.use_kibana_indexes))
);
const [resp] = await searchStrategy.search(requestContext, req, [
@ -121,14 +110,9 @@ export async function getTableData(
series,
};
} catch (err) {
if (err.body) {
err.response = err.body;
return {
...meta,
...handleErrorResponse(panel)(err),
};
}
return meta;
return {
...meta,
...handleError(err),
};
}
}

View file

@ -1,37 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const handleErrorResponse = (panel) => (error) => {
if (error.isBoom && error.status === 401) throw error;
const result = {};
let errorResponse;
try {
errorResponse = JSON.parse(error.response);
} catch (e) {
errorResponse = error.response;
}
if (!errorResponse && !(error.name === 'KQLSyntaxError')) {
errorResponse = {
message: error.message,
stack: error.stack,
};
}
if (error.name === 'KQLSyntaxError') {
errorResponse = {
message: error.shortMessage,
stack: error.stack,
};
}
result[panel.id] = {
id: panel.id,
statusCode: error.statusCode,
error: errorResponse,
series: [],
};
return result;
};

View file

@ -0,0 +1,99 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Panel } from '../../../common/types';
import { handleErrorResponse, ErrorResponse } from './handle_error_response';
describe('handleErrorResponse', () => {
const handleError = handleErrorResponse(({
id: 'test_panel',
} as unknown) as Panel);
test('should only handle errors that contain errBody', () => {
expect(handleError(new Error('Test Error'))).toMatchInlineSnapshot(`Object {}`);
expect(handleError({ errBody: 'test' } as ErrorResponse)).toMatchInlineSnapshot(`
Object {
"test_panel": Object {
"error": "test",
"id": "test_panel",
"series": Array [],
},
}
`);
});
test('should set as error the last value of caused_by', () => {
expect(
handleError({
errBody: {
error: {
reason: 'wrong 0',
caused_by: {
reason: 'wrong 1',
caused_by: {
caused_by: 'ok',
},
},
},
},
} as ErrorResponse)
).toMatchInlineSnapshot(`
Object {
"test_panel": Object {
"error": "ok",
"id": "test_panel",
"series": Array [],
},
}
`);
});
test('should use the previous error message if the actual value is empty', () => {
expect(
handleError({
errBody: {
error: {
reason: 'ok',
caused_by: {
reason: '',
},
},
},
} as ErrorResponse)
).toMatchInlineSnapshot(`
Object {
"test_panel": Object {
"error": "ok",
"id": "test_panel",
"series": Array [],
},
}
`);
});
test('shouldn not return empty error message', () => {
expect(
handleError({
errBody: {
error: {
reason: '',
},
},
} as ErrorResponse)
).toMatchInlineSnapshot(`
Object {
"test_panel": Object {
"error": "Unexpected error",
"id": "test_panel",
"series": Array [],
},
}
`);
});
});

View file

@ -0,0 +1,57 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import type { Panel } from '../../../common/types';
type ErrorType =
| {
reason: string;
caused_by?: ErrorType;
}
| string;
export type ErrorResponse = Error &
Partial<{
errBody:
| {
error: ErrorType;
}
| string;
}>;
const getErrorMessage = (errBody: ErrorType, defaultMessage?: string): string | undefined => {
if (typeof errBody === 'string') {
return errBody;
} else {
if (errBody.caused_by) {
return getErrorMessage(errBody.caused_by, errBody.reason);
}
return errBody.reason || defaultMessage;
}
};
export const handleErrorResponse = (panel: Panel) => (error: ErrorResponse) => {
const result: Record<string, unknown> = {};
if (error.errBody) {
const errorResponse =
typeof error.errBody === 'string' ? error.errBody : getErrorMessage(error.errBody.error);
result[panel.id] = {
id: panel.id,
error:
errorResponse ??
i18n.translate('visTypeTimeseries.handleErrorResponse.unexpectedError', {
defaultMessage: 'Unexpected error',
}),
series: [],
};
}
return result;
};

View file

@ -47,7 +47,16 @@ describe('dateHistogram(req, panel, series)', () => {
get: async (key) => (key === UI_SETTINGS.HISTOGRAM_MAX_BARS ? 100 : 50),
};
buildSeriesMetaParams = jest.fn(async () => {
return getIntervalAndTimefield(panel, indexPattern, series);
return getIntervalAndTimefield(
panel,
indexPattern,
{
min: '2017-01-01T00:00:00Z',
max: '2017-01-01T01:00:00Z',
maxBuckets: 1000,
},
series
);
});
});

View file

@ -7,7 +7,6 @@
*/
import { buildRequestBody } from './build_request_body';
import { getIntervalAndTimefield } from '../get_interval_and_timefield';
import type { FetchedIndexPattern, Panel, Series } from '../../../../common/types';
import type {
@ -27,6 +26,7 @@ export async function getSeriesRequestParams(
esShardTimeout,
uiSettings,
cachedIndexPatternFetcher,
buildSeriesMetaParams,
}: VisTypeTimeseriesRequestServices
) {
let seriesIndex = panelIndex;
@ -35,18 +35,6 @@ export async function getSeriesRequestParams(
seriesIndex = await cachedIndexPatternFetcher(series.series_index_pattern ?? '');
}
const buildSeriesMetaParams = async () => {
let index = seriesIndex;
/** This part of code is required to try to get the default timefield for string indices.
* The rest of the functionality available for Kibana indexes should not be active **/
if (!panel.use_kibana_indexes && index.indexPatternString) {
index = await cachedIndexPatternFetcher(index.indexPatternString, true);
}
return getIntervalAndTimefield(panel, index, series);
};
const request = await buildRequestBody(
req,
panel,
@ -55,7 +43,7 @@ export async function getSeriesRequestParams(
seriesIndex,
capabilities,
uiSettings,
buildSeriesMetaParams
() => buildSeriesMetaParams(seriesIndex, Boolean(panel.use_kibana_indexes), series)
);
return {

View file

@ -6,17 +6,18 @@
* Side Public License, v 1.
*/
import { Observable } from 'rxjs';
import { SharedGlobalConfig } from 'kibana/server';
import type { Observable } from 'rxjs';
import type { SharedGlobalConfig } from 'kibana/server';
import type { IRouter, IUiSettingsClient, KibanaRequest } from 'src/core/server';
import type {
DataRequestHandlerContext,
EsQueryConfig,
IndexPatternsService,
} from '../../data/server';
import type { VisPayload } from '../common/types';
import type { Series, VisPayload } from '../common/types';
import type { SearchStrategyRegistry } from './lib/search_strategies';
import type { CachedIndexPatternFetcher } from './lib/search_strategies/lib/cached_index_pattern_fetcher';
import type { FetchedIndexPattern } from '../common/types';
export type ConfigObservable = Observable<SharedGlobalConfig>;
@ -35,4 +36,13 @@ export interface VisTypeTimeseriesRequestServices {
indexPatternsService: IndexPatternsService;
searchStrategyRegistry: SearchStrategyRegistry;
cachedIndexPatternFetcher: CachedIndexPatternFetcher;
buildSeriesMetaParams: (
index: FetchedIndexPattern,
useKibanaIndexes: boolean,
series?: Series
) => Promise<{
maxBars: number;
timeField?: string;
interval: string;
}>;
}