Skip to content

Commit 9a76d34

Browse files
[Alerting] Show more user friendly ES error message when executor fails (#96254)
* WIP for ES error parser * Fix tests * Ensure the error shows up in the UI too * wip * Handle multiple types here * Fix tests * PR feedback Co-authored-by: Kibana Machine <[email protected]>
1 parent 8f2f65d commit 9a76d34

File tree

9 files changed

+196
-9
lines changed

9 files changed

+196
-9
lines changed

x-pack/plugins/alerting/server/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export { PluginSetupContract, PluginStartContract } from './plugin';
3333
export { FindResult } from './alerts_client';
3434
export { PublicAlertInstance as AlertInstance } from './alert_instance';
3535
export { parseDuration } from './lib';
36+
export { getEsErrorMessage } from './lib/errors';
3637

3738
export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext);
3839

x-pack/plugins/alerting/server/lib/alert_execution_status.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { Logger } from 'src/core/server';
99
import { AlertTaskState, AlertExecutionStatus, RawAlertExecutionStatus } from '../types';
1010
import { getReasonFromError } from './error_with_reason';
11+
import { getEsErrorMessage } from './errors';
1112

1213
export function executionStatusFromState(state: AlertTaskState): AlertExecutionStatus {
1314
const instanceIds = Object.keys(state.alertInstances ?? {});
@@ -23,7 +24,7 @@ export function executionStatusFromError(error: Error): AlertExecutionStatus {
2324
status: 'error',
2425
error: {
2526
reason: getReasonFromError(error),
26-
message: error.message,
27+
message: getEsErrorMessage(error),
2728
},
2829
};
2930
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 { getEsErrorMessage } from './es_error_parser';
9+
10+
describe('ES error parser', () => {
11+
test('should return all the cause of the error', () => {
12+
expect(
13+
getEsErrorMessage({
14+
name: '',
15+
message: '',
16+
error: {
17+
meta: {
18+
body: {
19+
error: {
20+
caused_by: {
21+
reason: 'reason1',
22+
},
23+
},
24+
},
25+
},
26+
},
27+
})
28+
).toStrictEqual(', caused by: "reason1"');
29+
30+
expect(
31+
getEsErrorMessage({
32+
name: '',
33+
message: '',
34+
meta: {
35+
body: {
36+
error: {
37+
caused_by: {
38+
reason: 'reason2',
39+
},
40+
},
41+
},
42+
},
43+
})
44+
).toStrictEqual(', caused by: "reason2"');
45+
46+
expect(
47+
getEsErrorMessage({
48+
name: '',
49+
message: '',
50+
meta: {
51+
body: {
52+
error: {
53+
caused_by: {
54+
reason: 'reason3',
55+
caused_by: {
56+
reason: 'reason4',
57+
},
58+
},
59+
},
60+
},
61+
},
62+
})
63+
).toStrictEqual(', caused by: "reason3,reason4"');
64+
65+
expect(
66+
getEsErrorMessage({
67+
name: '',
68+
message: '',
69+
meta: {
70+
body: {
71+
error: {
72+
failed_shards: [
73+
{
74+
reason: {
75+
caused_by: {
76+
reason: 'reason4',
77+
},
78+
},
79+
},
80+
],
81+
},
82+
},
83+
},
84+
})
85+
).toStrictEqual(', caused by: "reason4"');
86+
87+
expect(
88+
getEsErrorMessage({
89+
name: '',
90+
message: '',
91+
meta: {
92+
body: {
93+
error: {
94+
failed_shards: [
95+
{
96+
reason: {
97+
caused_by: {
98+
reason: 'reason5',
99+
caused_by: {
100+
reason: 'reason6',
101+
},
102+
},
103+
},
104+
},
105+
],
106+
},
107+
},
108+
},
109+
})
110+
).toStrictEqual(', caused by: "reason5,reason6"');
111+
});
112+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 { ResponseError } from '@elastic/elasticsearch/lib/errors';
9+
import { ElasticsearchError, ElasticsearchErrorCausedByObject } from './types';
10+
11+
const getEsCause = (
12+
obj: ElasticsearchErrorCausedByObject = {},
13+
causes: string[] = []
14+
): string[] => {
15+
const updated = [...causes];
16+
17+
if (obj.caused_by) {
18+
if (obj.caused_by?.reason) {
19+
updated.push(obj.caused_by?.reason);
20+
}
21+
22+
// Recursively find all the "caused by" reasons
23+
return getEsCause(obj.caused_by, updated);
24+
}
25+
26+
if (obj.failed_shards && obj.failed_shards.length) {
27+
for (const failure of obj.failed_shards) {
28+
if (failure && failure.reason) {
29+
updated.push(...getEsCause(failure.reason));
30+
}
31+
}
32+
}
33+
34+
return updated.filter(Boolean);
35+
};
36+
37+
export const getEsErrorMessage = (error: ElasticsearchError) => {
38+
let message = error?.message;
39+
const apiError = error?.error?.meta?.body?.error ?? error?.meta?.body?.error;
40+
if (apiError) {
41+
message += `, caused by: "${getEsCause(apiError as ElasticsearchErrorCausedByObject)}"`;
42+
}
43+
return message;
44+
};

x-pack/plugins/alerting/server/lib/errors/index.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
* 2.0.
66
*/
77

8-
import { ErrorThatHandlesItsOwnResponse } from './types';
8+
import { ErrorThatHandlesItsOwnResponse, ElasticsearchError } from './types';
9+
import { getEsErrorMessage } from './es_error_parser';
910

1011
export function isErrorThatHandlesItsOwnResponse(
1112
e: ErrorThatHandlesItsOwnResponse
1213
): e is ErrorThatHandlesItsOwnResponse {
1314
return typeof (e as ErrorThatHandlesItsOwnResponse).sendResponse === 'function';
1415
}
1516

16-
export { ErrorThatHandlesItsOwnResponse };
17+
export { ErrorThatHandlesItsOwnResponse, ElasticsearchError, getEsErrorMessage };
1718
export { AlertTypeDisabledError, AlertTypeDisabledReason } from './alert_type_disabled';

x-pack/plugins/alerting/server/lib/errors/types.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,31 @@
44
* 2.0; you may not use this file except in compliance with the Elastic License
55
* 2.0.
66
*/
7-
87
import { KibanaResponseFactory, IKibanaResponse } from '../../../../../../src/core/server';
98

109
export interface ErrorThatHandlesItsOwnResponse extends Error {
1110
sendResponse(res: KibanaResponseFactory): IKibanaResponse;
1211
}
12+
13+
export interface ElasticsearchErrorCausedByObject {
14+
reason?: string;
15+
caused_by?: ElasticsearchErrorCausedByObject;
16+
failed_shards?: Array<{
17+
reason?: {
18+
caused_by?: ElasticsearchErrorCausedByObject;
19+
};
20+
}>;
21+
}
22+
23+
interface ElasticsearchErrorMeta {
24+
body?: {
25+
error?: ElasticsearchErrorCausedByObject;
26+
};
27+
}
28+
29+
export interface ElasticsearchError extends Error {
30+
error?: {
31+
meta?: ElasticsearchErrorMeta;
32+
};
33+
meta?: ElasticsearchErrorMeta;
34+
}

x-pack/plugins/alerting/server/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {
1616
AlertTypeDisabledReason,
1717
ErrorThatHandlesItsOwnResponse,
1818
isErrorThatHandlesItsOwnResponse,
19+
ElasticsearchError,
1920
} from './errors';
2021
export {
2122
executionStatusFromState,

x-pack/plugins/alerting/server/task_runner/task_runner.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
executionStatusFromError,
2121
alertExecutionStatusToRaw,
2222
ErrorWithReason,
23+
ElasticsearchError,
2324
} from '../lib';
2425
import {
2526
RawAlert,
@@ -49,6 +50,7 @@ import {
4950
WithoutReservedActionGroups,
5051
} from '../../common';
5152
import { NormalizedAlertType } from '../alert_type_registry';
53+
import { getEsErrorMessage } from '../lib/errors';
5254

5355
const FALLBACK_RETRY_INTERVAL = '5m';
5456

@@ -480,7 +482,7 @@ export class TaskRunner<
480482
const executionStatus: AlertExecutionStatus = map(
481483
state,
482484
(alertTaskState: AlertTaskState) => executionStatusFromState(alertTaskState),
483-
(err: Error) => executionStatusFromError(err)
485+
(err: ElasticsearchError) => executionStatusFromError(err)
484486
);
485487

486488
// set the executionStatus date to same as event, if it's set
@@ -530,16 +532,18 @@ export class TaskRunner<
530532
}
531533

532534
return {
533-
state: map<AlertTaskState, Error, AlertTaskState>(
535+
state: map<AlertTaskState, ElasticsearchError, AlertTaskState>(
534536
state,
535537
(stateUpdates: AlertTaskState) => {
536538
return {
537539
...stateUpdates,
538540
previousStartedAt: startedAt,
539541
};
540542
},
541-
(err: Error) => {
542-
const message = `Executing Alert "${alertId}" has resulted in Error: ${err.message}`;
543+
(err: ElasticsearchError) => {
544+
const message = `Executing Alert "${alertId}" has resulted in Error: ${getEsErrorMessage(
545+
err
546+
)}`;
543547
if (isAlertSavedObjectNotFoundError(err, alertId)) {
544548
this.logger.debug(message);
545549
} else {

x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import type { estypes } from '@elastic/elasticsearch';
99
import { Logger, ElasticsearchClient } from 'kibana/server';
10+
import { getEsErrorMessage } from '../../../../alerting/server';
1011
import { DEFAULT_GROUPS } from '../index';
1112
import { getDateRangeInfo } from './date_range_info';
1213

@@ -137,7 +138,7 @@ export async function timeSeriesQuery(
137138
esResult = (await esClient.search(esQuery, { ignore: [404] })).body;
138139
} catch (err) {
139140
// console.log('time_series_query.ts error\n', JSON.stringify(err, null, 4));
140-
logger.warn(`${logPrefix} error: ${err.message}`);
141+
logger.warn(`${logPrefix} error: ${getEsErrorMessage(err)}`);
141142
return { results: [] };
142143
}
143144

0 commit comments

Comments
 (0)