mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[lens] show 'View details' UI action to open clusters inspector tab when request fails (#172971)
Closes https://github.com/elastic/kibana/issues/171570 PR make the following changes 1. Consolidates data EsError logic and Lens EsError logic. This resulted in being able to remove large parts of lens error_helper.tsx file. 2. Consolidates lens WorkspacePanel error logic. Before PR configuration errors and data loading errors each rendered their own version of EuiEmptyPrompt with slightly different behavior. Now, both render WorkspaceErrors component and have the same behavior 3. Updated lens ExpressionWrapper to return original error to embeddable output. ### Test - EsError in embeddable 1. install sample web logs 2. create new dashboard 3. Click "Create visualization" 4. Drag "timestamp" field into workspace. 5. Click "Save and return" 6. Add filter ``` { "error_query": { "indices": [ { "error_type": "exception", "message": "local shard failure message 123", "name": "kibana_sample_data_logs" } ] } } ``` 7. Verify EsError and "View details" action are displayed <img width="500" alt="Screenshot 2023-12-08 at 1 34 20 PM" src="1c65b7f3
-ece7-4000-a5b2-11127cc38f01"> ### Test - multiple configuration errors in lens editor 1. install sample web logs 2. create new lens visualization 3. Drag "timestamp" field into workspace. 4. Add filter ``` { "error_query": { "indices": [ { "error_type": "exception", "message": "local shard failure message 123", "name": "kibana_sample_data_logs" } ] } } ``` 5. Verify EsError and "View details" action are displayed <img width="500" alt="Screenshot 2023-12-08 at 1 09 22 PM" src="22023512
-d344-4f99-abbd-8427d764f821"> ### Test - EsError in embeddable 1. install sample web logs 2. create new dashboard 3. Click "Create visualization" 4. Drag "timestamp" field into workspace. 5. Change "Vertical axis" to "Cumulative sum". Select "Field" select and hit delete key 6. Clone layer one or more times 7. Verify pagination is displayed, allowing users to click through all configuration errors <img width="500" alt="Screenshot 2023-12-08 at 12 59 18 PM" src="6302658a
-8cf7-4a1a-a117-ae810c0af539"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
parent
5798255638
commit
f2ad024082
32 changed files with 584 additions and 908 deletions
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { createEsError } from './src/create_es_error';
|
||||
export { isEsError, EsError } from './src/es_error';
|
||||
export { isPainlessError, PainlessError } from './src/painless_error';
|
||||
export { renderSearchError } from './src/render_search_error';
|
||||
export type { IEsError } from './src/types';
|
||||
|
|
13
packages/kbn-search-errors/src/__snapshots__/es_error.test.tsx.snap
generated
Normal file
13
packages/kbn-search-errors/src/__snapshots__/es_error.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,13 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EsError should render error message 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<code
|
||||
data-test-subj="errMessage"
|
||||
>
|
||||
The supplied interval [2q] could not be parsed as a calendar interval.
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
`;
|
29
packages/kbn-search-errors/src/__snapshots__/painless_error.test.tsx.snap
generated
Normal file
29
packages/kbn-search-errors/src/__snapshots__/painless_error.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Painless error should render error message 1`] = `
|
||||
<div>
|
||||
<EuiText
|
||||
data-test-subj="painlessScript"
|
||||
size="s"
|
||||
>
|
||||
Error executing runtime field or scripted field on data view logs
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiCodeBlock
|
||||
data-test-subj="painlessStackTrace"
|
||||
isCopyable={true}
|
||||
paddingSize="s"
|
||||
>
|
||||
invalid
|
||||
^---- HERE
|
||||
</EuiCodeBlock>
|
||||
<EuiText
|
||||
data-test-subj="painlessHumanReadableError"
|
||||
size="s"
|
||||
>
|
||||
cannot resolve symbol [invalid]
|
||||
</EuiText>
|
||||
</div>
|
||||
`;
|
18
packages/kbn-search-errors/src/__snapshots__/tsdb_error.test.tsx.snap
generated
Normal file
18
packages/kbn-search-errors/src/__snapshots__/tsdb_error.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Tsdb error should render error message 1`] = `
|
||||
<div>
|
||||
<p
|
||||
className="eui-textBreakWord"
|
||||
>
|
||||
The field [bytes_counter] of Time series type [counter] has been used with the unsupported operation [sum].
|
||||
</p>
|
||||
<EuiLink
|
||||
external={true}
|
||||
href=""
|
||||
target="_blank"
|
||||
>
|
||||
See more about Time series field types and [counter] supported aggregations
|
||||
</EuiLink>
|
||||
</div>
|
||||
`;
|
64
packages/kbn-search-errors/src/create_es_error.ts
Normal file
64
packages/kbn-search-errors/src/create_es_error.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { estypes } from '@elastic/elasticsearch';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ApplicationStart, CoreStart } from '@kbn/core/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { IEsError } from './types';
|
||||
import { EsError } from './es_error';
|
||||
import { PainlessError } from './painless_error';
|
||||
import { TsdbError } from './tsdb_error';
|
||||
|
||||
export interface Services {
|
||||
application: ApplicationStart;
|
||||
docLinks: CoreStart['docLinks'];
|
||||
}
|
||||
|
||||
function getNestedCauses(errorCause: estypes.ErrorCause): estypes.ErrorCause[] {
|
||||
// Give shard failures priority, then try to get the error navigating nested objects
|
||||
if (errorCause.failed_shards) {
|
||||
return (errorCause.failed_shards as estypes.ShardFailure[]).map(
|
||||
(shardFailure) => shardFailure.reason
|
||||
);
|
||||
}
|
||||
return errorCause.caused_by ? getNestedCauses(errorCause.caused_by) : [errorCause];
|
||||
}
|
||||
|
||||
export function createEsError(
|
||||
err: IEsError,
|
||||
openInInspector: () => void,
|
||||
services: Services,
|
||||
dataView?: DataView
|
||||
) {
|
||||
const rootCauses = err.attributes?.error ? getNestedCauses(err.attributes?.error) : [];
|
||||
|
||||
const painlessCause = rootCauses.find((errorCause) => {
|
||||
return errorCause.lang && errorCause.lang === 'painless';
|
||||
});
|
||||
if (painlessCause) {
|
||||
return new PainlessError(err, openInInspector, painlessCause, services.application, dataView);
|
||||
}
|
||||
|
||||
const tsdbCause = rootCauses.find((errorCause) => {
|
||||
return (
|
||||
errorCause.type === 'illegal_argument_exception' &&
|
||||
errorCause.reason &&
|
||||
/\]\[counter\] is not supported for aggregation/.test(errorCause.reason)
|
||||
);
|
||||
});
|
||||
if (tsdbCause) {
|
||||
return new TsdbError(err, openInInspector, tsdbCause, services.docLinks);
|
||||
}
|
||||
|
||||
const causeReason = rootCauses[0]?.reason ?? err.attributes?.error?.reason;
|
||||
const message = causeReason
|
||||
? causeReason
|
||||
: i18n.translate('searchErrors.esError.unknownRootCause', { defaultMessage: 'unknown' });
|
||||
return new EsError(err, message, openInInspector);
|
||||
}
|
|
@ -6,41 +6,31 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EsError } from './es_error';
|
||||
import { IEsError } from './types';
|
||||
import type { ReactElement } from 'react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { createEsError } from './create_es_error';
|
||||
import { renderSearchError } from './render_search_error';
|
||||
import { shallow } from 'enzyme';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
||||
const services = {
|
||||
application: coreMock.createStart().application,
|
||||
docLinks: {
|
||||
links: {
|
||||
fleet: {
|
||||
datastreamsTSDSMetrics: '',
|
||||
},
|
||||
},
|
||||
} as CoreStart['docLinks'],
|
||||
};
|
||||
|
||||
describe('EsError', () => {
|
||||
it('contains the same body as the wrapped error', () => {
|
||||
const error = {
|
||||
statusCode: 500,
|
||||
message: 'nope',
|
||||
attributes: {
|
||||
error: {
|
||||
type: 'top_level_exception_type',
|
||||
reason: 'top-level reason',
|
||||
},
|
||||
},
|
||||
} as IEsError;
|
||||
const esError = new EsError(error, () => {});
|
||||
|
||||
expect(typeof esError.attributes).toEqual('object');
|
||||
expect(esError.attributes).toEqual(error.attributes);
|
||||
});
|
||||
|
||||
it('contains some explanation of the error in the message', () => {
|
||||
// error taken from Vega's issue
|
||||
const error = {
|
||||
message:
|
||||
'x_content_parse_exception: [x_content_parse_exception] Reason: [1:78] [date_histogram] failed to parse field [calendar_interval]',
|
||||
const esError = createEsError(
|
||||
{
|
||||
statusCode: 400,
|
||||
message: 'search_phase_execution_exception',
|
||||
attributes: {
|
||||
error: {
|
||||
root_cause: [
|
||||
{
|
||||
type: 'x_content_parse_exception',
|
||||
reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]',
|
||||
},
|
||||
],
|
||||
type: 'x_content_parse_exception',
|
||||
reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]',
|
||||
caused_by: {
|
||||
|
@ -49,10 +39,27 @@ describe('EsError', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
} as IEsError;
|
||||
const esError = new EsError(error, () => {});
|
||||
},
|
||||
() => {},
|
||||
services
|
||||
);
|
||||
|
||||
test('should set error.message to root "error cause" reason', () => {
|
||||
expect(esError.message).toEqual(
|
||||
'EsError: The supplied interval [2q] could not be parsed as a calendar interval.'
|
||||
'The supplied interval [2q] could not be parsed as a calendar interval.'
|
||||
);
|
||||
});
|
||||
|
||||
test('should render error message', () => {
|
||||
const searchErrorDisplay = renderSearchError(esError);
|
||||
expect(searchErrorDisplay).not.toBeUndefined();
|
||||
const wrapper = shallow(searchErrorDisplay?.body as ReactElement);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should return 1 action', () => {
|
||||
const searchErrorDisplay = renderSearchError(esError);
|
||||
expect(searchErrorDisplay).not.toBeUndefined();
|
||||
expect(searchErrorDisplay?.actions?.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,11 +7,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import { IEsError } from './types';
|
||||
import { getRootCause } from './utils';
|
||||
import { EuiButton, EuiCodeBlock } from '@elastic/eui';
|
||||
import type { IEsError } from './types';
|
||||
|
||||
/**
|
||||
* Checks if a given errors originated from Elasticsearch.
|
||||
|
@ -24,39 +22,25 @@ export function isEsError(e: any): e is IEsError {
|
|||
}
|
||||
|
||||
export class EsError extends Error {
|
||||
readonly attributes: IEsError['attributes'];
|
||||
public readonly attributes: IEsError['attributes'];
|
||||
private readonly openInInspector: () => void;
|
||||
|
||||
constructor(protected readonly err: IEsError, private readonly openInInspector: () => void) {
|
||||
super(
|
||||
`EsError: ${
|
||||
getRootCause(err?.attributes?.error)?.reason ||
|
||||
i18n.translate('searchErrors.esError.unknownRootCause', { defaultMessage: 'unknown' })
|
||||
}`
|
||||
);
|
||||
constructor(err: IEsError, message: string, openInInspector: () => void) {
|
||||
super(message);
|
||||
this.attributes = err.attributes;
|
||||
this.openInInspector = openInInspector;
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
|
||||
public getErrorMessage() {
|
||||
if (!this.attributes?.error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootCause = getRootCause(this.attributes.error)?.reason;
|
||||
const topLevelCause = this.attributes.error.reason;
|
||||
const cause = rootCause ?? topLevelCause;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCodeBlock data-test-subj="errMessage" isCopyable={true} paddingSize="s">
|
||||
{cause}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
<EuiCodeBlock data-test-subj="errMessage" isCopyable={true} paddingSize="s">
|
||||
{this.message}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
}
|
||||
|
||||
public getActions(application: ApplicationStart) {
|
||||
public getActions() {
|
||||
return [
|
||||
<EuiButton
|
||||
data-test-subj="viewEsErrorButton"
|
||||
|
|
|
@ -6,91 +6,86 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { createEsError } from './create_es_error';
|
||||
import { renderSearchError } from './render_search_error';
|
||||
import { shallow } from 'enzyme';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
const startMock = coreMock.createStart();
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { PainlessError } from './painless_error';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
const searchPhaseException = {
|
||||
error: {
|
||||
root_cause: [
|
||||
{
|
||||
type: 'script_exception',
|
||||
reason: 'compile error',
|
||||
script_stack: ['invalid', '^---- HERE'],
|
||||
script: 'invalid',
|
||||
lang: 'painless',
|
||||
position: {
|
||||
offset: 0,
|
||||
start: 0,
|
||||
end: 7,
|
||||
},
|
||||
const servicesMock = {
|
||||
application: coreMock.createStart().application,
|
||||
docLinks: {
|
||||
links: {
|
||||
fleet: {
|
||||
datastreamsTSDSMetrics: '',
|
||||
},
|
||||
],
|
||||
type: 'search_phase_execution_exception',
|
||||
reason: 'all shards failed',
|
||||
phase: 'query',
|
||||
grouped: true,
|
||||
failed_shards: [
|
||||
{
|
||||
shard: 0,
|
||||
index: '.kibana_11',
|
||||
node: 'b3HX8C96Q7q1zgfVLxEsPA',
|
||||
reason: {
|
||||
type: 'script_exception',
|
||||
reason: 'compile error',
|
||||
script_stack: ['invalid', '^---- HERE'],
|
||||
script: 'invalid',
|
||||
lang: 'painless',
|
||||
position: {
|
||||
offset: 0,
|
||||
start: 0,
|
||||
end: 7,
|
||||
},
|
||||
caused_by: {
|
||||
type: 'illegal_argument_exception',
|
||||
reason: 'cannot resolve symbol [invalid]',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 400,
|
||||
},
|
||||
} as CoreStart['docLinks'],
|
||||
};
|
||||
|
||||
describe('PainlessError', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const dataViewMock = {
|
||||
title: 'logs',
|
||||
id: '1234',
|
||||
} as unknown as DataView;
|
||||
|
||||
it('Should show reason and code', () => {
|
||||
const e = new PainlessError(
|
||||
{
|
||||
statusCode: 400,
|
||||
message: 'search_phase_execution_exception',
|
||||
attributes: {
|
||||
error: searchPhaseException.error,
|
||||
describe('Painless error', () => {
|
||||
const painlessError = createEsError(
|
||||
{
|
||||
statusCode: 400,
|
||||
message: 'search_phase_execution_exception',
|
||||
attributes: {
|
||||
error: {
|
||||
type: 'search_phase_execution_exception',
|
||||
reason: 'all shards failed',
|
||||
failed_shards: [
|
||||
{
|
||||
shard: 0,
|
||||
index: '.kibana_11',
|
||||
node: 'b3HX8C96Q7q1zgfVLxEsPA',
|
||||
reason: {
|
||||
type: 'script_exception',
|
||||
reason: 'compile error',
|
||||
script_stack: ['invalid', '^---- HERE'],
|
||||
script: 'invalid',
|
||||
lang: 'painless',
|
||||
position: {
|
||||
offset: 0,
|
||||
start: 0,
|
||||
end: 7,
|
||||
},
|
||||
caused_by: {
|
||||
type: 'illegal_argument_exception',
|
||||
reason: 'cannot resolve symbol [invalid]',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
() => {}
|
||||
},
|
||||
() => {},
|
||||
servicesMock,
|
||||
dataViewMock
|
||||
);
|
||||
|
||||
test('should set error.message to painless reason', () => {
|
||||
expect(painlessError.message).toEqual(
|
||||
'Error executing runtime field or scripted field on data view logs'
|
||||
);
|
||||
const component = mount(e.getErrorMessage());
|
||||
});
|
||||
|
||||
const failedShards = searchPhaseException.error.failed_shards![0];
|
||||
test('should render error message', () => {
|
||||
const searchErrorDisplay = renderSearchError(painlessError);
|
||||
expect(searchErrorDisplay).not.toBeUndefined();
|
||||
const wrapper = shallow(searchErrorDisplay?.body as ReactElement);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
const stackTraceElem = findTestSubject(component, 'painlessStackTrace').getDOMNode();
|
||||
const stackTrace = failedShards!.reason.script_stack!.splice(-2).join('\n');
|
||||
expect(stackTraceElem.textContent).toBe(stackTrace);
|
||||
|
||||
const humanReadableError = findTestSubject(
|
||||
component,
|
||||
'painlessHumanReadableError'
|
||||
).getDOMNode();
|
||||
expect(humanReadableError.textContent).toBe(failedShards?.reason.caused_by?.reason);
|
||||
|
||||
const actions = e.getActions(startMock.application);
|
||||
expect(actions.length).toBe(2);
|
||||
test('should return 2 actions', () => {
|
||||
const searchErrorDisplay = renderSearchError(painlessError);
|
||||
expect(searchErrorDisplay).not.toBeUndefined();
|
||||
expect(searchErrorDisplay?.actions?.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,43 +7,58 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonEmpty, EuiSpacer, EuiText, EuiCodeBlock } from '@elastic/eui';
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { IEsError } from './types';
|
||||
import { EsError, isEsError } from './es_error';
|
||||
import { getRootCause } from './utils';
|
||||
import { EsError } from './es_error';
|
||||
|
||||
export class PainlessError extends EsError {
|
||||
painlessStack?: string;
|
||||
indexPattern?: DataView;
|
||||
constructor(err: IEsError, openInInspector: () => void, indexPattern?: DataView) {
|
||||
super(err, openInInspector);
|
||||
this.indexPattern = indexPattern;
|
||||
private readonly applicationStart: ApplicationStart;
|
||||
private readonly painlessCause: estypes.ErrorCause;
|
||||
private readonly dataView?: DataView;
|
||||
|
||||
constructor(
|
||||
err: IEsError,
|
||||
openInInspector: () => void,
|
||||
painlessCause: estypes.ErrorCause,
|
||||
applicationStart: ApplicationStart,
|
||||
dataView?: DataView
|
||||
) {
|
||||
super(
|
||||
err,
|
||||
i18n.translate('searchErrors.painlessError.painlessScriptedFieldErrorMessage', {
|
||||
defaultMessage:
|
||||
'Error executing runtime field or scripted field on data view {indexPatternName}',
|
||||
values: {
|
||||
indexPatternName: dataView?.title || '',
|
||||
},
|
||||
}),
|
||||
openInInspector
|
||||
);
|
||||
this.applicationStart = applicationStart;
|
||||
this.painlessCause = painlessCause;
|
||||
this.dataView = dataView;
|
||||
}
|
||||
|
||||
public getErrorMessage() {
|
||||
const rootCause = getRootCause(this.err.attributes?.error);
|
||||
const scriptFromStackTrace = rootCause?.script_stack
|
||||
? rootCause?.script_stack?.slice(-2).join('\n')
|
||||
const scriptFromStackTrace = this.painlessCause?.script_stack
|
||||
? this.painlessCause?.script_stack?.slice(-2).join('\n')
|
||||
: undefined;
|
||||
// if the error has been properly processed it will highlight where it occurred.
|
||||
const hasScript = rootCause?.script_stack?.slice(-1)[0]?.indexOf('HERE') || -1 >= 0;
|
||||
const humanReadableError = rootCause?.caused_by?.reason;
|
||||
const hasScript = this.painlessCause?.script_stack?.slice(-1)[0]?.indexOf('HERE') || -1 >= 0;
|
||||
const humanReadableError = this.painlessCause?.caused_by?.reason;
|
||||
// fallback, show ES stacktrace
|
||||
const painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined;
|
||||
const painlessStack = this.painlessCause?.script_stack
|
||||
? this.painlessCause?.script_stack.join('\n')
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<EuiText size="s" data-test-subj="painlessScript">
|
||||
{i18n.translate('searchErrors.painlessError.painlessScriptedFieldErrorMessage', {
|
||||
defaultMessage:
|
||||
'Error executing runtime field or scripted field on index pattern {indexPatternName}',
|
||||
values: {
|
||||
indexPatternName: this?.indexPattern?.title,
|
||||
},
|
||||
})}
|
||||
{this.message}
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
{scriptFromStackTrace || painlessStack ? (
|
||||
|
@ -56,21 +71,21 @@ export class PainlessError extends EsError {
|
|||
{humanReadableError}
|
||||
</EuiText>
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getActions(application: ApplicationStart) {
|
||||
function onClick(indexPatternId?: string) {
|
||||
application.navigateToApp('management', {
|
||||
path: `/kibana/indexPatterns${indexPatternId ? `/patterns/${indexPatternId}` : ''}`,
|
||||
});
|
||||
}
|
||||
const actions = super.getActions(application) ?? [];
|
||||
getActions() {
|
||||
const actions = super.getActions() ?? [];
|
||||
actions.push(
|
||||
<EuiButtonEmpty
|
||||
key="editPainlessScript"
|
||||
onClick={() => onClick(this?.indexPattern?.id)}
|
||||
onClick={() => () => {
|
||||
const dataViewId = this.dataView?.id;
|
||||
this.applicationStart.navigateToApp('management', {
|
||||
path: `/kibana/indexPatterns${dataViewId ? `/patterns/${dataViewId}` : ''}`,
|
||||
});
|
||||
}}
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate('searchErrors.painlessError.buttonTxt', {
|
||||
|
@ -81,13 +96,3 @@ export class PainlessError extends EsError {
|
|||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
export function isPainlessError(err: Error | IEsError) {
|
||||
if (!isEsError(err)) return false;
|
||||
|
||||
const rootCause = getRootCause((err as IEsError).attributes?.error);
|
||||
if (!rootCause) return false;
|
||||
|
||||
const { lang } = rootCause;
|
||||
return lang === 'painless';
|
||||
}
|
||||
|
|
|
@ -9,23 +9,18 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ReactNode } from 'react';
|
||||
import { BfetchRequestError } from '@kbn/bfetch-error';
|
||||
import type { ApplicationStart } from '@kbn/core-application-browser';
|
||||
import { EsError } from './es_error';
|
||||
|
||||
export function renderSearchError({
|
||||
error,
|
||||
application,
|
||||
}: {
|
||||
error: Error;
|
||||
application: ApplicationStart;
|
||||
}): { title: string; body: ReactNode; actions?: ReactNode[] } | undefined {
|
||||
export function renderSearchError(
|
||||
error: Error
|
||||
): { title: string; body: ReactNode; actions?: ReactNode[] } | undefined {
|
||||
if (error instanceof EsError) {
|
||||
return {
|
||||
title: i18n.translate('searchErrors.search.esErrorTitle', {
|
||||
defaultMessage: 'Cannot retrieve search results',
|
||||
}),
|
||||
body: error.getErrorMessage(),
|
||||
actions: error.getActions(application),
|
||||
actions: error.getActions(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
88
packages/kbn-search-errors/src/tsdb_error.test.tsx
Normal file
88
packages/kbn-search-errors/src/tsdb_error.test.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 type { ReactElement } from 'react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { createEsError } from './create_es_error';
|
||||
import { renderSearchError } from './render_search_error';
|
||||
import { shallow } from 'enzyme';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
||||
const servicesMock = {
|
||||
application: coreMock.createStart().application,
|
||||
docLinks: {
|
||||
links: {
|
||||
fleet: {
|
||||
datastreamsTSDSMetrics: '',
|
||||
},
|
||||
},
|
||||
} as CoreStart['docLinks'],
|
||||
};
|
||||
|
||||
describe('Tsdb error', () => {
|
||||
const tsdbError = createEsError(
|
||||
{
|
||||
statusCode: 400,
|
||||
message: 'search_phase_execution_exception',
|
||||
attributes: {
|
||||
error: {
|
||||
type: 'status_exception',
|
||||
reason: 'error while executing search',
|
||||
caused_by: {
|
||||
type: 'search_phase_execution_exception',
|
||||
reason: 'all shards failed',
|
||||
phase: 'query',
|
||||
grouped: true,
|
||||
failed_shards: [
|
||||
{
|
||||
shard: 0,
|
||||
index: 'tsdb_index',
|
||||
reason: {
|
||||
type: 'illegal_argument_exception',
|
||||
reason:
|
||||
'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]',
|
||||
},
|
||||
},
|
||||
],
|
||||
caused_by: {
|
||||
type: 'illegal_argument_exception',
|
||||
reason:
|
||||
'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]',
|
||||
caused_by: {
|
||||
type: 'illegal_argument_exception',
|
||||
reason:
|
||||
'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {},
|
||||
servicesMock
|
||||
);
|
||||
|
||||
test('should set error.message to tsdb reason', () => {
|
||||
expect(tsdbError.message).toEqual(
|
||||
'The field [bytes_counter] of Time series type [counter] has been used with the unsupported operation [sum].'
|
||||
);
|
||||
});
|
||||
|
||||
test('should render error message', () => {
|
||||
const searchErrorDisplay = renderSearchError(tsdbError);
|
||||
expect(searchErrorDisplay).not.toBeUndefined();
|
||||
const wrapper = shallow(searchErrorDisplay?.body as ReactElement);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should return 1 actions', () => {
|
||||
const searchErrorDisplay = renderSearchError(tsdbError);
|
||||
expect(searchErrorDisplay).not.toBeUndefined();
|
||||
expect(searchErrorDisplay?.actions?.length).toBe(1);
|
||||
});
|
||||
});
|
55
packages/kbn-search-errors/src/tsdb_error.tsx
Normal file
55
packages/kbn-search-errors/src/tsdb_error.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import type { IEsError } from './types';
|
||||
import { EsError } from './es_error';
|
||||
|
||||
export class TsdbError extends EsError {
|
||||
private readonly docLinks: CoreStart['docLinks'];
|
||||
|
||||
constructor(
|
||||
err: IEsError,
|
||||
openInInspector: () => void,
|
||||
tsdbCause: estypes.ErrorCause,
|
||||
docLinks: CoreStart['docLinks']
|
||||
) {
|
||||
const [fieldName, _type, _isCounter, opUsed] = tsdbCause.reason!.match(/\[(\w)*\]/g)!;
|
||||
super(
|
||||
err,
|
||||
i18n.translate('searchErrors.tsdbError.message', {
|
||||
defaultMessage:
|
||||
'The field {field} of Time series type [counter] has been used with the unsupported operation {op}.',
|
||||
values: {
|
||||
field: fieldName,
|
||||
op: opUsed,
|
||||
},
|
||||
}),
|
||||
openInInspector
|
||||
);
|
||||
this.docLinks = docLinks;
|
||||
}
|
||||
|
||||
public getErrorMessage() {
|
||||
return (
|
||||
<div>
|
||||
<p className="eui-textBreakWord">{this.message}</p>
|
||||
<EuiLink href={this.docLinks.links.fleet.datastreamsTSDSMetrics} external target="_blank">
|
||||
{i18n.translate('searchErrors.tsdbError.tsdbCounterDocsLabel', {
|
||||
defaultMessage:
|
||||
'See more about Time series field types and [counter] supported aggregations',
|
||||
})}
|
||||
</EuiLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,25 +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.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
|
||||
function getFailedShardCause(error: estypes.ErrorCause): estypes.ErrorCause | undefined {
|
||||
const failedShards = error.failed_shards || error.caused_by?.failed_shards;
|
||||
return failedShards ? failedShards[0]?.reason : undefined;
|
||||
}
|
||||
|
||||
function getNestedCause(error: estypes.ErrorCause): estypes.ErrorCause {
|
||||
return error.caused_by ? getNestedCause(error.caused_by) : error;
|
||||
}
|
||||
|
||||
export function getRootCause(error?: estypes.ErrorCause): estypes.ErrorCause | undefined {
|
||||
return error
|
||||
? // Give shard failures priority, then try to get the error navigating nested objects
|
||||
getFailedShardCause(error) || getNestedCause(error)
|
||||
: undefined;
|
||||
}
|
|
@ -20,7 +20,6 @@
|
|||
"@kbn/core",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/core-application-browser",
|
||||
"@kbn/bfetch-error",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue