|
@@ -1,3 +1,4 @@
|
|
|
+from abc import ABC, abstractmethod
|
|
|
from datetime import timedelta
|
|
|
|
|
|
import sentry_sdk
|
|
@@ -6,6 +7,7 @@ from rest_framework import serializers
|
|
|
from rest_framework.exceptions import ParseError
|
|
|
from rest_framework.request import Request
|
|
|
from rest_framework.response import Response
|
|
|
+from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey
|
|
|
from sentry_protos.snuba.v1alpha.endpoint_tags_list_pb2 import (
|
|
|
AttributeValuesRequest,
|
|
|
AttributeValuesResponse,
|
|
@@ -13,7 +15,7 @@ from sentry_protos.snuba.v1alpha.endpoint_tags_list_pb2 import (
|
|
|
TraceItemAttributesResponse,
|
|
|
)
|
|
|
from sentry_protos.snuba.v1alpha.request_common_pb2 import RequestMeta, TraceItemName
|
|
|
-from sentry_protos.snuba.v1alpha.trace_item_attribute_pb2 import AttributeKey
|
|
|
+from sentry_protos.snuba.v1alpha.trace_item_attribute_pb2 import AttributeKey as AlphaAttributeKey
|
|
|
from sentry_relay.consts import SPAN_STATUS_CODE_TO_NAME
|
|
|
from snuba_sdk import Condition, Op
|
|
|
|
|
@@ -26,6 +28,9 @@ from sentry.api.event_search import translate_escape_sequences
|
|
|
from sentry.api.paginator import ChainPaginator
|
|
|
from sentry.api.serializers import serialize
|
|
|
from sentry.api.utils import handle_query_errors
|
|
|
+from sentry.models.organization import Organization
|
|
|
+from sentry.search.eap.spans import SearchResolver
|
|
|
+from sentry.search.eap.types import SearchResolverConfig
|
|
|
from sentry.search.events.builder.base import BaseQueryBuilder
|
|
|
from sentry.search.events.builder.spans_indexed import SpansIndexedQueryBuilder
|
|
|
from sentry.search.events.types import QueryBuilderConfig, SnubaParams
|
|
@@ -60,9 +65,9 @@ class OrganizationSpansFieldsEndpointSerializer(serializers.Serializer):
|
|
|
|
|
|
def validate_type(self, value):
|
|
|
if value == "string":
|
|
|
- return AttributeKey.Type.TYPE_STRING
|
|
|
+ return AlphaAttributeKey.Type.TYPE_STRING
|
|
|
if value == "number":
|
|
|
- return AttributeKey.Type.TYPE_FLOAT
|
|
|
+ return AlphaAttributeKey.Type.TYPE_FLOAT
|
|
|
raise NotImplementedError
|
|
|
|
|
|
def validate(self, attrs):
|
|
@@ -75,7 +80,7 @@ class OrganizationSpansFieldsEndpointSerializer(serializers.Serializer):
|
|
|
class OrganizationSpansFieldsEndpoint(OrganizationSpansFieldsEndpointBase):
|
|
|
snuba_methods = ["GET"]
|
|
|
|
|
|
- def get(self, request: Request, organization) -> Response:
|
|
|
+ def get(self, request: Request, organization: Organization) -> Response:
|
|
|
if not features.has(
|
|
|
"organizations:performance-trace-explorer", organization, actor=request.user
|
|
|
):
|
|
@@ -187,7 +192,7 @@ class OrganizationSpansFieldsEndpoint(OrganizationSpansFieldsEndpointBase):
|
|
|
class OrganizationSpansFieldValuesEndpoint(OrganizationSpansFieldsEndpointBase):
|
|
|
snuba_methods = ["GET"]
|
|
|
|
|
|
- def get(self, request: Request, organization, key: str) -> Response:
|
|
|
+ def get(self, request: Request, organization: Organization, key: str) -> Response:
|
|
|
if not features.has(
|
|
|
"organizations:performance-trace-explorer", organization, actor=request.user
|
|
|
):
|
|
@@ -210,69 +215,27 @@ class OrganizationSpansFieldValuesEndpoint(OrganizationSpansFieldsEndpointBase):
|
|
|
return Response(serializer.errors, status=400)
|
|
|
serialized = serializer.validated_data
|
|
|
|
|
|
+ executor: BaseSpanFieldValuesAutocompletionExecutor
|
|
|
+
|
|
|
if serialized["dataset"] == "spans" and features.has(
|
|
|
"organizations:visibility-explore-dataset", organization, actor=request.user
|
|
|
):
|
|
|
- start_timestamp = Timestamp()
|
|
|
- start_timestamp.FromDatetime(
|
|
|
- snuba_params.start_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
- )
|
|
|
-
|
|
|
- end_timestamp = Timestamp()
|
|
|
- end_timestamp.FromDatetime(
|
|
|
- snuba_params.end_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
- + timedelta(days=1)
|
|
|
- )
|
|
|
-
|
|
|
- query = translate_escape_sequences(request.GET.get("query", ""))
|
|
|
- rpc_request = AttributeValuesRequest(
|
|
|
- meta=RequestMeta(
|
|
|
- organization_id=organization.id,
|
|
|
- cogs_category="performance",
|
|
|
- referrer=Referrer.API_SPANS_TAG_VALUES_RPC.value,
|
|
|
- project_ids=snuba_params.project_ids,
|
|
|
- start_timestamp=start_timestamp,
|
|
|
- end_timestamp=end_timestamp,
|
|
|
- trace_item_name=TraceItemName.TRACE_ITEM_NAME_EAP_SPANS,
|
|
|
- ),
|
|
|
- name=key,
|
|
|
- value_substring_match=query,
|
|
|
- limit=max_span_tag_values,
|
|
|
- offset=0,
|
|
|
- )
|
|
|
- rpc_response = snuba_rpc.rpc(rpc_request, AttributeValuesResponse)
|
|
|
-
|
|
|
- paginator = ChainPaginator(
|
|
|
- [
|
|
|
- [
|
|
|
- TagValue(
|
|
|
- key=key,
|
|
|
- value=tag_value,
|
|
|
- times_seen=None,
|
|
|
- first_seen=None,
|
|
|
- last_seen=None,
|
|
|
- )
|
|
|
- for tag_value in rpc_response.values
|
|
|
- if tag_value
|
|
|
- ]
|
|
|
- ],
|
|
|
- max_limit=max_span_tag_values,
|
|
|
+ executor = EAPSpanFieldValuesAutocompletionExecutor(
|
|
|
+ organization=organization,
|
|
|
+ snuba_params=snuba_params,
|
|
|
+ key=key,
|
|
|
+ query=request.GET.get("query"),
|
|
|
+ max_span_tag_values=max_span_tag_values,
|
|
|
)
|
|
|
-
|
|
|
- return self.paginate(
|
|
|
- request=request,
|
|
|
- paginator=paginator,
|
|
|
- on_results=lambda results: serialize(results, request.user),
|
|
|
- default_per_page=max_span_tag_values,
|
|
|
- max_per_page=max_span_tag_values,
|
|
|
+ else:
|
|
|
+ executor = SpanFieldValuesAutocompletionExecutor(
|
|
|
+ organization=organization,
|
|
|
+ snuba_params=snuba_params,
|
|
|
+ key=key,
|
|
|
+ query=request.GET.get("query"),
|
|
|
+ max_span_tag_values=max_span_tag_values,
|
|
|
)
|
|
|
|
|
|
- executor = SpanFieldValuesAutocompletionExecutor(
|
|
|
- snuba_params=snuba_params,
|
|
|
- key=key,
|
|
|
- query=request.GET.get("query"),
|
|
|
- max_span_tag_values=max_span_tag_values,
|
|
|
- )
|
|
|
tag_values = executor.execute()
|
|
|
|
|
|
tag_values.sort(key=lambda tag: tag.value)
|
|
@@ -288,62 +251,27 @@ class OrganizationSpansFieldValuesEndpoint(OrganizationSpansFieldsEndpointBase):
|
|
|
)
|
|
|
|
|
|
|
|
|
-class SpanFieldValuesAutocompletionExecutor:
|
|
|
- ID_KEYS = {
|
|
|
- "id",
|
|
|
- "span_id",
|
|
|
- "parent_span",
|
|
|
- "parent_span_id",
|
|
|
- "trace",
|
|
|
- "trace_id",
|
|
|
- "transaction.id",
|
|
|
- "transaction_id",
|
|
|
- "segment.id",
|
|
|
- "segment_id",
|
|
|
- "profile.id",
|
|
|
- "profile_id",
|
|
|
- "replay.id",
|
|
|
- "replay_id",
|
|
|
- }
|
|
|
- NUMERIC_KEYS = {"span.duration", "span.self_time"}
|
|
|
- TIMESTAMP_KEYS = {"timestamp"}
|
|
|
+class BaseSpanFieldValuesAutocompletionExecutor(ABC):
|
|
|
PROJECT_SLUG_KEYS = {"project", "project.name"}
|
|
|
PROJECT_ID_KEYS = {"project.id"}
|
|
|
- SPAN_STATUS_KEYS = {"span.status"}
|
|
|
|
|
|
def __init__(
|
|
|
self,
|
|
|
+ organization: Organization,
|
|
|
snuba_params: SnubaParams,
|
|
|
key: str,
|
|
|
query: str | None,
|
|
|
max_span_tag_values: int,
|
|
|
):
|
|
|
+ self.organization = organization
|
|
|
self.snuba_params = snuba_params
|
|
|
self.key = key
|
|
|
- self.query = query
|
|
|
+ self.query = query or ""
|
|
|
self.max_span_tag_values = max_span_tag_values
|
|
|
|
|
|
+ @abstractmethod
|
|
|
def execute(self) -> list[TagValue]:
|
|
|
- if (
|
|
|
- self.key in self.NUMERIC_KEYS
|
|
|
- or self.key in self.ID_KEYS
|
|
|
- or self.key in self.TIMESTAMP_KEYS
|
|
|
- ):
|
|
|
- return self.noop_autocomplete_function()
|
|
|
-
|
|
|
- if self.key in self.PROJECT_ID_KEYS:
|
|
|
- return self.project_id_autocomplete_function()
|
|
|
-
|
|
|
- if self.key in self.PROJECT_SLUG_KEYS:
|
|
|
- return self.project_slug_autocomplete_function()
|
|
|
-
|
|
|
- if self.key in self.SPAN_STATUS_KEYS:
|
|
|
- return self.span_status_autocomplete_function()
|
|
|
-
|
|
|
- return self.default_autocomplete_function()
|
|
|
-
|
|
|
- def noop_autocomplete_function(self) -> list[TagValue]:
|
|
|
- return []
|
|
|
+ raise NotImplementedError
|
|
|
|
|
|
def project_id_autocomplete_function(self) -> list[TagValue]:
|
|
|
return [
|
|
@@ -371,6 +299,50 @@ class SpanFieldValuesAutocompletionExecutor:
|
|
|
if not self.query or self.query in project.slug
|
|
|
]
|
|
|
|
|
|
+
|
|
|
+class SpanFieldValuesAutocompletionExecutor(BaseSpanFieldValuesAutocompletionExecutor):
|
|
|
+ ID_KEYS = {
|
|
|
+ "id",
|
|
|
+ "span_id",
|
|
|
+ "parent_span",
|
|
|
+ "parent_span_id",
|
|
|
+ "trace",
|
|
|
+ "trace_id",
|
|
|
+ "transaction.id",
|
|
|
+ "transaction_id",
|
|
|
+ "segment.id",
|
|
|
+ "segment_id",
|
|
|
+ "profile.id",
|
|
|
+ "profile_id",
|
|
|
+ "replay.id",
|
|
|
+ "replay_id",
|
|
|
+ }
|
|
|
+ NUMERIC_KEYS = {"span.duration", "span.self_time"}
|
|
|
+ TIMESTAMP_KEYS = {"timestamp"}
|
|
|
+ SPAN_STATUS_KEYS = {"span.status"}
|
|
|
+
|
|
|
+ def execute(self) -> list[TagValue]:
|
|
|
+ if (
|
|
|
+ self.key in self.NUMERIC_KEYS
|
|
|
+ or self.key in self.ID_KEYS
|
|
|
+ or self.key in self.TIMESTAMP_KEYS
|
|
|
+ ):
|
|
|
+ return self.noop_autocomplete_function()
|
|
|
+
|
|
|
+ if self.key in self.PROJECT_ID_KEYS:
|
|
|
+ return self.project_id_autocomplete_function()
|
|
|
+
|
|
|
+ if self.key in self.PROJECT_SLUG_KEYS:
|
|
|
+ return self.project_slug_autocomplete_function()
|
|
|
+
|
|
|
+ if self.key in self.SPAN_STATUS_KEYS:
|
|
|
+ return self.span_status_autocomplete_function()
|
|
|
+
|
|
|
+ return self.default_autocomplete_function()
|
|
|
+
|
|
|
+ def noop_autocomplete_function(self) -> list[TagValue]:
|
|
|
+ return []
|
|
|
+
|
|
|
def span_status_autocomplete_function(self) -> list[TagValue]:
|
|
|
query = self.get_autocomplete_query_base()
|
|
|
|
|
@@ -424,3 +396,78 @@ class SpanFieldValuesAutocompletionExecutor:
|
|
|
for row in results["data"]
|
|
|
if row[self.key] is not None
|
|
|
]
|
|
|
+
|
|
|
+
|
|
|
+class EAPSpanFieldValuesAutocompletionExecutor(BaseSpanFieldValuesAutocompletionExecutor):
|
|
|
+ def __init__(
|
|
|
+ self,
|
|
|
+ organization: Organization,
|
|
|
+ snuba_params: SnubaParams,
|
|
|
+ key: str,
|
|
|
+ query: str | None,
|
|
|
+ max_span_tag_values: int,
|
|
|
+ ):
|
|
|
+ super().__init__(organization, snuba_params, key, query, max_span_tag_values)
|
|
|
+ self.attribute_key = self.resolve_attribute_key(key, snuba_params)
|
|
|
+
|
|
|
+ def resolve_attribute_key(self, key: str, snuba_params: SnubaParams) -> AttributeKey | None:
|
|
|
+ resolver = SearchResolver(params=snuba_params, config=SearchResolverConfig())
|
|
|
+ resolved, _ = resolver.resolve_attribute(key)
|
|
|
+ proto = resolved.proto_definition
|
|
|
+ if not isinstance(proto, AttributeKey):
|
|
|
+ return None
|
|
|
+ return proto
|
|
|
+
|
|
|
+ def execute(self) -> list[TagValue]:
|
|
|
+ if self.key in self.PROJECT_ID_KEYS:
|
|
|
+ return self.project_id_autocomplete_function()
|
|
|
+
|
|
|
+ if self.key in self.PROJECT_SLUG_KEYS:
|
|
|
+ return self.project_slug_autocomplete_function()
|
|
|
+
|
|
|
+ return self.default_autocomplete_function()
|
|
|
+
|
|
|
+ def default_autocomplete_function(self) -> list[TagValue]:
|
|
|
+ if self.attribute_key is None:
|
|
|
+ return []
|
|
|
+
|
|
|
+ start_timestamp = Timestamp()
|
|
|
+ start_timestamp.FromDatetime(
|
|
|
+ self.snuba_params.start_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
+ )
|
|
|
+
|
|
|
+ end_timestamp = Timestamp()
|
|
|
+ end_timestamp.FromDatetime(
|
|
|
+ self.snuba_params.end_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
+ + timedelta(days=1)
|
|
|
+ )
|
|
|
+
|
|
|
+ query = translate_escape_sequences(self.query)
|
|
|
+ rpc_request = AttributeValuesRequest(
|
|
|
+ meta=RequestMeta(
|
|
|
+ organization_id=self.organization.id,
|
|
|
+ cogs_category="performance",
|
|
|
+ referrer=Referrer.API_SPANS_TAG_VALUES_RPC.value,
|
|
|
+ project_ids=self.snuba_params.project_ids,
|
|
|
+ start_timestamp=start_timestamp,
|
|
|
+ end_timestamp=end_timestamp,
|
|
|
+ trace_item_name=TraceItemName.TRACE_ITEM_NAME_EAP_SPANS,
|
|
|
+ ),
|
|
|
+ name=self.attribute_key.name,
|
|
|
+ value_substring_match=query,
|
|
|
+ limit=self.max_span_tag_values,
|
|
|
+ offset=0,
|
|
|
+ )
|
|
|
+ rpc_response = snuba_rpc.rpc(rpc_request, AttributeValuesResponse)
|
|
|
+
|
|
|
+ return [
|
|
|
+ TagValue(
|
|
|
+ key=self.key,
|
|
|
+ value=tag_value,
|
|
|
+ times_seen=None,
|
|
|
+ first_seen=None,
|
|
|
+ last_seen=None,
|
|
|
+ )
|
|
|
+ for tag_value in rpc_response.values
|
|
|
+ if tag_value
|
|
|
+ ]
|