1111
1212from elastic_transport import ObjectApiResponse
1313from elasticsearch import Elasticsearch # type: ignore[reportMissingTypeStubs]
14+ from elasticsearch .exceptions import BadRequestError
1415from semver import Version
1516
1617from . import ecs , integrations , misc , utils
1718from .config import load_current_package_version
19+ from .esql_errors import EsqlSchemaError , EsqlSemanticError , EsqlSyntaxError , cleanup_empty_indices
1820from .integrations import (
1921 load_integrations_manifests ,
2022 load_integrations_schemas ,
2123)
2224from .rule import RuleMeta
2325from .schemas import get_stack_schemas
26+ from .schemas .definitions import HTTP_STATUS_BAD_REQUEST
27+ from .utils import combine_dicts
2428
2529
2630def get_rule_integrations (metadata : RuleMeta ) -> list [str ]:
2731 """Retrieve rule integrations from metadata."""
28- rule_integrations : list [str ] = []
2932 if metadata .integration :
30- if isinstance (metadata .integration , list ):
31- rule_integrations = metadata .integration
32- else :
33- rule_integrations = [metadata .integration ]
33+ rule_integrations : list [str ] = (
34+ metadata .integration if isinstance (metadata .integration , list ) else [metadata .integration ]
35+ )
36+ else :
37+ rule_integrations : list [str ] = []
3438 return rule_integrations
3539
3640
37- def prepare_integration_mappings (
41+ def create_index_with_index_mapping (
42+ elastic_client : Elasticsearch , index_name : str , mappings : dict [str , Any ]
43+ ) -> ObjectApiResponse [Any ] | None :
44+ """Create an index with the specified mappings and settings to support large number of fields and nested objects."""
45+ try :
46+ return elastic_client .indices .create (
47+ index = index_name ,
48+ mappings = {"properties" : mappings },
49+ settings = {
50+ "index.mapping.total_fields.limit" : 10000 ,
51+ "index.mapping.nested_fields.limit" : 500 ,
52+ "index.mapping.nested_objects.limit" : 10000 ,
53+ },
54+ )
55+ except BadRequestError as e :
56+ error_message = str (e )
57+ if (
58+ e .status_code == HTTP_STATUS_BAD_REQUEST
59+ and "validation_exception" in error_message
60+ and "Validation Failed: 1: this action would add [2] shards" in error_message
61+ ):
62+ cleanup_empty_indices (elastic_client )
63+ try :
64+ return elastic_client .indices .create (
65+ index = index_name ,
66+ mappings = {"properties" : mappings },
67+ settings = {
68+ "index.mapping.total_fields.limit" : 10000 ,
69+ "index.mapping.nested_fields.limit" : 500 ,
70+ "index.mapping.nested_objects.limit" : 10000 ,
71+ },
72+ )
73+ except BadRequestError as retry_error :
74+ raise EsqlSchemaError (str (retry_error ), elastic_client ) from retry_error
75+ raise EsqlSchemaError (error_message , elastic_client ) from e
76+
77+
78+ def get_existing_mappings (elastic_client : Elasticsearch , indices : list [str ]) -> tuple [dict [str , Any ], dict [str , Any ]]:
79+ """Retrieve mappings for all matching existing index templates."""
80+ existing_mappings : dict [str , Any ] = {}
81+ index_lookup : dict [str , Any ] = {}
82+ for index in indices :
83+ index_tmpl_mappings = get_simulated_index_template_mappings (elastic_client , index )
84+ index_lookup [index ] = index_tmpl_mappings
85+ combine_dicts (existing_mappings , index_tmpl_mappings )
86+ return existing_mappings , index_lookup
87+
88+
89+ def get_simulated_index_template_mappings (elastic_client : Elasticsearch , name : str ) -> dict [str , Any ]:
90+ """
91+ Return the mappings from the index configuration that would be applied
92+ to the specified index from an existing index template
93+
94+ https://elasticsearch-py.readthedocs.io/en/stable/api/indices.html#elasticsearch.client.IndicesClient.simulate_index_template
95+ """
96+ template = elastic_client .indices .simulate_index_template (name = name )
97+ if not template :
98+ return {}
99+ return template ["template" ]["mappings" ]["properties" ]
100+
101+
102+ def prepare_integration_mappings ( # noqa: PLR0913
38103 rule_integrations : list [str ],
39104 event_dataset_integrations : list [utils .EventDataset ],
40105 package_manifests : Any ,
@@ -97,14 +162,14 @@ def create_remote_indices(
97162 """Create remote indices for validation and return the index string."""
98163 suffix = str (int (time .time () * 1000 ))
99164 test_index = f"rule-test-index-{ suffix } "
100- response = misc . create_index_with_index_mapping (elastic_client , test_index , existing_mappings )
165+ response = create_index_with_index_mapping (elastic_client , test_index , existing_mappings )
101166 log (f"Index `{ test_index } ` created: { response } " )
102167 full_index_str = test_index
103168
104169 # create all integration indices
105170 for index , properties in index_lookup .items ():
106171 ind_index_str = f"test-{ index .rstrip ('*' )} { suffix } "
107- response = misc . create_index_with_index_mapping (elastic_client , ind_index_str , properties )
172+ response = create_index_with_index_mapping (elastic_client , ind_index_str , properties )
108173 log (f"Index `{ ind_index_str } ` created: { response } " )
109174 full_index_str = f"{ full_index_str } , { ind_index_str } "
110175
@@ -124,8 +189,13 @@ def execute_query_against_indices(
124189 response = elastic_client .esql .query (query = query )
125190 log (f"Got query response: { response } " )
126191 query_columns = response .get ("columns" , [])
192+ except BadRequestError as e :
193+ error_msg = str (e )
194+ if "parsing_exception" in error_msg :
195+ raise EsqlSyntaxError (str (e ), elastic_client ) from e
196+ raise EsqlSemanticError (str (e ), elastic_client ) from e
127197 finally :
128- if delete_indices :
198+ if delete_indices or misc . getdefault ( "skip_empty_index_cleanup" )() :
129199 for index_str in test_index_str .split ("," ):
130200 response = elastic_client .indices .delete (index = index_str .strip ())
131201 log (f"Test index `{ index_str } ` deleted: { response } " )
@@ -182,7 +252,7 @@ def get_ecs_schema_mappings(current_version: Version) -> dict[str, Any]:
182252 return ecs_schema
183253
184254
185- def prepare_mappings (
255+ def prepare_mappings ( # noqa: PLR0913
186256 elastic_client : Elasticsearch ,
187257 indices : list [str ],
188258 event_dataset_integrations : list [utils .EventDataset ],
@@ -191,7 +261,7 @@ def prepare_mappings(
191261 log : Callable [[str ], None ],
192262) -> tuple [dict [str , Any ], dict [str , Any ], dict [str , Any ]]:
193263 """Prepare index mappings for the given indices and rule integrations."""
194- existing_mappings , index_lookup = misc . get_existing_mappings (elastic_client , indices )
264+ existing_mappings , index_lookup = get_existing_mappings (elastic_client , indices )
195265
196266 # Collect mappings for the integrations
197267 rule_integrations = get_rule_integrations (metadata )
0 commit comments