Skip to content

feat: Expose sentry conventions in UI #92370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions src/sentry/api/endpoints/organization_trace_item_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
from sentry.search.eap.resolver import SearchResolver
from sentry.search.eap.spans.definitions import SPAN_DEFINITIONS
from sentry.search.eap.types import SearchResolverConfig, SupportedTraceItemType
from sentry.search.eap.utils import can_expose_attribute, translate_internal_to_public_alias
from sentry.search.eap.utils import (
can_expose_attribute,
is_sentry_convention_replacement_attribute,
translate_internal_to_public_alias,
translate_to_sentry_conventions,
)
from sentry.search.events.types import SnubaParams
from sentry.snuba.referrer import Referrer
from sentry.tagstore.types import TagValue
Expand Down Expand Up @@ -169,6 +174,12 @@ def get(self, request: Request, organization: Organization) -> Response:
paginator=ChainPaginator([]),
)

use_sentry_conventions = features.has(
"organizations:performance-sentry-conventions-fields", organization, actor=request.user
)

sentry_sdk.set_tag("feature.use_sentry_conventions", use_sentry_conventions)

serialized = serializer.validated_data
substring_match = serialized.get("substring_match", "")
query_string = serialized.get("query")
Expand All @@ -183,7 +194,7 @@ def get(self, request: Request, organization: Organization) -> Response:
resolver = SearchResolver(
params=snuba_params, config=SearchResolverConfig(), definitions=column_definitions
)
filter, _, _ = resolver.resolve_query(query_string)
query_filter, _, _ = resolver.resolve_query(query_string)
meta = resolver.resolve_meta(referrer=referrer.value)
meta.trace_item_type = constants.SUPPORTED_TRACE_ITEM_TYPE_MAP.get(
trace_item_type, ProtoTraceItemType.TRACE_ITEM_TYPE_SPAN
Expand All @@ -208,17 +219,48 @@ def data_fn(offset: int, limit: int):
page_token=PageToken(offset=offset),
type=attr_type,
value_substring_match=value_substring_match,
intersecting_attributes_filter=filter,
intersecting_attributes_filter=query_filter,
)

with handle_query_errors():
rpc_response = snuba_rpc.attribute_names_rpc(rpc_request)

return [
as_attribute_key(attribute.name, serialized["attribute_type"], trace_item_type)
for attribute in rpc_response.attributes
if attribute.name and can_expose_attribute(attribute.name, trace_item_type)
]
if use_sentry_conventions:
attribute_keys = {}
for attribute in rpc_response.attributes:
if attribute.name and can_expose_attribute(attribute.name, trace_item_type):
attr_key = as_attribute_key(
attribute.name, serialized["attribute_type"], trace_item_type
)
public_alias = attr_key["name"]
replacement = translate_to_sentry_conventions(public_alias, trace_item_type)
if public_alias != replacement:
attr_key = as_attribute_key(
replacement, serialized["attribute_type"], trace_item_type
)

attribute_keys[attr_key["name"]] = attr_key

attributes = list(attribute_keys.values())
sentry_sdk.set_context("api_response", {"attributes": attributes})
return attributes

attributes = list(
filter(
lambda x: not is_sentry_convention_replacement_attribute(
x["name"], trace_item_type
),
[
as_attribute_key(
attribute.name, serialized["attribute_type"], trace_item_type
)
for attribute in rpc_response.attributes
if attribute.name and can_expose_attribute(attribute.name, trace_item_type)
],
)
)
sentry_sdk.set_context("api_response", {"attributes": attributes})
return attributes

