[9.0] Auto increase fields limit of the alert indices (#216719) (#218202)

# Backport

This will backport the following commits from `main` to `9.0`:
- [Auto increase fields limit of the alert indices
(#216719)](https://github.com/elastic/kibana/pull/216719)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Ersin
Erdal","email":"92688503+ersin-erdal@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-04-15T07:38:27Z","message":"Auto
increase fields limit of the alert indices (#216719)\n\nThis PR adds the
auto-increase the fields limit on startup when an\nalerts index reaches
its limits because of the dynamic fields.\n\n# To verify:\nTo be able to
test this PR we need a rule type that adds dynamic fields.\nI used the
custom threshold rule for this:\n\nGo to the custom threshold rule type
definition and change its\nalerts.mappings to:\n```\n mappings: {\n //
dynamic: true,\n fieldMap: {\n 'kibana.alerting.grouping': {\n type:
'object',\n dynamic: true,\n array: false,\n required: false,\n },\n
...legacyExperimentalFieldMap,\n ...Array(412)\n .fill(0)\n
.reduce((acc, val, i) => {\n acc[`${i + 1}`] = { type: 'keyword', array:
false, required: false };\n return acc;\n }, {}),\n },\n
dynamicTemplates: [\n {\n strings_as_keywords: {\n path_match:
'kibana.alert.grouping.*',\n match_mapping_type: 'string',\n mapping:
{\n type: 'keyword',\n ignore_above: 1024,\n },\n },\n },\n ],\n },\n
```\n \n Above changes adds 412 dummy fields to the alerts index to make
it close to reach its fields limit (default: 2500).\n And makes
everything under `kibana.alert.grouping` path to be added to the index
as dynamic fields.\n \n Then apply the below changes to the custom
threshold rule executor:\n ```\n const grouping: Record<string, string>
= {};\n groups?.forEach((groupObj) => (grouping[groupObj.field] =
groupObj.value));\n \n const { uuid, start } = alertsClient.report({\n
id: `${group}`,\n actionGroup: actionGroupId,\n payload: {\n
[ALERT_REASON]: reason,\n [ALERT_EVALUATION_VALUES]: evaluationValues,\n
[ALERT_EVALUATION_THRESHOLD]: threshold,\n [ALERT_GROUP]: groups,\n //
@ts-ignore\n ['kibana.alerting.grouping']: grouping,\n
...flattenAdditionalContext(additionalContext),\n
...getEcsGroups(groups),\n },\n }); \n ```\n \nAbove changes add the
selected groups under `kibana.alerting.grouping` path.\n \nThen: \n- Run
ES with ` path.data=../your-local-data-path` to keep the data for the
next start.\n- Run Kibana\n- Create a custom threshold rule that
generates an alert and has at least 2 groups.\n- Let the rule run.\n- Go
to `Stack Management` > `Index Management` and search for observability
threshold index.\n- Check its mappings, it should show the dummy fields
you have added to the rule type and the first grouping you have selected
while you were creating the rule type.\n- Go to the Dev Tools and find
your alert in the
`.internal.alerts-observability.threshold.alerts-default-000001`
index.\nThe other groups you have selected should be saved under
`_ignored` field:\n```\n\"_ignored\": [\n
\"kibana.alerting.grouping.host.name\"\n],\n```\n- Stop Kibana\n-
increase the number of dummy fields you have added to the rule type
definition:\n```\n ...Array(412) <-- make this greater than 412\n
.fill(0)\n```\n- Start kibana again.\n- The new fields should be added
to the mappings. Check them on `Stack Management` > `Index Management`
\n- Check also the index settings: `Stack Management` > `Index
Management` >
`.internal.alerts-observability.threshold.alerts-default-000001` >
settings tab.\n- `\"mapping\" > \"total_fields\" > \"limit\" ` should be
greater than 2500\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f6c30d6b9ad1a46a73cc5c084a5e70051d78a7cb","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","v9.0.0","backport:version","v9.1.0","v8.19.0"],"title":"Auto
increase fields limit of the alert
indices","number":216719,"url":"https://github.com/elastic/kibana/pull/216719","mergeCommit":{"message":"Auto
increase fields limit of the alert indices (#216719)\n\nThis PR adds the
auto-increase the fields limit on startup when an\nalerts index reaches
its limits because of the dynamic fields.\n\n# To verify:\nTo be able to
test this PR we need a rule type that adds dynamic fields.\nI used the
custom threshold rule for this:\n\nGo to the custom threshold rule type
definition and change its\nalerts.mappings to:\n```\n mappings: {\n //
dynamic: true,\n fieldMap: {\n 'kibana.alerting.grouping': {\n type:
'object',\n dynamic: true,\n array: false,\n required: false,\n },\n
...legacyExperimentalFieldMap,\n ...Array(412)\n .fill(0)\n
.reduce((acc, val, i) => {\n acc[`${i + 1}`] = { type: 'keyword', array:
false, required: false };\n return acc;\n }, {}),\n },\n
dynamicTemplates: [\n {\n strings_as_keywords: {\n path_match:
'kibana.alert.grouping.*',\n match_mapping_type: 'string',\n mapping:
{\n type: 'keyword',\n ignore_above: 1024,\n },\n },\n },\n ],\n },\n
```\n \n Above changes adds 412 dummy fields to the alerts index to make
it close to reach its fields limit (default: 2500).\n And makes
everything under `kibana.alert.grouping` path to be added to the index
as dynamic fields.\n \n Then apply the below changes to the custom
threshold rule executor:\n ```\n const grouping: Record<string, string>
= {};\n groups?.forEach((groupObj) => (grouping[groupObj.field] =
groupObj.value));\n \n const { uuid, start } = alertsClient.report({\n
id: `${group}`,\n actionGroup: actionGroupId,\n payload: {\n
[ALERT_REASON]: reason,\n [ALERT_EVALUATION_VALUES]: evaluationValues,\n
[ALERT_EVALUATION_THRESHOLD]: threshold,\n [ALERT_GROUP]: groups,\n //
@ts-ignore\n ['kibana.alerting.grouping']: grouping,\n
...flattenAdditionalContext(additionalContext),\n
...getEcsGroups(groups),\n },\n }); \n ```\n \nAbove changes add the
selected groups under `kibana.alerting.grouping` path.\n \nThen: \n- Run
ES with ` path.data=../your-local-data-path` to keep the data for the
next start.\n- Run Kibana\n- Create a custom threshold rule that
generates an alert and has at least 2 groups.\n- Let the rule run.\n- Go
to `Stack Management` > `Index Management` and search for observability
threshold index.\n- Check its mappings, it should show the dummy fields
you have added to the rule type and the first grouping you have selected
while you were creating the rule type.\n- Go to the Dev Tools and find
your alert in the
`.internal.alerts-observability.threshold.alerts-default-000001`
index.\nThe other groups you have selected should be saved under
`_ignored` field:\n```\n\"_ignored\": [\n
\"kibana.alerting.grouping.host.name\"\n],\n```\n- Stop Kibana\n-
increase the number of dummy fields you have added to the rule type
definition:\n```\n ...Array(412) <-- make this greater than 412\n
.fill(0)\n```\n- Start kibana again.\n- The new fields should be added
to the mappings. Check them on `Stack Management` > `Index Management`
\n- Check also the index settings: `Stack Management` > `Index
Management` >
`.internal.alerts-observability.threshold.alerts-default-000001` >
settings tab.\n- `\"mapping\" > \"total_fields\" > \"limit\" ` should be
greater than 2500\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f6c30d6b9ad1a46a73cc5c084a5e70051d78a7cb"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/216719","number":216719,"mergeCommit":{"message":"Auto
increase fields limit of the alert indices (#216719)\n\nThis PR adds the
auto-increase the fields limit on startup when an\nalerts index reaches
its limits because of the dynamic fields.\n\n# To verify:\nTo be able to
test this PR we need a rule type that adds dynamic fields.\nI used the
custom threshold rule for this:\n\nGo to the custom threshold rule type
definition and change its\nalerts.mappings to:\n```\n mappings: {\n //
dynamic: true,\n fieldMap: {\n 'kibana.alerting.grouping': {\n type:
'object',\n dynamic: true,\n array: false,\n required: false,\n },\n
...legacyExperimentalFieldMap,\n ...Array(412)\n .fill(0)\n
.reduce((acc, val, i) => {\n acc[`${i + 1}`] = { type: 'keyword', array:
false, required: false };\n return acc;\n }, {}),\n },\n
dynamicTemplates: [\n {\n strings_as_keywords: {\n path_match:
'kibana.alert.grouping.*',\n match_mapping_type: 'string',\n mapping:
{\n type: 'keyword',\n ignore_above: 1024,\n },\n },\n },\n ],\n },\n
```\n \n Above changes adds 412 dummy fields to the alerts index to make
it close to reach its fields limit (default: 2500).\n And makes
everything under `kibana.alert.grouping` path to be added to the index
as dynamic fields.\n \n Then apply the below changes to the custom
threshold rule executor:\n ```\n const grouping: Record<string, string>
= {};\n groups?.forEach((groupObj) => (grouping[groupObj.field] =
groupObj.value));\n \n const { uuid, start } = alertsClient.report({\n
id: `${group}`,\n actionGroup: actionGroupId,\n payload: {\n
[ALERT_REASON]: reason,\n [ALERT_EVALUATION_VALUES]: evaluationValues,\n
[ALERT_EVALUATION_THRESHOLD]: threshold,\n [ALERT_GROUP]: groups,\n //
@ts-ignore\n ['kibana.alerting.grouping']: grouping,\n
...flattenAdditionalContext(additionalContext),\n
...getEcsGroups(groups),\n },\n }); \n ```\n \nAbove changes add the
selected groups under `kibana.alerting.grouping` path.\n \nThen: \n- Run
ES with ` path.data=../your-local-data-path` to keep the data for the
next start.\n- Run Kibana\n- Create a custom threshold rule that
generates an alert and has at least 2 groups.\n- Let the rule run.\n- Go
to `Stack Management` > `Index Management` and search for observability
threshold index.\n- Check its mappings, it should show the dummy fields
you have added to the rule type and the first grouping you have selected
while you were creating the rule type.\n- Go to the Dev Tools and find
your alert in the
`.internal.alerts-observability.threshold.alerts-default-000001`
index.\nThe other groups you have selected should be saved under
`_ignored` field:\n```\n\"_ignored\": [\n
\"kibana.alerting.grouping.host.name\"\n],\n```\n- Stop Kibana\n-
increase the number of dummy fields you have added to the rule type
definition:\n```\n ...Array(412) <-- make this greater than 412\n
.fill(0)\n```\n- Start kibana again.\n- The new fields should be added
to the mappings. Check them on `Stack Management` > `Index Management`
\n- Check also the index settings: `Stack Management` > `Index
Management` >
`.internal.alerts-observability.threshold.alerts-default-000001` >
settings tab.\n- `\"mapping\" > \"total_fields\" > \"limit\" ` should be
greater than 2500\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f6c30d6b9ad1a46a73cc5c084a5e70051d78a7cb"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ersin Erdal 2025-04-18 00:52:40 +02:00 committed by GitHub
parent ddc4bbcb53
commit 74a6d1b837
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 972 additions and 26 deletions

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import { 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<Record<string, MappingDynamicTemplate>>;
}
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 } : {}),
},
},
};
};

View file

@ -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,
@ -480,6 +481,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,
},
},
},
@ -914,6 +916,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: {

View file

@ -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,
@ -108,7 +112,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'))
@ -140,7 +146,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')
@ -170,7 +178,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'));
@ -206,7 +216,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 },
},
},
}));
@ -239,7 +251,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 },
},
},
}));
@ -270,7 +284,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 },
},
},
}));
@ -607,6 +623,390 @@ 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',
body: {
'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',
body: {
'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',
body: {
'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',
body: {
'index.mapping.total_fields.limit': 2506,
'index.mapping.total_fields.ignore_dynamic_beyond_limit': true,
},
});
expect(clusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith(1, {
body: {
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, {
body: {
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',
body: {
'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',
body: {
'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',
body: {
'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',
body: {
'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);

View file

@ -10,7 +10,8 @@ import { Logger, ElasticsearchClient } from '@kbn/core/server';
import { get, sortBy } from 'lodash';
import { IIndexPatternString } from '../resource_installer_utils';
import { retryTransientEsErrors } from './retry_transient_es_errors';
import { DataStreamAdapter } from './data_stream_adapter';
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,
body: { 'index.mapping.total_fields.limit': totalFieldsLimit },
body: {
'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;
}
@ -207,3 +247,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<number | undefined> => {
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;
}
};

View file

@ -183,12 +183,104 @@ 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,
body: {
...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(
@ -289,6 +381,7 @@ describe('createOrUpdateComponentTemplate', () => {
settings: {
...existingIndexTemplate.index_template.template?.settings,
'index.mapping.total_fields.limit': 2500,
'index.mapping.total_fields.ignore_dynamic_beyond_limit': true,
},
},
},

View file

@ -8,11 +8,11 @@
import {
ClusterPutComponentTemplateRequest,
IndicesGetIndexTemplateIndexTemplateItem,
type IndicesPutIndexTemplateRequest,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { 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;
@ -49,18 +49,10 @@ const getIndexTemplatesUsingComponentTemplate = async (
async (template: IndicesGetIndexTemplateIndexTemplateItem) => {
await retryTransientEsErrors(
() =>
esClient.indices.putIndexTemplate({
name: template.name,
body: {
...template.index_template,
template: {
...template.index_template.template,
settings: {
...template.index_template.template?.settings,
'index.mapping.total_fields.limit': totalFieldsLimit,
},
},
} as IndicesPutIndexTemplateRequest['body'],
updateIndexTemplateFieldsLimit({
esClient,
template,
limit: totalFieldsLimit,
}),
{ logger }
);
@ -79,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

View file

@ -49,6 +49,7 @@ const IndexTemplate = (namespace: string = 'default', useDataStream: boolean = f
},
}),
'index.mapping.ignore_malformed': true,
'index.mapping.total_fields.ignore_dynamic_beyond_limit': true,
'index.mapping.total_fields.limit': 2500,
},
},

View file

@ -72,6 +72,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,

View file

@ -0,0 +1,37 @@
/*
* 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,
body: {
...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,
},
});
};

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import { 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<Record<string, MappingDynamicTemplate>>;
};
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,
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { MappingDynamicTemplate } from '@elastic/elasticsearch/lib/api/types';
import type {
IRouter,
CustomRequestHandlerContext,
@ -198,6 +199,7 @@ export type GetViewInAppRelativeUrlFn<Params extends RuleTypeParams> = (
interface ComponentTemplateSpec {
dynamic?: 'strict' | false; // defaults to 'strict'
fieldMap: FieldMap;
dynamicTemplates?: Array<Record<string, MappingDynamicTemplate>>;
}
export type FormatAlert<AlertData extends RuleAlertData> = (

View file

@ -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',

View file

@ -939,6 +939,78 @@ function getAlwaysFiringAlertAsDataRuleType() {
return result;
}
function getAlwaysFiringAlertAsDataWithDynamicTemplatesRuleType() {
const paramsSchema = schema.object({
dynamic_fields: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
});
type ParamsType = TypeOf<typeof paramsSchema>;
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',
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(),
@ -1369,6 +1441,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));

View file

@ -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<PropertyName, MappingProperty> = {};
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<Alert>();
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<T>(): Promise<Array<SearchHit<T>>> {
const searchResult = await es.search({
index: alertsAsDataIndex,
query: { match_all: {} },
});
return searchResult.hits.hits as Array<SearchHit<T>>;
}
async function waitForEventLogDocs(
id: string,
actions: Map<string, { gte: number } | { equal: number }>
) {
return await retry.try(async () => {
return await getEventLog({
getService,
spaceId: Spaces.space1.id,
type: 'alert',
id,
provider: 'alerting',
actions,
});
});
}
}

View file

@ -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'));
});
}

View file

@ -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',
},
});