From 8a80259b0ed9ceb89c361d4f78d05be1831a9a41 Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Wed, 16 Jul 2025 03:55:55 +0200 Subject: [PATCH 1/8] Handle is_partial in ES|QL query responses in Elasticsearch query rule --- .../common/es_query/esql_query_utils.ts | 1 + .../es_query/lib/fetch_esql_query.test.ts | 218 ++++++++++++++++++ .../es_query/lib/fetch_esql_query.ts | 20 ++ 3 files changed, 239 insertions(+) diff --git a/x-pack/platform/plugins/shared/stack_alerts/common/es_query/esql_query_utils.ts b/x-pack/platform/plugins/shared/stack_alerts/common/es_query/esql_query_utils.ts index daea91c06673f..a8f9f0c74d075 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/common/es_query/esql_query_utils.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/common/es_query/esql_query_utils.ts @@ -48,6 +48,7 @@ const ESQL_DOCUMENT_ID = 'esql_query_document'; export interface EsqlTable { columns: EsqlResultColumn[]; values: EsqlResultRow[]; + is_partial?: boolean; } export const ALERT_ID_COLUMN = 'Alert ID'; diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts index c16087100151b..e2e02c4af7471 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts @@ -62,6 +62,10 @@ describe('fetchEsqlQuery', () => { global.Date.now = jest.fn(() => fakeNow.getTime()); }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('fetch', () => { it('should throw a user error when the error is a verification_exception error', async () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -100,6 +104,219 @@ describe('fetchEsqlQuery', () => { expect(getErrorSource(e)).toBe(TaskErrorSource.USER); } }); + + it('should throw an error when is_partial is true but no results are returned and (Alert if matches are found) is selected', async () => { + const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ + columns: [], // no results + values: [], + is_partial: true, // is_partial is true + }); + + (getEsqlQueryHits as jest.Mock).mockReturnValue({ + results: { + esResult: { + _shards: { failed: 0, successful: 0, total: 0 }, + aggregations: {}, + hits: { hits: [] }, + timed_out: false, + took: 0, + }, + isCountAgg: false, + isGroupAgg: false, + }, + }); + + await expect( + fetchEsqlQuery({ + ruleId: 'testRuleId', + alertLimit: 1, + params: defaultParams, + services: { + logger, + scopedClusterClient, + // @ts-expect-error + share: { + url: { + locators: { + get: jest.fn().mockReturnValue({ + getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'), + } as unknown as LocatorPublic), + }, + }, + } as SharePluginStart, + ruleResultService: mockRuleResultService, + }, + spacePrefix: '', + dateStart: new Date().toISOString(), + dateEnd: new Date().toISOString(), + }) + ).rejects.toThrow( + 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.' + ); + + expect(mockRuleResultService.addLastRunWarning).not.toHaveBeenCalled(); + expect(mockRuleResultService.setLastRunOutcomeMessage).not.toHaveBeenCalled(); + }); + + it('should add a warning when is_partial is true and (Alert per row) is selected', async () => { + const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ + columns: [], + values: [], + is_partial: true, // is_partial is true + }); + + (getEsqlQueryHits as jest.Mock).mockReturnValue({ + results: { + esResult: { + _shards: { failed: 0, successful: 0, total: 0 }, + aggregations: {}, + hits: { hits: [] }, + timed_out: false, + took: 0, + }, + isCountAgg: false, + isGroupAgg: true, + }, + }); + + await fetchEsqlQuery({ + ruleId: 'testRuleId', + alertLimit: 1, + params: { ...defaultParams, groupBy: 'row' }, + services: { + logger, + scopedClusterClient, + // @ts-expect-error + share: { + url: { + locators: { + get: jest.fn().mockReturnValue({ + getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'), + } as unknown as LocatorPublic), + }, + }, + } as SharePluginStart, + ruleResultService: mockRuleResultService, + }, + spacePrefix: '', + dateStart: new Date().toISOString(), + dateEnd: new Date().toISOString(), + }); + + const warning = + 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.'; + expect(mockRuleResultService.addLastRunWarning).toHaveBeenCalledWith(warning); + expect(mockRuleResultService.setLastRunOutcomeMessage).toHaveBeenCalledWith(warning); + }); + + it('should add a warning when is_partial is true, has some results and (Alert if matches are found) is selected', async () => { + const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ + columns: [], + values: [], + is_partial: true, // is_partial is true + }); + + (getEsqlQueryHits as jest.Mock).mockReturnValue({ + results: { + esResult: { + _shards: { failed: 0, successful: 0, total: 0 }, + aggregations: {}, + hits: { hits: [{ foo: 'bar' }] }, // has data + timed_out: false, + took: 0, + }, + isCountAgg: false, + isGroupAgg: true, + }, + }); + + await fetchEsqlQuery({ + ruleId: 'testRuleId', + alertLimit: 1, + params: defaultParams, + services: { + logger, + scopedClusterClient, + // @ts-expect-error + share: { + url: { + locators: { + get: jest.fn().mockReturnValue({ + getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'), + } as unknown as LocatorPublic), + }, + }, + } as SharePluginStart, + ruleResultService: mockRuleResultService, + }, + spacePrefix: '', + dateStart: new Date().toISOString(), + dateEnd: new Date().toISOString(), + }); + + const warning = + 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.'; + expect(mockRuleResultService.addLastRunWarning).toHaveBeenCalledWith(warning); + expect(mockRuleResultService.setLastRunOutcomeMessage).toHaveBeenCalledWith(warning); + }); + + it('should not add a warning or throw when is_partial is false', async () => { + const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ + columns: [], + values: [], + is_partial: false, // is_partial is true + }); + + (getEsqlQueryHits as jest.Mock).mockReturnValue({ + results: { + esResult: { + _shards: { failed: 0, successful: 0, total: 0 }, + aggregations: {}, + hits: { hits: [{ foo: 'bar' }] }, // has data + timed_out: false, + took: 0, + }, + isCountAgg: false, + isGroupAgg: true, + }, + }); + + const result = await fetchEsqlQuery({ + ruleId: 'testRuleId', + alertLimit: 1, + params: defaultParams, + services: { + logger, + scopedClusterClient, + // @ts-expect-error + share: { + url: { + locators: { + get: jest.fn().mockReturnValue({ + getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'), + } as unknown as LocatorPublic), + }, + }, + } as SharePluginStart, + ruleResultService: mockRuleResultService, + }, + spacePrefix: '', + dateStart: new Date().toISOString(), + dateEnd: new Date().toISOString(), + }); + + expect(result).toEqual({ + index: null, + link: '/app/r?l=DISCOVER_APP_LOCATOR', + parsedResults: { results: [], truncated: false }, + }); + expect(mockRuleResultService.addLastRunWarning).not.toHaveBeenCalled(); + expect(mockRuleResultService.setLastRunOutcomeMessage).not.toHaveBeenCalled(); + }); }); describe('getEsqlQuery', () => { @@ -289,6 +506,7 @@ describe('fetchEsqlQuery', () => { 'The query returned multiple rows with the same alert ID. There are duplicate results for alert IDs: 1.2.0' ); }); + describe('generateLink', () => { it('should generate a link', () => { const { dateStart, dateEnd } = getTimeRange(); diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts index cfbac7bb7c313..3c2bfbb332f5c 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts @@ -82,6 +82,26 @@ export async function fetchEsqlQuery({ ruleResultService.setLastRunOutcomeMessage(warning); } + const isPartial = response.is_partial ?? false; + + if (ruleResultService && isPartial) { + const warning = `The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.`; + const hasResults = results.esResult?.hits?.hits?.length > 0; + + if (!isGroupAgg && !hasResults) { + // If this is not a grouped query (Alert per row) and there are no results, we fail the run + throw createTaskRunError(new Error(warning), TaskErrorSource.FRAMEWORK); + } + + if (isGroupAgg || (!isGroupAgg && hasResults)) { + // For grouped queries (Alert per row) no matter if there are results or not. + // And the non-grouped queries (Alert if matches are found) if there are results, + // we show a warning instead of throwing an error. + ruleResultService.addLastRunWarning(warning); + ruleResultService.setLastRunOutcomeMessage(warning); + } + } + const link = generateLink(params, discoverLocator, dateStart, dateEnd, spacePrefix); return { From ffd27321cb68d9b479ba0b8d81f3dcfa212d096c Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Thu, 17 Jul 2025 17:09:54 +0200 Subject: [PATCH 2/8] use i18 --- .../server/rule_types/es_query/lib/fetch_esql_query.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts index 3c2bfbb332f5c..a9e73d1abcb45 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts @@ -20,6 +20,7 @@ import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import type { EsqlTable } from '../../../../common'; import { getEsqlQueryHits } from '../../../../common'; import type { OnlyEsqlQueryRuleParams } from '../types'; +import { i18n } from '@kbn/i18n'; export interface FetchEsqlQueryOpts { ruleId: string; @@ -85,7 +86,10 @@ export async function fetchEsqlQuery({ const isPartial = response.is_partial ?? false; if (ruleResultService && isPartial) { - const warning = `The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.`; + const warning = i18n.translate('xpack.stackAlerts.esQuery.isPartialWarning', { + defaultMessage: + 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.', + }); const hasResults = results.esResult?.hits?.hits?.length > 0; if (!isGroupAgg && !hasResults) { From abd382baef9baaf071ee351f07009eb2d21f8d40 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Jul 2025 15:37:27 +0000 Subject: [PATCH 3/8] [CI] Auto-commit changed files from 'node scripts/eslint_all_files --no-cache --fix' --- .../server/rule_types/es_query/lib/fetch_esql_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts index a9e73d1abcb45..1241e2101afdd 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts @@ -17,10 +17,10 @@ import { ecsFieldMap, alertFieldMap } from '@kbn/alerts-as-data-utils'; import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server'; import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; +import { i18n } from '@kbn/i18n'; import type { EsqlTable } from '../../../../common'; import { getEsqlQueryHits } from '../../../../common'; import type { OnlyEsqlQueryRuleParams } from '../types'; -import { i18n } from '@kbn/i18n'; export interface FetchEsqlQueryOpts { ruleId: string; From 83df48f44033bd35831da3a31b1d8d2b075aa5cd Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Fri, 18 Jul 2025 20:55:24 +0200 Subject: [PATCH 4/8] Remove throwing error --- .../es_query/lib/fetch_esql_query.test.ts | 108 +----------------- .../es_query/lib/fetch_esql_query.ts | 16 +-- 2 files changed, 3 insertions(+), 121 deletions(-) diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts index e2e02c4af7471..fa76d13bf7f37 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts @@ -105,61 +105,7 @@ describe('fetchEsqlQuery', () => { } }); - it('should throw an error when is_partial is true but no results are returned and (Alert if matches are found) is selected', async () => { - const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ - columns: [], // no results - values: [], - is_partial: true, // is_partial is true - }); - - (getEsqlQueryHits as jest.Mock).mockReturnValue({ - results: { - esResult: { - _shards: { failed: 0, successful: 0, total: 0 }, - aggregations: {}, - hits: { hits: [] }, - timed_out: false, - took: 0, - }, - isCountAgg: false, - isGroupAgg: false, - }, - }); - - await expect( - fetchEsqlQuery({ - ruleId: 'testRuleId', - alertLimit: 1, - params: defaultParams, - services: { - logger, - scopedClusterClient, - // @ts-expect-error - share: { - url: { - locators: { - get: jest.fn().mockReturnValue({ - getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'), - } as unknown as LocatorPublic), - }, - }, - } as SharePluginStart, - ruleResultService: mockRuleResultService, - }, - spacePrefix: '', - dateStart: new Date().toISOString(), - dateEnd: new Date().toISOString(), - }) - ).rejects.toThrow( - 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.' - ); - - expect(mockRuleResultService.addLastRunWarning).not.toHaveBeenCalled(); - expect(mockRuleResultService.setLastRunOutcomeMessage).not.toHaveBeenCalled(); - }); - - it('should add a warning when is_partial is true and (Alert per row) is selected', async () => { + it('should add a warning when is_partial is true', async () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ columns: [], @@ -211,58 +157,6 @@ describe('fetchEsqlQuery', () => { expect(mockRuleResultService.setLastRunOutcomeMessage).toHaveBeenCalledWith(warning); }); - it('should add a warning when is_partial is true, has some results and (Alert if matches are found) is selected', async () => { - const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ - columns: [], - values: [], - is_partial: true, // is_partial is true - }); - - (getEsqlQueryHits as jest.Mock).mockReturnValue({ - results: { - esResult: { - _shards: { failed: 0, successful: 0, total: 0 }, - aggregations: {}, - hits: { hits: [{ foo: 'bar' }] }, // has data - timed_out: false, - took: 0, - }, - isCountAgg: false, - isGroupAgg: true, - }, - }); - - await fetchEsqlQuery({ - ruleId: 'testRuleId', - alertLimit: 1, - params: defaultParams, - services: { - logger, - scopedClusterClient, - // @ts-expect-error - share: { - url: { - locators: { - get: jest.fn().mockReturnValue({ - getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'), - } as unknown as LocatorPublic), - }, - }, - } as SharePluginStart, - ruleResultService: mockRuleResultService, - }, - spacePrefix: '', - dateStart: new Date().toISOString(), - dateEnd: new Date().toISOString(), - }); - - const warning = - 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.'; - expect(mockRuleResultService.addLastRunWarning).toHaveBeenCalledWith(warning); - expect(mockRuleResultService.setLastRunOutcomeMessage).toHaveBeenCalledWith(warning); - }); - it('should not add a warning or throw when is_partial is false', async () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts index a9e73d1abcb45..349e2908fda91 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts @@ -90,20 +90,8 @@ export async function fetchEsqlQuery({ defaultMessage: 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.', }); - const hasResults = results.esResult?.hits?.hits?.length > 0; - - if (!isGroupAgg && !hasResults) { - // If this is not a grouped query (Alert per row) and there are no results, we fail the run - throw createTaskRunError(new Error(warning), TaskErrorSource.FRAMEWORK); - } - - if (isGroupAgg || (!isGroupAgg && hasResults)) { - // For grouped queries (Alert per row) no matter if there are results or not. - // And the non-grouped queries (Alert if matches are found) if there are results, - // we show a warning instead of throwing an error. - ruleResultService.addLastRunWarning(warning); - ruleResultService.setLastRunOutcomeMessage(warning); - } + ruleResultService.addLastRunWarning(warning); + ruleResultService.setLastRunOutcomeMessage(warning); } const link = generateLink(params, discoverLocator, dateStart, dateEnd, spacePrefix); From d2c3d5e9a5b46e3324e971297adf874fe6aa2f2d Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Fri, 18 Jul 2025 20:56:33 +0200 Subject: [PATCH 5/8] fix test name --- .../server/rule_types/es_query/lib/fetch_esql_query.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts index fa76d13bf7f37..e80ea50fccbe3 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts @@ -157,7 +157,7 @@ describe('fetchEsqlQuery', () => { expect(mockRuleResultService.setLastRunOutcomeMessage).toHaveBeenCalledWith(warning); }); - it('should not add a warning or throw when is_partial is false', async () => { + it('should not add a warning when is_partial is false', async () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ columns: [], From 93e48c8e9675728af1a34417b1bd40e44da42500 Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Mon, 21 Jul 2025 18:53:15 +0200 Subject: [PATCH 6/8] Improve the warning message --- .../common/es_query/esql_query_utils.ts | 8 ++++++ .../es_query/lib/fetch_esql_query.test.ts | 16 +++++++++++- .../es_query/lib/fetch_esql_query.ts | 25 ++++++++++++++++--- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/x-pack/platform/plugins/shared/stack_alerts/common/es_query/esql_query_utils.ts b/x-pack/platform/plugins/shared/stack_alerts/common/es_query/esql_query_utils.ts index a8f9f0c74d075..e4ed92cd1a262 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/common/es_query/esql_query_utils.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/common/es_query/esql_query_utils.ts @@ -17,6 +17,7 @@ import { isFunctionExpression, } from '@kbn/esql-ast'; import { getArgsFromRenameFunction } from '@kbn/esql-utils'; +import type { EsqlEsqlShardFailure } from '@elastic/elasticsearch/lib/api/types'; import { ActionGroupId } from './constants'; type EsqlDocument = Record; @@ -49,6 +50,13 @@ export interface EsqlTable { columns: EsqlResultColumn[]; values: EsqlResultRow[]; is_partial?: boolean; + _clusters?: { + details?: { + [key: string]: { + failures?: EsqlEsqlShardFailure[]; + }; + }; + }; } export const ALERT_ID_COLUMN = 'Alert ID'; diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts index e80ea50fccbe3..36be504641ded 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts @@ -16,6 +16,7 @@ import { publicRuleResultServiceMock } from '@kbn/alerting-plugin/server/monitor import { getEsqlQueryHits } from '../../../../common'; import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; +import { EsqlEsqlShardFailure } from '@elastic/elasticsearch/lib/api/types'; const getTimeRange = () => { const date = Date.now(); @@ -106,11 +107,24 @@ describe('fetchEsqlQuery', () => { }); it('should add a warning when is_partial is true', async () => { + const shardFailure: EsqlEsqlShardFailure = { + reason: { type: 'test_failure', reason: 'too big data' }, + shard: 0, + index: 'test-index', + }; + const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ columns: [], values: [], is_partial: true, // is_partial is true + _clusters: { + details: { + 'cluster-1': { + failures: [shardFailure], + }, + }, + }, }); (getEsqlQueryHits as jest.Mock).mockReturnValue({ @@ -152,7 +166,7 @@ describe('fetchEsqlQuery', () => { }); const warning = - 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.'; + 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues. Failures: [{"reason":{"type":"test_failure","reason":"too big data"},"shard":0,"index":"test-index"}]'; expect(mockRuleResultService.addLastRunWarning).toHaveBeenCalledWith(warning); expect(mockRuleResultService.setLastRunOutcomeMessage).toHaveBeenCalledWith(warning); }); diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts index 68dccd61000da..af92e1609b6c6 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts @@ -18,6 +18,7 @@ import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/se import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { i18n } from '@kbn/i18n'; +import type { EsqlEsqlShardFailure } from '@elastic/elasticsearch/lib/api/types'; import type { EsqlTable } from '../../../../common'; import { getEsqlQueryHits } from '../../../../common'; import type { OnlyEsqlQueryRuleParams } from '../types'; @@ -86,10 +87,7 @@ export async function fetchEsqlQuery({ const isPartial = response.is_partial ?? false; if (ruleResultService && isPartial) { - const warning = i18n.translate('xpack.stackAlerts.esQuery.isPartialWarning', { - defaultMessage: - 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.', - }); + const warning = getPartialResultsWarning(response); ruleResultService.addLastRunWarning(warning); ruleResultService.setLastRunOutcomeMessage(warning); } @@ -170,3 +168,22 @@ export function generateLink( return redirectUrl; } + +function getPartialResultsWarning(response: EsqlTable) { + const clusters = response?._clusters?.details ?? {}; + const shardFailures = Object.keys(clusters).reduce((acc, cluster) => { + const failures = clusters[cluster]?.failures ?? []; + + if (failures.length > 0) { + acc.push(...failures); + } + + return acc; + }, []); + + return i18n.translate('xpack.stackAlerts.esQuery.partialResultsWarning', { + defaultMessage: + 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues. Failures: {failures}', + values: { failures: JSON.stringify(shardFailures) }, + }); +} From 34e8381fc7dc663c42361ee84eec0c330c780c89 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:18:14 +0000 Subject: [PATCH 7/8] [CI] Auto-commit changed files from 'node scripts/eslint_all_files --no-cache --fix' --- .../server/rule_types/es_query/lib/fetch_esql_query.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts index 36be504641ded..1a11fcda14f57 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts @@ -16,7 +16,7 @@ import { publicRuleResultServiceMock } from '@kbn/alerting-plugin/server/monitor import { getEsqlQueryHits } from '../../../../common'; import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; -import { EsqlEsqlShardFailure } from '@elastic/elasticsearch/lib/api/types'; +import type { EsqlEsqlShardFailure } from '@elastic/elasticsearch/lib/api/types'; const getTimeRange = () => { const date = Date.now(); From d3a2bb99ea626b995393515a807551d7aa68f86d Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Wed, 23 Jul 2025 00:58:31 +0200 Subject: [PATCH 8/8] Fix the warning message --- .../es_query/lib/fetch_esql_query.test.ts | 57 ++++++++++++++++++- .../es_query/lib/fetch_esql_query.ts | 4 +- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts index 36be504641ded..0eb8612e4372c 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts @@ -16,7 +16,7 @@ import { publicRuleResultServiceMock } from '@kbn/alerting-plugin/server/monitor import { getEsqlQueryHits } from '../../../../common'; import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; -import { EsqlEsqlShardFailure } from '@elastic/elasticsearch/lib/api/types'; +import type { EsqlEsqlShardFailure } from '@elastic/elasticsearch/lib/api/types'; const getTimeRange = () => { const date = Date.now(); @@ -171,6 +171,61 @@ describe('fetchEsqlQuery', () => { expect(mockRuleResultService.setLastRunOutcomeMessage).toHaveBeenCalledWith(warning); }); + it('should add a warning when is_partial is true but there is no shard failure', async () => { + const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ + columns: [], + values: [], + is_partial: true, // is_partial is true + _clusters: { + details: {}, + }, + }); + + (getEsqlQueryHits as jest.Mock).mockReturnValue({ + results: { + esResult: { + _shards: { failed: 0, successful: 0, total: 0 }, + aggregations: {}, + hits: { hits: [] }, + timed_out: false, + took: 0, + }, + isCountAgg: false, + isGroupAgg: true, + }, + }); + + await fetchEsqlQuery({ + ruleId: 'testRuleId', + alertLimit: 1, + params: { ...defaultParams, groupBy: 'row' }, + services: { + logger, + scopedClusterClient, + // @ts-expect-error + share: { + url: { + locators: { + get: jest.fn().mockReturnValue({ + getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'), + } as unknown as LocatorPublic), + }, + }, + } as SharePluginStart, + ruleResultService: mockRuleResultService, + }, + spacePrefix: '', + dateStart: new Date().toISOString(), + dateEnd: new Date().toISOString(), + }); + + const warning = + 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.'; + expect(mockRuleResultService.addLastRunWarning).toHaveBeenCalledWith(warning); + expect(mockRuleResultService.setLastRunOutcomeMessage).toHaveBeenCalledWith(warning); + }); + it('should not add a warning when is_partial is false', async () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); scopedClusterClient.asCurrentUser.transport.request.mockResolvedValueOnce({ diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts index af92e1609b6c6..e1e412b39b3d6 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts @@ -183,7 +183,9 @@ function getPartialResultsWarning(response: EsqlTable) { return i18n.translate('xpack.stackAlerts.esQuery.partialResultsWarning', { defaultMessage: - 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues. Failures: {failures}', + shardFailures.length > 0 + ? 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues. Failures: {failures}' + : 'The query returned partial results. Some clusters may have been skipped due to timeouts or other issues.', values: { failures: JSON.stringify(shardFailures) }, }); }