22from typing import Literal , NotRequired , TypedDict
33
44from sentry .api import event_search
5- from sentry .api .event_search import ParenExpression , QueryToken , SearchFilter
5+ from sentry .api .event_search import ParenExpression , QueryToken , SearchFilter , SearchValue
66from sentry .relay .types import RuleCondition
77from sentry .sentry_metrics .extraction_rules import MetricsExtractionRule
88from sentry .snuba .metrics .extraction import SearchQueryConverter , TagSpec
7070
7171
7272class SpanAttributeMetricSpec (TypedDict ):
73+ """
74+ Represents a metric extraction rule to that extracts metrics from span attributes.
75+ """
76+
7377 category : Literal ["span" ]
7478 mri : str
7579 field : NotRequired [str | None ]
@@ -78,46 +82,50 @@ class SpanAttributeMetricSpec(TypedDict):
7882
7983
8084def convert_to_metric_spec (extraction_rule : MetricsExtractionRule ) -> SpanAttributeMetricSpec :
85+ """
86+ Converts a persisted MetricsExtractionRule into a SpanAttributeMetricSpec that satisfies
87+ MetricSpec of relay metric extraction config.
88+ """
8189
82- field = _get_field (extraction_rule )
83-
84- # TODO(metrics): simplify MetricsExtractionRule in a follwup PR
85- parsed_conditions = _parse_conditions ([extraction_rule .condition ])
90+ parsed_search_query = event_search .parse_search_query (extraction_rule .condition )
91+ extended_search_query = _extend_search_query (parsed_search_query )
8692
8793 return {
8894 "category" : "span" ,
8995 "mri" : extraction_rule .generate_mri (),
90- "field" : field ,
91- "tags" : _get_tags (extraction_rule , parsed_conditions ),
92- "condition" : _get_rule_condition (extraction_rule , parsed_conditions ),
96+ "field" : _get_field ( extraction_rule ) ,
97+ "tags" : _get_tags (extraction_rule , parsed_search_query ),
98+ "condition" : _get_rule_condition (extraction_rule , extended_search_query ),
9399 }
94100
95101
96- def _get_field (extraction_rule : MetricsExtractionRule ) -> str | None :
97- if _is_counter (extraction_rule ):
98- return None
99-
100- return _map_span_attribute_name (extraction_rule .span_attribute )
102+ # Tag extraction functions
101103
102104
103105def _get_tags (
104- extraction_rule : MetricsExtractionRule , conditions : Sequence [QueryToken ] | None
106+ extraction_rule : MetricsExtractionRule , parsed_search_query : Sequence [QueryToken ] | None
105107) -> list [TagSpec ]:
106108 """
107- Merges the explicitly defined tags with the tags extracted from the search conditions .
109+ Merges the explicitly defined tags with the tags extracted from the search query .
108110 """
109- token_list = _flatten_query_tokens (conditions ) if conditions else []
111+
112+ token_list = _flatten_query_tokens (parsed_search_query ) if parsed_search_query else []
110113 search_token_keys = {token .key .name for token in token_list }
111114
112115 tag_keys = extraction_rule .tags .union (search_token_keys )
113116
114117 return [TagSpec (key = key , field = _map_span_attribute_name (key )) for key in sorted (tag_keys )]
115118
116119
117- def _flatten_query_tokens (conditions : Sequence [QueryToken ]) -> list [SearchFilter ]:
120+ def _flatten_query_tokens (parsed_search_query : Sequence [QueryToken ]) -> list [SearchFilter ]:
121+ """
122+ Takes a parsed search query and flattens it into a list of SearchFilter tokens.
123+ Removes any parenthesis and boolean operators.
124+ """
125+
118126 query_tokens : list [SearchFilter ] = []
119127
120- for token in conditions :
128+ for token in parsed_search_query :
121129 if isinstance (token , SearchFilter ):
122130 query_tokens .append (token )
123131 elif isinstance (token , ParenExpression ):
@@ -126,42 +134,100 @@ def _flatten_query_tokens(conditions: Sequence[QueryToken]) -> list[SearchFilter
126134 return query_tokens
127135
128136
129- def _parse_conditions (conditions : Sequence [str ] | None ) -> Sequence [QueryToken ]:
130- if not conditions :
131- return []
137+ # Condition string parsing and transformation functions
132138
133- non_empty_conditions = [condition for condition in conditions if condition ]
134139
135- search_query = " or " .join ([f"({ condition } )" for condition in non_empty_conditions ])
136- return event_search .parse_search_query (search_query )
140+ def _extend_search_query (parsed_search_query : Sequence [QueryToken ]) -> Sequence [QueryToken ]:
141+ return _visit_numeric_tokens (parsed_search_query )
142+
143+
144+ def _visit_numeric_tokens (parsed_search_query : Sequence [QueryToken ]) -> list [QueryToken ]:
145+ """
146+ Visits each token in the parsed search query and converts numeric tokens into paren expressions.
147+ """
148+
149+ query_tokens : list [QueryToken ] = []
150+
151+ for token in parsed_search_query :
152+ if isinstance (token , SearchFilter ):
153+ query_tokens .append (_extend_numeric_token (token ))
154+ elif isinstance (token , ParenExpression ):
155+ query_tokens = query_tokens + _visit_numeric_tokens (token .children )
156+ else :
157+ query_tokens .append (token )
158+
159+ return query_tokens
160+
161+
162+ def _extend_numeric_token (token : SearchFilter ) -> ParenExpression | SearchFilter :
163+ """
164+ Since all search filter values are parsed as strings by default, we need to make sure that
165+ numeric values are treated as such when constructing the rule condition. This function
166+ expands the original token into a paren expression if the value is a numeric string.
167+
168+ Example:
169+ `key:'123'` -> `key:'123' OR key:123`
170+ `key:['123', '456']` -> `key:['123', '456'] OR key:[123, 456]`
171+ """
172+
173+ if token .operator == "=" or token .operator == "!=" :
174+ if not str (token .value .value ).isdigit ():
175+ return token
176+
177+ numeric_value_token = SearchFilter (
178+ key = token .key , operator = token .operator , value = SearchValue (int (token .value .value ))
179+ )
180+
181+ elif token .is_in_filter :
182+ str_values = [str (value ) for value in token .value .value ]
183+ if not all (value .isdigit () for value in str_values ):
184+ return token
185+
186+ numeric_values = [int (value ) for value in str_values ]
187+ numeric_value_token = SearchFilter (
188+ key = token .key , operator = token .operator , value = SearchValue (numeric_values )
189+ )
190+
191+ return ParenExpression (
192+ children = [
193+ token ,
194+ "OR" ,
195+ numeric_value_token ,
196+ ]
197+ )
198+
199+
200+ # Conversion to RuleCondition functions
137201
138202
139203def _get_rule_condition (
140- extraction_rule : MetricsExtractionRule , parsed_conditions : Sequence [QueryToken ]
204+ extraction_rule : MetricsExtractionRule , parsed_search_query : Sequence [QueryToken ]
141205) -> RuleCondition | None :
142206 if _is_counter (extraction_rule ):
143- return _get_counter_rule_condition (extraction_rule , parsed_conditions )
207+ return _get_counter_rule_condition (extraction_rule , parsed_search_query )
144208
145- if not parsed_conditions :
209+ if not parsed_search_query :
146210 return None
147211
148- return SearchQueryConverter (parsed_conditions , field_mapper = _map_span_attribute_name ).convert ()
212+ return SearchQueryConverter (
213+ parsed_search_query , field_mapper = _map_span_attribute_name
214+ ).convert ()
149215
150216
151217def _get_counter_rule_condition (
152- extraction_rule : MetricsExtractionRule , parsed_conditions : Sequence [QueryToken ]
218+ extraction_rule : MetricsExtractionRule , parsed_search_query : Sequence [QueryToken ]
153219) -> RuleCondition | None :
154220 is_top_level = extraction_rule .span_attribute in _TOP_LEVEL_SPAN_ATTRIBUTES
155221
156- if not parsed_conditions :
157- # temporary workaround for span.duration counter metric
222+ if not parsed_search_query :
223+ # workaround for span.duration and other top level attributes that are always present
158224 if is_top_level :
159225 return None
160226
161227 return _get_exists_condition (extraction_rule .span_attribute )
162228
163229 condition_dict = SearchQueryConverter (
164- parsed_conditions , field_mapper = _map_span_attribute_name
230+ parsed_search_query , field_mapper = _map_span_attribute_name
165231 ).convert ()
166232
167233 if is_top_level :
@@ -194,6 +260,9 @@ def _get_exists_condition(span_attribute: str) -> RuleCondition:
194260 }
195261
196262
263+ # General helpers
264+
265+
197266def _map_span_attribute_name (span_attribute : str ) -> str :
198267 if span_attribute in _TOP_LEVEL_SPAN_ATTRIBUTES :
199268 return span_attribute
@@ -208,3 +277,10 @@ def _map_span_attribute_name(span_attribute: str) -> str:
208277
209278def _is_counter (extraction_rule : MetricsExtractionRule ) -> bool :
210279 return extraction_rule .type == "c"
280+
281+
282+ def _get_field (extraction_rule : MetricsExtractionRule ) -> str | None :
283+ if _is_counter (extraction_rule ):
284+ return None
285+
286+ return _map_span_attribute_name (extraction_rule .span_attribute )
0 commit comments