return self.paginate(
request=request,
Expand Down
31 changes: 29 additions & 2 deletions src/sentry/api/endpoints/project_trace_item_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from sentry_protos.snuba.v1.endpoint_trace_item_details_pb2 import TraceItemDetailsRequest
from sentry_protos.snuba.v1.request_common_pb2 import RequestMeta

from sentry import features
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import region_silo_endpoint
Expand All @@ -18,16 +19,23 @@
from sentry.models.project import Project
from sentry.search.eap import constants
from sentry.search.eap.types import SupportedTraceItemType, TraceItemAttribute
from sentry.search.eap.utils import PRIVATE_ATTRIBUTES, translate_internal_to_public_alias
from sentry.search.eap.utils import (
PRIVATE_ATTRIBUTES,
is_sentry_convention_replacement_attribute,
translate_internal_to_public_alias,
translate_to_sentry_conventions,
)
from sentry.snuba.referrer import Referrer
from sentry.utils import snuba_rpc


def convert_rpc_attribute_to_json(
attributes: list[dict],
trace_item_type: SupportedTraceItemType,
use_sentry_conventions: bool = False,
) -> list[TraceItemAttribute]:
result: list[TraceItemAttribute] = []
seen_sentry_conventions: set[str] = set()
for attribute in attributes:
internal_name = attribute["name"]
if internal_name in PRIVATE_ATTRIBUTES.get(trace_item_type, []):
Expand All @@ -53,6 +61,17 @@ def convert_rpc_attribute_to_json(
internal_name, column_type, trace_item_type
)

if use_sentry_conventions and external_name:
external_name = translate_to_sentry_conventions(external_name, trace_item_type)
if external_name in seen_sentry_conventions:
continue
seen_sentry_conventions.add(external_name)
else:
if external_name and is_sentry_convention_replacement_attribute(
external_name, trace_item_type
):
continue

if trace_item_type == SupportedTraceItemType.SPANS and internal_name.startswith(
"sentry."
):
Expand Down Expand Up @@ -149,10 +168,18 @@ def get(request: Request, project: Project, item_id: str) -> Response:

resp = MessageToDict(snuba_rpc.trace_item_details_rpc(req))

use_sentry_conventions = features.has(
"organizations:performance-sentry-conventions-fields",
project.organization,
actor=request.user,
)

resp_dict = {
"itemId": serialize_item_id(resp["itemId"], item_type),
"timestamp": resp["timestamp"],
"attributes": convert_rpc_attribute_to_json(resp["attributes"], item_type),
"attributes": convert_rpc_attribute_to_json(
resp["attributes"], item_type, use_sentry_conventions
),
}

return Response(resp_dict)
2 changes: 2 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ def register_temporary_features(manager: FeatureManager):
manager.add("organizations:performance-trace-details", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable trace explorer features
manager.add("organizations:performance-trace-explorer", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable sentry convention fields
manager.add("organizations:performance-sentry-conventions-fields", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable querying spans fields stats from comparative workflows project
manager.add("organizations:performance-spans-fields-stats", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable FE/BE for tracing without performance
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/search/eap/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ class ResolvedAttribute(ResolvedColumn):
is_aggregate: bool = field(default=False, init=False)
# There are columns in RPC that are available but we don't want rendered to the user
private: bool = False
replacement: str | None = field(default=None)
deprecation_status: str | None = field(default=None)

@property
def proto_definition(self) -> AttributeKey:
Expand Down
12 changes: 12 additions & 0 deletions src/sentry/search/eap/ourlogs/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,15 @@
for definition in OURLOG_ATTRIBUTE_DEFINITIONS.values()
if definition.private
}

LOGS_REPLACEMENT_ATTRIBUTES: set[str] = {
definition.replacement
for definition in OURLOG_ATTRIBUTE_DEFINITIONS.values()
if definition.replacement
}

LOGS_REPLACEMENT_MAP: dict[str, str] = {
definition.public_alias: definition.replacement
for definition in OURLOG_ATTRIBUTE_DEFINITIONS.values()
if definition.replacement
}
69 changes: 68 additions & 1 deletion src/sentry/search/eap/spans/attributes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Literal
import logging
import os
from dataclasses import replace
from typing import Any, Literal

from sentry_protos.snuba.v1.trace_item_attribute_pb2 import VirtualColumnContext

Expand All @@ -14,15 +17,20 @@
simple_sentry_field,
)
from sentry.search.eap.common_columns import COMMON_COLUMNS
from sentry.search.eap.spans.sentry_conventions import SENTRY_CONVENTIONS_DIRECTORY
from sentry.search.events.constants import (
PRECISE_FINISH_TS,
PRECISE_START_TS,
SPAN_MODULE_CATEGORY_VALUES,
)
from sentry.search.events.types import SnubaParams
from sentry.search.utils import DEVICE_CLASS
from sentry.utils import json
from sentry.utils.validators import is_empty_string, is_event_id_or_list, is_span_id

logger = logging.getLogger(__name__)


SPAN_ATTRIBUTE_DEFINITIONS = {
column.public_alias: column
for column in COMMON_COLUMNS
Expand Down Expand Up @@ -426,6 +434,53 @@
]
}

DEPRECATED_ATTRIBUTES: list[dict[str, Any]] = []
try:
with open(os.path.join(SENTRY_CONVENTIONS_DIRECTORY, "deprecated_attributes.json"), "rb") as f:
DEPRECATED_ATTRIBUTES = json.loads(f.read())["attributes"]
except Exception:
logger.exception("Failed to load deprecated attributes from 'deprecated_attributes.json'")

Comment on lines +438 to +442
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to this check 🤔 would it help to have a test that would effectively make sure that the json is accessible and valid?


try:
for attribute in DEPRECATED_ATTRIBUTES:
deprecation = attribute.get("deprecation", {})
attr_type = attribute.get("type", "string")
key = attribute["key"]
if (
"replacement" in deprecation
and "_status" in deprecation
and deprecation["_status"] == "backfill"
):
status = deprecation["_status"]
replacement = deprecation["replacement"]
if key in SPAN_ATTRIBUTE_DEFINITIONS:
deprecated_attr = SPAN_ATTRIBUTE_DEFINITIONS[key]
SPAN_ATTRIBUTE_DEFINITIONS[key] = replace(
deprecated_attr, replacement=replacement, deprecation_status=status
)
# TODO: Introduce units to attribute schema.
SPAN_ATTRIBUTE_DEFINITIONS[replacement] = replace(
deprecated_attr, public_alias=replacement, internal_name=replacement
)
else:
Comment on lines +463 to +465
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing a replace here so that I can preserve search_type if it's already defined and added a TODO to introduce units to the schema.

SPAN_ATTRIBUTE_DEFINITIONS[key] = ResolvedAttribute(
public_alias=key,
internal_name=key,
search_type=attr_type,
replacement=replacement,
deprecation_status=status,
)

SPAN_ATTRIBUTE_DEFINITIONS[replacement] = ResolvedAttribute(
public_alias=replacement,
internal_name=replacement,
search_type=attr_type,
)

Comment on lines +467 to +479
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds the deprecated attributes to the definitions if they don't already exist so they get replaced with the new convention correctly.

except Exception as e:
logger.exception("Failed to update attribute definitions: %s", e)


def device_class_context_constructor(params: SnubaParams) -> VirtualColumnContext:
# EAP defaults to lower case `unknown`, but in querybuilder we used `Unknown`
Expand Down Expand Up @@ -503,6 +558,18 @@ def is_starred_segment_context_constructor(params: SnubaParams) -> VirtualColumn
if definition.private
}

SPANS_REPLACEMENT_ATTRIBUTES: set[str] = {
definition.replacement
for definition in SPAN_ATTRIBUTE_DEFINITIONS.values()
if definition.replacement
}

SPANS_REPLACEMENT_MAP: dict[str, str] = {
definition.public_alias: definition.replacement
for definition in SPAN_ATTRIBUTE_DEFINITIONS.values()
if definition.replacement
}


SPAN_VIRTUAL_CONTEXTS = {
"device.class": VirtualColumnDefinition(
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/search/eap/spans/sentry_conventions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import os

SENTRY_CONVENTIONS_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)))
Loading
Loading