Browse Source

feat(metrics): extend counter condtion (#73427)

Ogi 8 months ago
parent
commit
54c9857428

+ 47 - 11
src/sentry/snuba/metrics/span_attribute_extraction.py

@@ -1,5 +1,5 @@
 from collections.abc import Sequence
-from typing import Any, Literal, NotRequired, TypedDict
+from typing import Literal, NotRequired, TypedDict
 
 from sentry.api import event_search
 from sentry.api.event_search import ParenExpression, QueryToken, SearchFilter
@@ -85,20 +85,20 @@ def convert_to_metric_spec(extraction_rule: MetricsExtractionRule) -> SpanAttrib
         "category": "span",
         "mri": extraction_rule.generate_mri(),
         "field": field,
-        "tags": _get_tags(extraction_rule.tags, parsed_conditions),
-        "condition": _get_rule_condition(parsed_conditions),
+        "tags": _get_tags(extraction_rule, parsed_conditions),
+        "condition": _get_rule_condition(extraction_rule, parsed_conditions),
     }
 
 
 def _get_field(extraction_rule: MetricsExtractionRule) -> str | None:
-    if extraction_rule.type == "c":
+    if _is_counter(extraction_rule):
         return None
 
     return _map_span_attribute_name(extraction_rule.span_attribute)
 
 
 def _get_tags(
-    explicitly_defined_tags: set[str], conditions: Sequence[QueryToken] | None
+    extraction_rule: MetricsExtractionRule, conditions: Sequence[QueryToken] | None
 ) -> list[TagSpec]:
     """
     Merges the explicitly defined tags with the tags extracted from the search conditions.
@@ -106,7 +106,7 @@ def _get_tags(
     token_list = _flatten_query_tokens(conditions) if conditions else []
     search_token_keys = {token.key.name for token in token_list}
 
-    tag_keys = explicitly_defined_tags.union(search_token_keys)
+    tag_keys = extraction_rule.tags.union(search_token_keys)
 
     return [TagSpec(key=key, field=_map_span_attribute_name(key)) for key in sorted(tag_keys)]
 
@@ -133,14 +133,46 @@ def _parse_conditions(conditions: Sequence[str] | None) -> Sequence[QueryToken]:
     return event_search.parse_search_query(search_query)
 
 
-def _get_rule_condition(parsed_search_query: Sequence[Any] | None) -> RuleCondition | None:
-    if not parsed_search_query:
-        return None
+def _get_rule_condition(
+    extraction_rule: MetricsExtractionRule, parsed_conditions: Sequence[QueryToken]
+) -> RuleCondition | None:
+    if not parsed_conditions:
+        if not _is_counter(extraction_rule):
+            return None
+
+        return _get_exists_condition(extraction_rule.span_attribute)
 
-    return SearchQueryConverter(
-        parsed_search_query, field_mapper=_map_span_attribute_name
+    condition_dict = SearchQueryConverter(
+        parsed_conditions, field_mapper=_map_span_attribute_name
     ).convert()
 
+    return (
+        _append_exists_condition(condition_dict, extraction_rule.span_attribute)
+        if _is_counter(extraction_rule)
+        else condition_dict
+    )
+
+
+def _append_exists_condition(rule_condition: RuleCondition, span_attribute: str) -> RuleCondition:
+    return {
+        "op": "and",
+        "inner": [
+            rule_condition,
+            _get_exists_condition(span_attribute),
+        ],
+    }
+
+
+def _get_exists_condition(span_attribute: str) -> RuleCondition:
+    return {
+        "op": "not",
+        "inner": {
+            "name": _map_span_attribute_name(span_attribute),
+            "op": "eq",
+            "value": None,
+        },
+    }
+
 
 def _map_span_attribute_name(span_attribute: str) -> str:
     if span_attribute in _TOP_LEVEL_SPAN_ATTRIBUTES:
@@ -152,3 +184,7 @@ def _map_span_attribute_name(span_attribute: str) -> str:
     sanitized_span_attr = span_attribute.replace(".", "\\.")
 
     return f"span.data.{sanitized_span_attr}"
+
+
+def _is_counter(extraction_rule: MetricsExtractionRule) -> bool:
+    return extraction_rule.type == "c"

+ 4 - 1
tests/sentry/relay/config/test_metric_extraction.py

@@ -2168,7 +2168,10 @@ def test_get_span_attribute_metrics(default_project: Project) -> None:
             },
             {
                 "category": "span",
-                "condition": None,
+                "condition": {
+                    "op": "not",
+                    "inner": {"name": "span.duration", "op": "eq", "value": None},
+                },
                 "field": None,
                 "mri": "c:custom/span.duration@none",
                 "tags": [],

+ 23 - 2
tests/sentry/snuba/metrics/test_span_attribute_extraction.py

@@ -153,12 +153,33 @@ def test_counter():
 
     assert not metric_spec["field"]
     assert metric_spec["mri"] == "c:custom/foobar@none"
-    assert metric_spec["tags"] == []
+    assert metric_spec["condition"] == {
+        "inner": {"name": "span.data.foobar", "op": "eq", "value": None},
+        "op": "not",
+    }
+
+
+def test_counter_extends_conditions():
+    rule = MetricsExtractionRule(
+        span_attribute="foobar", type="c", unit="none", tags=set(), conditions=["abc:xyz"]
+    )
+
+    metric_spec = convert_to_metric_spec(rule)
+
+    assert not metric_spec["field"]
+    assert metric_spec["mri"] == "c:custom/foobar@none"
+    assert metric_spec["condition"] == {
+        "op": "and",
+        "inner": [
+            {"op": "eq", "name": "span.data.abc", "value": "xyz"},
+            {"inner": {"name": "span.data.foobar", "op": "eq", "value": None}, "op": "not"},
+        ],
+    }
 
 
 def test_empty_conditions():
     rule = MetricsExtractionRule(
-        span_attribute="foobar", type="c", unit="none", tags=set(), conditions=[""]
+        span_attribute="foobar", type="d", unit="none", tags=set(), conditions=[""]
     )
 
     metric_spec = convert_to_metric_spec(rule)