mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# 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:
parent
ddc4bbcb53
commit
74a6d1b837
16 changed files with 972 additions and 26 deletions
|
@ -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 } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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> = (
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue