diff --git a/x-pack/platform/plugins/shared/alerting/common/alert_schema/field_maps/component_template_from_field_map.ts b/x-pack/platform/plugins/shared/alerting/common/alert_schema/field_maps/component_template_from_field_map.ts index 74257f2f286e6..669751ca40f42 100644 --- a/x-pack/platform/plugins/shared/alerting/common/alert_schema/field_maps/component_template_from_field_map.ts +++ b/x-pack/platform/plugins/shared/alerting/common/alert_schema/field_maps/component_template_from_field_map.ts @@ -5,7 +5,10 @@ * 2.0. */ -import type { ClusterPutComponentTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; +import type { + ClusterPutComponentTemplateRequest, + MappingDynamicTemplate, +} from '@elastic/elasticsearch/lib/api/types'; import { type FieldMap } from '@kbn/alerts-as-data-utils'; import { mappingFromFieldMap } from './mapping_from_field_map'; @@ -14,12 +17,14 @@ export interface GetComponentTemplateFromFieldMapOpts { fieldMap: FieldMap; includeSettings?: boolean; dynamic?: 'strict' | false; + dynamicTemplates?: Array>; } export const getComponentTemplateFromFieldMap = ({ name, fieldMap, dynamic, includeSettings, + dynamicTemplates, }: GetComponentTemplateFromFieldMapOpts): ClusterPutComponentTemplateRequest => { return { name, @@ -37,7 +42,10 @@ export const getComponentTemplateFromFieldMap = ({ : {}), }, - mappings: mappingFromFieldMap(fieldMap, dynamic ?? 'strict'), + mappings: { + ...mappingFromFieldMap(fieldMap, dynamic ?? 'strict'), + ...(dynamicTemplates ? { dynamic_templates: dynamicTemplates } : {}), + }, }, }; }; diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_service/alerts_service.test.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_service/alerts_service.test.ts index 77f01b32d5c01..c3ecf02a63da2 100644 --- a/x-pack/platform/plugins/shared/alerting/server/alerts_service/alerts_service.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_service/alerts_service.test.ts @@ -174,6 +174,7 @@ const getIndexTemplatePutBody = (opts?: GetIndexTemplatePutBodyOpts) => { }), 'index.mapping.ignore_malformed': true, 'index.mapping.total_fields.limit': 2500, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, }, mappings: { dynamic: false, @@ -479,6 +480,7 @@ describe('Alerts Service', () => { settings: { ...existingIndexTemplate.index_template.template?.settings, 'index.mapping.total_fields.limit': 2500, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, }, }, }); @@ -899,6 +901,7 @@ describe('Alerts Service', () => { }, }), 'index.mapping.ignore_malformed': true, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, 'index.mapping.total_fields.limit': 2500, }, mappings: { diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts index 85a7a31a97357..178486d48213e 100644 --- a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts @@ -69,7 +69,9 @@ const IndexPatterns = { describe('createConcreteWriteIndex', () => { for (const useDataStream of [false, true]) { const label = useDataStream ? 'data streams' : 'aliases'; - const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts: useDataStream }); + const dataStreamAdapter = getDataStreamAdapter({ + useDataStreamForAlerts: useDataStream, + }); beforeEach(() => { jest.resetAllMocks(); @@ -79,7 +81,9 @@ describe('createConcreteWriteIndex', () => { describe(`using ${label} for alert indices`, () => { it(`should call esClient to put index template`, async () => { clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ + data_streams: [], + })); await createConcreteWriteIndex({ logger, esClient: clusterClient, @@ -106,7 +110,9 @@ describe('createConcreteWriteIndex', () => { it(`should retry on transient ES errors`, async () => { clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ + data_streams: [], + })); clusterClient.indices.create .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) @@ -138,7 +144,9 @@ describe('createConcreteWriteIndex', () => { it(`should log and throw error if max retries exceeded`, async () => { clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ + data_streams: [], + })); clusterClient.indices.create.mockRejectedValue(new EsErrors.ConnectionError('foo')); clusterClient.indices.createDataStream.mockRejectedValue( new EsErrors.ConnectionError('foo') @@ -168,7 +176,9 @@ describe('createConcreteWriteIndex', () => { it(`should log and throw error if ES throws error`, async () => { clusterClient.indices.getAlias.mockImplementation(async () => ({})); - clusterClient.indices.getDataStream.mockImplementation(async () => ({ data_streams: [] })); + clusterClient.indices.getDataStream.mockImplementation(async () => ({ + data_streams: [], + })); clusterClient.indices.create.mockRejectedValueOnce(new Error('generic error')); clusterClient.indices.createDataStream.mockRejectedValueOnce(new Error('generic error')); @@ -204,7 +214,9 @@ describe('createConcreteWriteIndex', () => { clusterClient.indices.create.mockRejectedValueOnce(error); clusterClient.indices.get.mockImplementationOnce(async () => ({ '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, + aliases: { + '.alerts-test.alerts-default': { is_write_index: true }, + }, }, })); @@ -237,7 +249,9 @@ describe('createConcreteWriteIndex', () => { .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) .mockImplementationOnce(async () => ({ '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, + aliases: { + '.alerts-test.alerts-default': { is_write_index: true }, + }, }, })); @@ -268,7 +282,9 @@ describe('createConcreteWriteIndex', () => { clusterClient.indices.create.mockRejectedValueOnce(error); clusterClient.indices.get.mockImplementationOnce(async () => ({ '.internal.alerts-test.alerts-default-000001': { - aliases: { '.alerts-test.alerts-default': { is_write_index: false } }, + aliases: { + '.alerts-test.alerts-default': { is_write_index: false }, + }, }, })); @@ -599,6 +615,386 @@ describe('createConcreteWriteIndex', () => { ); }); + it(`should increase the limit and retry if ES throws an exceeded limit error`, async () => { + const existingIndexTemplate = { + name: 'test-template', + index_template: { + index_patterns: ['test*'], + composed_of: ['test-mappings'], + template: { + settings: { + auto_expand_replicas: '0-1', + hidden: true, + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-empty-default`, + }, + 'index.mapping.total_fields.limit': 1800, + }, + mappings: { + dynamic: false, + }, + }, + }, + }; + + clusterClient.indices.getIndexTemplate.mockResolvedValue({ + index_templates: [existingIndexTemplate], + }); + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + + if (useDataStream) { + clusterClient.indices.putMapping + .mockRejectedValueOnce(new Error('Limit of total fields [2500] has been exceeded')) + .mockRejectedValueOnce(new Error('Limit of total fields [2501] has been exceeded')) + .mockRejectedValueOnce(new Error('Limit of total fields [2503] has been exceeded')) + .mockResolvedValue({ acknowledged: true }); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.putSettings).toBeCalledTimes(4); + expect(clusterClient.indices.putIndexTemplate).toBeCalledTimes(3); + expect(logger.info).toBeCalledTimes(3); + + expect(clusterClient.indices.putSettings).toHaveBeenNthCalledWith(1, { + index: '.alerts-test.alerts-default', + settings: { + 'index.mapping.total_fields.limit': 2500, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }); + expect(clusterClient.indices.putSettings).toHaveBeenNthCalledWith(2, { + index: '.alerts-test.alerts-default', + settings: { + 'index.mapping.total_fields.limit': 2501, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }); + expect(clusterClient.indices.putSettings).toHaveBeenNthCalledWith(3, { + index: '.alerts-test.alerts-default', + settings: { + 'index.mapping.total_fields.limit': 2503, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }); + expect(clusterClient.indices.putSettings).toHaveBeenNthCalledWith(4, { + index: '.alerts-test.alerts-default', + settings: { + 'index.mapping.total_fields.limit': 2506, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith(1, { + composed_of: ['test-mappings'], + index_patterns: ['test*'], + template: { + mappings: { + dynamic: false, + }, + settings: { + auto_expand_replicas: '0-1', + hidden: true, + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: '.alerts-empty-default', + }, + 'index.mapping.total_fields.limit': 2501, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }, + name: 'test-template', + }); + + expect(logger.info).toHaveBeenNthCalledWith( + 1, + 'total_fields.limit of .alerts-test.alerts-default has been increased from 2500 to 2501' + ); + expect(logger.info).toHaveBeenNthCalledWith( + 2, + 'total_fields.limit of .alerts-test.alerts-default has been increased from 2501 to 2503' + ); + expect(logger.info).toHaveBeenNthCalledWith( + 3, + 'total_fields.limit of .alerts-test.alerts-default has been increased from 2503 to 2506' + ); + expect(logger.debug).toHaveBeenNthCalledWith( + 3, + `Retrying PUT mapping for .alerts-test.alerts-default with increased total_fields.limit of 2501. Attempt: 1` + ); + expect(logger.debug).toHaveBeenNthCalledWith( + 4, + `Retrying PUT mapping for .alerts-test.alerts-default with increased total_fields.limit of 2503. Attempt: 2` + ); + expect(logger.debug).toHaveBeenNthCalledWith( + 5, + `Retrying PUT mapping for .alerts-test.alerts-default with increased total_fields.limit of 2506. Attempt: 3` + ); + } else { + clusterClient.indices.putMapping + .mockResolvedValueOnce({ acknowledged: true }) + .mockRejectedValueOnce(new Error('Limit of total fields [2500] has been exceeded')) + .mockRejectedValueOnce(new Error('Limit of total fields [2501] has been exceeded')) + .mockRejectedValueOnce(new Error('Limit of total fields [2503] has been exceeded')) + .mockResolvedValue({ acknowledged: true }); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }); + + expect(clusterClient.indices.putSettings).toBeCalledTimes(5); + expect(clusterClient.indices.putIndexTemplate).toBeCalledTimes(3); + expect(logger.info).toBeCalledTimes(4); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith(1, { + composed_of: ['test-mappings'], + index_patterns: ['test*'], + template: { + mappings: { + dynamic: false, + }, + settings: { + auto_expand_replicas: '0-1', + hidden: true, + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: '.alerts-empty-default', + }, + 'index.mapping.total_fields.limit': 2501, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }, + name: 'test-template', + }); + + expect(clusterClient.indices.putSettings).toHaveBeenNthCalledWith(2, { + index: '.internal.alerts-test.alerts-default-000001', + settings: { + 'index.mapping.total_fields.limit': 2500, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }); + expect(clusterClient.indices.putSettings).toHaveBeenNthCalledWith(3, { + index: '.internal.alerts-test.alerts-default-000001', + settings: { + 'index.mapping.total_fields.limit': 2501, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }); + expect(clusterClient.indices.putSettings).toHaveBeenNthCalledWith(4, { + index: '.internal.alerts-test.alerts-default-000001', + settings: { + 'index.mapping.total_fields.limit': 2503, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }); + expect(clusterClient.indices.putSettings).toHaveBeenNthCalledWith(5, { + index: '.internal.alerts-test.alerts-default-000001', + settings: { + 'index.mapping.total_fields.limit': 2506, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }); + + // The first call to logger.info is in createAliasStream, therefore we start testing from 2nd + expect(logger.info).toHaveBeenNthCalledWith( + 2, + 'total_fields.limit of alias_2 has been increased from 2500 to 2501' + ); + expect(logger.info).toHaveBeenNthCalledWith( + 3, + 'total_fields.limit of alias_2 has been increased from 2501 to 2503' + ); + expect(logger.info).toHaveBeenNthCalledWith( + 4, + 'total_fields.limit of alias_2 has been increased from 2503 to 2506' + ); + } + }); + + it(`should stop increasing the limit after 100 attemps`, async () => { + const existingIndexTemplate = { + name: 'test-template', + index_template: { + index_patterns: ['test*'], + composed_of: ['test-mappings'], + template: { + settings: { + auto_expand_replicas: '0-1', + hidden: true, + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-empty-default`, + }, + 'index.mapping.total_fields.limit': 1800, + }, + mappings: { + dynamic: false, + }, + }, + }, + }; + + clusterClient.indices.getIndexTemplate.mockResolvedValue({ + index_templates: [existingIndexTemplate], + }); + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + + if (useDataStream) { + clusterClient.indices.putMapping.mockRejectedValue( + new Error('Limit of total fields [2501] has been exceeded') + ); + + await expect( + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + '"Limit of total fields [2501] has been exceeded"' + ); + + expect(logger.info).toHaveBeenCalledTimes(100); + } + }); + + it(`should not increase the limit when the index template is not found`, async () => { + clusterClient.indices.getIndexTemplate.mockResolvedValue({ + index_templates: [], + }); + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + + if (useDataStream) { + clusterClient.indices.putMapping + .mockRejectedValueOnce(new Error('Limit of total fields [2500] has been exceeded')) + .mockResolvedValue({ acknowledged: true }); + + await expect( + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + '"Limit of total fields [2500] has been exceeded"' + ); + + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(1); + expect(logger.info).not.toHaveBeenCalled(); + } + }); + + it(`should log an error when there is an error while increasing the fields limit`, async () => { + const error = new Error('generic error'); + const existingIndexTemplate = { + name: 'test-template', + index_template: { + index_patterns: ['test*'], + composed_of: ['test-mappings'], + template: { + settings: { + auto_expand_replicas: '0-1', + hidden: true, + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-empty-default`, + }, + 'index.mapping.total_fields.limit': 1800, + }, + mappings: { + dynamic: false, + }, + }, + }, + }; + + clusterClient.indices.getIndexTemplate.mockResolvedValue({ + index_templates: [existingIndexTemplate], + }); + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + clusterClient.indices.putSettings.mockResolvedValueOnce({ + acknowledged: true, + }); + clusterClient.indices.putIndexTemplate.mockRejectedValueOnce(error); + + if (useDataStream) { + clusterClient.indices.putMapping + .mockRejectedValueOnce(new Error('Limit of total fields [2500] has been exceeded')) + .mockResolvedValueOnce({ acknowledged: true }); + + await expect( + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Limit of total fields [2500] has been exceeded"` + ); + + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + 'An error occured while increasing total_fields.limit of .alerts-test.alerts-default - generic error', + error + ); + } else { + clusterClient.indices.putMapping + .mockRejectedValueOnce(new Error('Limit of total fields [2500] has been exceeded')) + .mockResolvedValueOnce({ acknowledged: true }); + + await expect( + createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + dataStreamAdapter, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Limit of total fields [2500] has been exceeded"` + ); + + expect(logger.error).toHaveBeenCalledWith( + 'An error occured while increasing total_fields.limit of alias_1 - generic error', + error + ); + } + }); + it(`should log and return when simulating updated mappings throws error`, async () => { clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); clusterClient.indices.getDataStream.mockImplementation(async () => GetDataStreamResponse); diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_concrete_write_index.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_concrete_write_index.ts index 8ecd2cfc89aca..d9726fb74accf 100644 --- a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_concrete_write_index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_concrete_write_index.ts @@ -11,6 +11,7 @@ import { get, sortBy } from 'lodash'; import type { IIndexPatternString } from '../resource_installer_utils'; import { retryTransientEsErrors } from './retry_transient_es_errors'; import type { DataStreamAdapter } from './data_stream_adapter'; +import { updateIndexTemplateFieldsLimit } from './update_index_template_fields_limit'; export interface ConcreteIndexInfo { index: string; @@ -31,8 +32,11 @@ interface UpdateIndexOpts { esClient: ElasticsearchClient; totalFieldsLimit: number; concreteIndexInfo: ConcreteIndexInfo; + attempt?: number; } +const MAX_FIELDS_LIMIT_INCREASE_ATTEMPTS = 100; + const updateTotalFieldLimitSetting = async ({ logger, esClient, @@ -45,7 +49,10 @@ const updateTotalFieldLimitSetting = async ({ () => esClient.indices.putSettings({ index, - settings: { 'index.mapping.total_fields.limit': totalFieldsLimit }, + settings: { + 'index.mapping.total_fields.limit': totalFieldsLimit, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, }), { logger } ); @@ -66,6 +73,7 @@ const updateUnderlyingMapping = async ({ logger, esClient, concreteIndexInfo, + attempt = 1, }: UpdateIndexOpts) => { const { index, alias } = concreteIndexInfo; let simulatedIndexMapping: IndicesSimulateIndexTemplateResponse; @@ -96,6 +104,38 @@ const updateUnderlyingMapping = async ({ return; } catch (err) { + if (attempt <= MAX_FIELDS_LIMIT_INCREASE_ATTEMPTS) { + try { + const newLimit = await increaseFieldsLimit({ + err, + esClient, + concreteIndexInfo, + logger, + increment: attempt, + }); + if (newLimit) { + logger.debug( + `Retrying PUT mapping for ${alias} with increased total_fields.limit of ${newLimit}. Attempt: ${attempt}` + ); + await updateUnderlyingMapping({ + logger, + esClient, + concreteIndexInfo, + totalFieldsLimit: newLimit, + attempt: attempt + 1, + }); + return; + } + } catch (e) { + logger.error( + `An error occured while increasing total_fields.limit of ${alias} - ${e.message}`, + e + ); + // Throw the original error + throw err; + } + } + logger.error(`Failed to PUT mapping for ${alias}: ${err.message}`); throw err; } @@ -205,3 +245,59 @@ export async function setConcreteWriteIndex(opts: SetConcreteWriteIndexOpts) { ); } } + +const increaseFieldsLimit = async ({ + err, + esClient, + concreteIndexInfo, + logger, + increment, +}: { + err: Error; + esClient: ElasticsearchClient; + concreteIndexInfo: ConcreteIndexInfo; + logger: Logger; + increment: number; +}): Promise => { + const { alias } = concreteIndexInfo; + const match = err.message + ? err.message.match(/Limit of total fields \[(\d+)\] has been exceeded/) + : null; + + if (match !== null) { + const exceededLimit = parseInt(match[1], 10); + const newLimit = exceededLimit + increment; + + const { index_templates: indexTemplates } = await retryTransientEsErrors( + () => + esClient.indices.getIndexTemplate({ + name: `${alias}-index-template`, + }), + { logger } + ); + + if (indexTemplates.length <= 0) { + logger.error(`No index template found for ${alias}`); + return; + } + const template = indexTemplates[0]; + + // Update the limit in the index + await updateTotalFieldLimitSetting({ + logger, + esClient, + totalFieldsLimit: newLimit, + concreteIndexInfo, + }); + // Update the limit in the index template + await retryTransientEsErrors( + () => updateIndexTemplateFieldsLimit({ esClient, template, limit: newLimit }), + { logger } + ); + logger.info( + `total_fields.limit of ${alias} has been increased from ${exceededLimit} to ${newLimit}` + ); + + return newLimit; + } +}; diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_component_template.test.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_component_template.test.ts index ab7b13c822083..770cfc3599995 100644 --- a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_component_template.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_component_template.test.ts @@ -183,11 +183,101 @@ describe('createOrUpdateComponentTemplate', () => { settings: { ...existingIndexTemplate.index_template.template?.settings, 'index.mapping.total_fields.limit': 2500, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, }, }, }); }); + it(`should flatten ignore_missing_component_templates when ignore_missing_component_templates is provided`, async () => { + clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce( + new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 400, + body: { + error: { + root_cause: [ + { + type: 'illegal_argument_exception', + reason: + 'updating component template [.alerts-ecs-mappings] results in invalid composable template [.alerts-security.alerts-default-index-template] after templates are merged', + }, + ], + type: 'illegal_argument_exception', + reason: + 'updating component template [.alerts-ecs-mappings] results in invalid composable template [.alerts-security.alerts-default-index-template] after templates are merged', + caused_by: { + type: 'illegal_argument_exception', + reason: + 'composable template [.alerts-security.alerts-default-index-template] template after composition with component templates [.alerts-ecs-mappings, .alerts-security.alerts-mappings, .alerts-technical-mappings] is invalid', + caused_by: { + type: 'illegal_argument_exception', + reason: + 'invalid composite mappings for [.alerts-security.alerts-default-index-template]', + caused_by: { + type: 'illegal_argument_exception', + reason: 'Limit of total fields [2500] has been exceeded', + }, + }, + }, + }, + }, + }) + ) + ); + const existingIndexTemplate = { + name: 'test-template', + index_template: { + index_patterns: ['test*'], + composed_of: ['test-mappings'], + ignore_missing_component_templates: ['test-mappings', 'test-mappings-2'], + template: { + settings: { + auto_expand_replicas: '0-1', + hidden: true, + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-empty-default`, + }, + 'index.mapping.total_fields.limit': 1800, + }, + mappings: { + dynamic: false, + }, + }, + }, + }; + + clusterClient.indices.getIndexTemplate.mockResolvedValueOnce({ + index_templates: [existingIndexTemplate], + }); + + await createOrUpdateComponentTemplate({ + logger, + esClient: clusterClient, + template: ComponentTemplate, + totalFieldsLimit: 2500, + }); + + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith({ + name: existingIndexTemplate.name, + ...existingIndexTemplate.index_template, + template: { + ...existingIndexTemplate.index_template.template, + settings: { + ...existingIndexTemplate.index_template.template?.settings, + 'index.mapping.total_fields.limit': 2500, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }, + ignore_missing_component_templates: ['test-mappings', 'test-mappings-2'], + }); + + expect(logger.info).toHaveBeenCalledWith( + 'The total number of fields defined by the templates cannot exceed the limit [2500]. if you want to add more fields, please increase the limit' + ); + }); + it(`should update index template field limit and retry if putTemplate throws error with field limit error when there are malformed index templates`, async () => { clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce( new EsErrors.ResponseError( @@ -287,6 +377,7 @@ describe('createOrUpdateComponentTemplate', () => { settings: { ...existingIndexTemplate.index_template.template?.settings, 'index.mapping.total_fields.limit': 2500, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, }, }, }); diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_component_template.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_component_template.ts index 06675a1998bc8..20f6e6762a794 100644 --- a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_component_template.ts +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_component_template.ts @@ -12,6 +12,7 @@ import type { import type { Logger, ElasticsearchClient } from '@kbn/core/server'; import { asyncForEach } from '@kbn/std'; import { retryTransientEsErrors } from './retry_transient_es_errors'; +import { updateIndexTemplateFieldsLimit } from './update_index_template_fields_limit'; interface CreateOrUpdateComponentTemplateOpts { logger: Logger; @@ -48,21 +49,10 @@ const getIndexTemplatesUsingComponentTemplate = async ( async (template: IndicesGetIndexTemplateIndexTemplateItem) => { await retryTransientEsErrors( () => - esClient.indices.putIndexTemplate({ - name: template.name, - ...template.index_template, - template: { - ...template.index_template.template, - settings: { - ...template.index_template.template?.settings, - 'index.mapping.total_fields.limit': totalFieldsLimit, - }, - }, - // GET brings string | string[] | undefined but this PUT expects string[] - ignore_missing_component_templates: template.index_template - .ignore_missing_component_templates - ? [template.index_template.ignore_missing_component_templates].flat() - : undefined, + updateIndexTemplateFieldsLimit({ + esClient, + template, + limit: totalFieldsLimit, }), { logger } ); @@ -81,6 +71,11 @@ const createOrUpdateComponentTemplateHelper = async ( } catch (error) { const reason = error?.meta?.body?.error?.caused_by?.caused_by?.caused_by?.reason; if (reason && reason.match(/Limit of total fields \[\d+\] has been exceeded/) != null) { + if (reason === `Limit of total fields [${totalFieldsLimit}] has been exceeded`) { + logger.info( + `The total number of fields defined by the templates cannot exceed the limit [${totalFieldsLimit}]. if you want to add more fields, please increase the limit` + ); + } // This error message occurs when there is an index template using this component template // that contains a field limit setting that using this component template exceeds // Specifically, this can happen for the ECS component template when we add new fields diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts index a6232e75db3a2..d6d221f8e9a36 100644 --- a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts @@ -48,6 +48,7 @@ const IndexTemplate = (namespace = 'default', useDataStream = false) => ({ }, }), 'index.mapping.ignore_malformed': true, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, 'index.mapping.total_fields.limit': 2500, }, }, diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_index_template.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_index_template.ts index 0883c349e7abb..f9d90d0d4551e 100644 --- a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_index_template.ts +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/create_or_update_index_template.ts @@ -71,6 +71,7 @@ export const getIndexTemplate = ({ }), 'index.mapping.ignore_malformed': true, 'index.mapping.total_fields.limit': totalFieldsLimit, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, }, mappings: { dynamic: false, diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/update_index_template_fields_limit.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/update_index_template_fields_limit.ts new file mode 100644 index 0000000000000..f700a45684272 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_service/lib/update_index_template_fields_limit.ts @@ -0,0 +1,35 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { ElasticsearchClient } from '@kbn/core/server'; +import type { IndicesGetIndexTemplateIndexTemplateItem } from '@elastic/elasticsearch/lib/api/types'; + +export const updateIndexTemplateFieldsLimit = ({ + esClient, + template, + limit, +}: { + esClient: ElasticsearchClient; + template: IndicesGetIndexTemplateIndexTemplateItem; + limit: number; +}) => { + return esClient.indices.putIndexTemplate({ + name: template.name, + ...template.index_template, + template: { + ...template.index_template.template, + settings: { + ...template.index_template.template?.settings, + 'index.mapping.total_fields.limit': limit, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }, + // GET brings string | string[] | undefined but this PUT expects string[] + ignore_missing_component_templates: template.index_template.ignore_missing_component_templates + ? [template.index_template.ignore_missing_component_templates].flat() + : undefined, + }); +}; diff --git a/x-pack/platform/plugins/shared/alerting/server/alerts_service/resource_installer_utils.ts b/x-pack/platform/plugins/shared/alerting/server/alerts_service/resource_installer_utils.ts index c7445da8368b0..1afb4fc51c933 100644 --- a/x-pack/platform/plugins/shared/alerting/server/alerts_service/resource_installer_utils.ts +++ b/x-pack/platform/plugins/shared/alerting/server/alerts_service/resource_installer_utils.ts @@ -5,7 +5,10 @@ * 2.0. */ -import type { ClusterPutComponentTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; +import type { + ClusterPutComponentTemplateRequest, + MappingDynamicTemplate, +} from '@elastic/elasticsearch/lib/api/types'; import type { FieldMap } from '@kbn/alerts-as-data-utils'; import { getComponentTemplateFromFieldMap } from '../../common'; @@ -68,6 +71,7 @@ type GetComponentTemplateOpts = GetComponentTemplateNameOpts & { fieldMap: FieldMap; dynamic?: 'strict' | false; includeSettings?: boolean; + dynamicTemplates?: Array>; }; export const getComponentTemplate = ({ @@ -76,10 +80,12 @@ export const getComponentTemplate = ({ name, dynamic, includeSettings, + dynamicTemplates, }: GetComponentTemplateOpts): ClusterPutComponentTemplateRequest => getComponentTemplateFromFieldMap({ name: getComponentTemplateName({ context, name }), fieldMap, dynamic, includeSettings, + dynamicTemplates, }); diff --git a/x-pack/platform/plugins/shared/alerting/server/types.ts b/x-pack/platform/plugins/shared/alerting/server/types.ts index f023bf7b05d25..2e51a8a40c375 100644 --- a/x-pack/platform/plugins/shared/alerting/server/types.ts +++ b/x-pack/platform/plugins/shared/alerting/server/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { MappingDynamicTemplate } from '@elastic/elasticsearch/lib/api/types'; import type { IRouter, CustomRequestHandlerContext, @@ -201,6 +202,7 @@ export type GetViewInAppRelativeUrlFn = ( interface ComponentTemplateSpec { dynamic?: 'strict' | false; // defaults to 'strict' fieldMap: FieldMap; + dynamicTemplates?: Array>; } export type FormatAlert = ( diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/plugin.ts index fef7153c87118..5e9651fbd3945 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/plugin.ts @@ -75,6 +75,7 @@ const testRuleTypes = [ 'test.longRunning', 'test.exceedsAlertLimit', 'test.always-firing-alert-as-data', + 'test.always-firing-alert-as-data-with-dynamic-templates', 'test.patternFiringAad', 'test.waitingRule', 'test.patternFiringAutoRecoverFalse', diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/rule_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/rule_types.ts index 0080f72d67c6f..37dfc5ab50eb7 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/rule_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/rule_types.ts @@ -954,6 +954,79 @@ function getAlwaysFiringAlertAsDataRuleType() { return result; } +function getAlwaysFiringAlertAsDataWithDynamicTemplatesRuleType() { + const paramsSchema = schema.object({ + dynamic_fields: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + }); + type ParamsType = TypeOf; + + const result: RuleType< + ParamsType, + never, + RuleTypeState, + {}, + {}, + 'default', + 'recovered', + { 'kibana.alert.dynamic': { [key: string]: any } } + > = { + id: 'test.always-firing-alert-as-data-with-dynamic-templates', + name: 'Test: Rule with dynamicTemplates and writing Alerts as Data', + actionGroups: [{ id: 'default', name: 'Default' }], + category: 'management', + producer: 'alertsFixture', + solution: 'stack', + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + doesSetRecoveryContext: true, + validate: { + params: paramsSchema, + }, + async executor(ruleExecutorOptions) { + const { services, params } = ruleExecutorOptions; + + services.alertsClient?.report({ + id: '1', + actionGroup: 'default', + payload: { 'kibana.alert.dynamic': params.dynamic_fields }, + }); + + return { state: {} }; + }, + alerts: { + context: 'observability.test.alerts.dynamic.templates', + mappings: { + dynamic: false, + fieldMap: { + ['kibana.alert.dynamic']: { + type: 'object', + dynamic: true, + array: false, + required: false, + }, + }, + dynamicTemplates: [ + { + strings_as_keywords: { + path_match: 'kibana.alert.dynamic.*', + match_mapping_type: 'string', + mapping: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + ], + }, + useLegacyAlerts: false, + useEcs: false, + shouldWrite: true, + }, + }; + return result; +} + function getWaitingRuleType(logger: Logger) { const ParamsType = schema.object({ source: schema.string(), @@ -1394,6 +1467,7 @@ export function defineRuleTypes( alerting.registerType(getPatternSuccessOrFailureRuleType()); alerting.registerType(getExceedsAlertLimitRuleType()); alerting.registerType(getAlwaysFiringAlertAsDataRuleType()); + alerting.registerType(getAlwaysFiringAlertAsDataWithDynamicTemplatesRuleType()); alerting.registerType(getPatternFiringAutoRecoverFalseRuleType()); alerting.registerType(getPatternFiringAlertsAsDataRuleType()); alerting.registerType(getWaitingRuleType(logger)); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_dynamic_templates.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_dynamic_templates.ts new file mode 100644 index 0000000000000..0a4556a1b2b7e --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_dynamic_templates.ts @@ -0,0 +1,225 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { + MappingProperty, + PropertyName, + SearchHit, +} from '@elastic/elasticsearch/lib/api/types'; +import { alertFieldMap, type Alert } from '@kbn/alerts-as-data-utils'; +import { TOTAL_FIELDS_LIMIT } from '@kbn/alerting-plugin/server'; +import { get } from 'lodash'; +import type { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { Spaces } from '../../../../scenarios'; +import { + getEventLog, + getTestRuleData, + getUrlPrefix, + ObjectRemover, +} from '../../../../../common/lib'; + +// eslint-disable-next-line import/no-default-export +export default function createAlertsAsDataDynamicTemplatesTest({ getService }: FtrProviderContext) { + const es = getService('es'); + const retry = getService('retry'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const objectRemover = new ObjectRemover(supertestWithoutAuth); + + const alertsAsDataIndex = + '.internal.alerts-observability.test.alerts.dynamic.templates.alerts-default-000001'; + + describe('dynamic templates', function () { + this.tags('skipFIPS'); + describe('alerts as data fields limit', function () { + afterEach(async () => { + await objectRemover.removeAll(); + await es.deleteByQuery({ + index: alertsAsDataIndex, + query: { match_all: {} }, + conflicts: 'proceed', + }); + }); + + it(`should add the dynamic fields`, async () => { + // First run doesn't add the dynamic fields + const createdRule = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.always-firing-alert-as-data-with-dynamic-templates', + schedule: { interval: '1d' }, + throttle: null, + params: {}, + actions: [], + }) + ); + expect(createdRule.status).to.eql(200); + const ruleId = createdRule.body.id; + objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting'); + + await waitForEventLogDocs(ruleId, new Map([['execute', { equal: 1 }]])); + + const existingFields = alertFieldMap; + const numberOfExistingFields = Object.keys(existingFields).length; + // there is no way to get the real number of fields from ES. + // Eventhough we have only as many as alertFieldMap fields, + // ES counts the each childs of the nested objects and multi_fields as seperate fields. + // therefore we add 9 to get the real number. + const nestedObjectsAndMultiFields = 9; + // Number of free slots that we want to have, so we can add dynamic fields as many + const numberofFreeSlots = 3; + const totalFields = + numberOfExistingFields + nestedObjectsAndMultiFields + numberofFreeSlots; + + const dummyFields: Record = {}; + for (let i = 0; i < TOTAL_FIELDS_LIMIT - totalFields; i++) { + const key = `${i}`.padStart(4, '0'); + dummyFields[key] = { type: 'keyword' }; + } + // add dummyFields to the index mappings, so it will reach the fields limits. + await es.indices.putMapping({ + index: alertsAsDataIndex, + properties: dummyFields, + dynamic: false, + }); + + await supertestWithoutAuth + .put(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${ruleId}`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + schedule: { interval: '1d' }, + throttle: null, + params: { + dynamic_fields: { 'host.id': '1', 'host.name': 'host-1' }, + }, + actions: [], + enabled: undefined, + rule_type_id: undefined, + consumer: undefined, + }) + ) + .expect(200); + + const runSoon = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${ruleId}/_run_soon`) + .set('kbn-xsrf', 'foo'); + expect(runSoon.status).to.eql(204); + + await waitForEventLogDocs(ruleId, new Map([['execute', { equal: 2 }]])); + + // Query for alerts + const alerts = await queryForAlertDocs(); + const alert = alerts[0]; + + // host.name is ignored + expect(alert._ignored).to.eql(['kibana.alert.dynamic.host.name']); + + const mapping = await es.indices.getMapping({ index: alertsAsDataIndex }); + const dynamicField = get( + mapping[alertsAsDataIndex], + 'mappings.properties.kibana.properties.alert.properties.dynamic.properties.host.properties.id.type' + ); + + // new dynamic field has been added + expect(dynamicField).to.eql('text'); + }); + }); + + describe('index field limits', () => { + afterEach(async () => { + await es.indices.delete({ + index: 'index-fields-limit-test-index', + }); + await es.indices.deleteIndexTemplate({ + name: 'index-fields-limit-test-template', + }); + }); + it('should return an exceeded limit error', async () => { + const template = await es.indices.putIndexTemplate({ + name: 'index-fields-limit-test-template', + template: { + mappings: { + properties: { + '@timestamp': { + type: 'date', + }, + 'field-2': { + type: 'keyword', + }, + 'field-3': { + type: 'keyword', + }, + 'field-4': { + type: 'keyword', + }, + 'field-5': { + type: 'keyword', + }, + }, + }, + settings: { + 'index.mapping.total_fields.limit': 5, + 'index.mapping.total_fields.ignore_dynamic_beyond_limit': true, + }, + }, + index_patterns: ['index-fields-limit-test-*'], + }); + + expect(template).to.eql({ acknowledged: true }); + + const index = await es.indices.create({ + index: 'index-fields-limit-test-index', + }); + + expect(index).to.eql({ + acknowledged: true, + index: 'index-fields-limit-test-index', + shards_acknowledged: true, + }); + + try { + await es.indices.putMapping({ + index: 'index-fields-limit-test-index', + properties: { + 'field-6': { + type: 'keyword', + }, + }, + }); + } catch (e) { + expect(e.message).to.contain('Limit of total fields [5] has been exceeded'); + } + }); + }); + }); + async function queryForAlertDocs(): Promise>> { + const searchResult = await es.search({ + index: alertsAsDataIndex, + query: { match_all: {} }, + }); + return searchResult.hits.hits as Array>; + } + + async function waitForEventLogDocs( + id: string, + actions: Map + ) { + return await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id, + provider: 'alerting', + actions, + }); + }); + } +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/index.ts index 21ebb4671fe72..a9a5fa13d88a7 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/index.ts @@ -15,5 +15,6 @@ export default function alertsAsDataTests({ loadTestFile }: FtrProviderContext) loadTestFile(require.resolve('./alerts_as_data_flapping')); loadTestFile(require.resolve('./alerts_as_data_conflicts')); loadTestFile(require.resolve('./alerts_as_data_alert_delay')); + loadTestFile(require.resolve('./alerts_as_data_dynamic_templates.ts')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts index 671f8b2aeb5d2..641b23cd8f448 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts @@ -171,6 +171,7 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F ignore_malformed: 'true', total_fields: { limit: '2500', + ignore_dynamic_beyond_limit: 'true', }, }, hidden: 'true', @@ -206,6 +207,7 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F ignore_malformed: 'true', total_fields: { limit: '2500', + ignore_dynamic_beyond_limit: 'true', }, });