Skip to content

Commit 60656d2

Browse files
authored
fix(eap): Handle contexts in trace-item attributes (#112524)
- Take contexts into account for the trace item attributes so we return them as possible fields
1 parent 7a1cbce commit 60656d2

File tree

6 files changed

+257
-91
lines changed

6 files changed

+257
-91
lines changed

src/sentry/api/endpoints/organization_trace_item_attributes.py

Lines changed: 116 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
TraceItemType as ProtoTraceItemType,
2121
)
2222
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey
23-
from sentry_protos.snuba.v1.trace_item_filter_pb2 import ExistsFilter, OrFilter, TraceItemFilter
23+
from sentry_protos.snuba.v1.trace_item_filter_pb2 import (
24+
ExistsFilter,
25+
OrFilter,
26+
TraceItemFilter,
27+
)
2428

2529
from sentry import features, options
2630
from sentry.api.api_owners import ApiOwner
@@ -54,7 +58,11 @@
5458
from sentry.search.eap.resolver import SearchResolver
5559
from sentry.search.eap.spans.definitions import SPAN_DEFINITIONS
5660
from sentry.search.eap.trace_metrics.definitions import TRACE_METRICS_DEFINITIONS
57-
from sentry.search.eap.types import SearchResolverConfig, SupportedTraceItemType
61+
from sentry.search.eap.types import (
62+
AttributeSourceType,
63+
SearchResolverConfig,
64+
SupportedTraceItemType,
65+
)
5866
from sentry.search.eap.utils import (
5967
can_expose_attribute,
6068
get_secondary_aliases,
@@ -79,6 +87,10 @@
7987
POSSIBLE_ATTRIBUTE_TYPES = ["string", "number", "boolean"]
8088

8189

90+
class ProxyResolvedAttribute(ResolvedAttribute):
91+
pass
92+
93+
8294
class TraceItemAttributeKey(TypedDict):
8395
key: str
8496
name: str
@@ -218,6 +230,7 @@ def as_attribute_key(
218230
name: str,
219231
attr_type: Literal["string", "number", "boolean"],
220232
item_type: SupportedTraceItemType,
233+
is_proxy: bool = False,
221234
) -> TraceItemAttributeKey:
222235
public_key, public_name, attribute_source = translate_internal_to_public_alias(
223236
name, attr_type, item_type
@@ -226,6 +239,8 @@ def as_attribute_key(
226239

227240
if public_key is not None and public_name is not None:
228241
pass
242+
elif is_proxy:
243+
public_key = public_name = name
229244
elif attr_type == "number":
230245
public_key = f"tags[{name},number]"
231246
public_name = name
@@ -237,7 +252,11 @@ def as_attribute_key(
237252
public_name = name
238253

239254
serialized_source: dict[str, str | bool] = {
240-
"source_type": attribute_source["source_type"].value
255+
"source_type": (
256+
attribute_source["source_type"].value
257+
if not is_proxy
258+
else AttributeSourceType.SENTRY.value
259+
)
241260
}
242261
if attribute_source.get("is_transformed_alias"):
243262
serialized_source["is_transformed_alias"] = True
@@ -378,18 +397,54 @@ def query_trace_attributes(
378397
all_aliased_attributes = []
379398
# our aliases don't exist in the db, so filter over our aliases
380399
# virtually page through defined aliases before we hit the db
381-
if substring_match and offset <= len(column_definitions.columns):
382-
for index, column in enumerate(column_definitions.columns.values()):
383-
if (
384-
column.proto_type == attr_type
385-
and substring_match in column.public_alias
386-
and not column.secondary_alias
387-
and not column.private
388-
):
389-
all_aliased_attributes.append(column)
400+
if offset <= len(column_definitions.columns) + len(column_definitions.contexts):
401+
if substring_match:
402+
for column in column_definitions.columns.values():
403+
if (
404+
column.proto_type == attr_type
405+
and substring_match in column.public_alias
406+
and not column.secondary_alias
407+
and not column.private
408+
):
409+
all_aliased_attributes.append(column)
410+
for (
411+
public_label,
412+
virtual_context,
413+
) in column_definitions.contexts.items():
414+
if (
415+
substring_match in public_label
416+
and virtual_context.search_type is not None
417+
and not virtual_context.secondary_alias
418+
and constants.TYPE_MAP[virtual_context.search_type] == attr_type
419+
):
420+
all_aliased_attributes.append(
421+
ProxyResolvedAttribute(
422+
public_alias=public_label,
423+
internal_name=public_label,
424+
search_type=virtual_context.search_type,
425+
)
426+
)
427+
else:
428+
for (
429+
public_label,
430+
virtual_context,
431+
) in column_definitions.contexts.items():
432+
if (
433+
substring_match in public_label
434+
and virtual_context.search_type is not None
435+
and not virtual_context.secondary_alias
436+
and constants.TYPE_MAP[virtual_context.search_type] == attr_type
437+
):
438+
all_aliased_attributes.append(
439+
ProxyResolvedAttribute(
440+
public_alias=public_label,
441+
internal_name=public_label,
442+
search_type=virtual_context.search_type,
443+
)
444+
)
390445
aliased_attributes = all_aliased_attributes[offset : offset + limit]
391446
with sentry_sdk.start_span(op="query", name="attribute_names") as span:
392-
if len(aliased_attributes) < limit - 1:
447+
if len(aliased_attributes) < limit:
393448
offset -= len(all_aliased_attributes) - len(aliased_attributes)
394449
limit -= len(aliased_attributes)
395450
rpc_request = TraceItemAttributeNamesRequest(
@@ -419,6 +474,7 @@ def query_trace_attributes(
419474
include_internal,
420475
substring_match,
421476
aliased_attributes,
477+
all_aliased_attributes,
422478
)
423479

424480
sentry_sdk.set_context("api_response", {"attributes": attributes})
@@ -433,7 +489,8 @@ def serialize_trace_attributes_using_sentry_conventions(
433489
trace_item_type: SupportedTraceItemType,
434490
include_internal: bool,
435491
substring_match: str,
436-
aliased_attributes: list[ResolvedAttribute],
492+
aliased_attributes: list[ResolvedAttribute | ProxyResolvedAttribute],
493+
exclude_attributes: list[ResolvedAttribute | ProxyResolvedAttribute],
437494
) -> list[TraceItemAttributeKey]:
438495
attribute_keys = {}
439496
for attribute in rpc_response.attributes:
@@ -457,11 +514,21 @@ def serialize_trace_attributes_using_sentry_conventions(
457514
)
458515

459516
attribute_keys[attr_key["name"]] = attr_key
517+
for aliased_attr in exclude_attributes:
518+
attr_key = as_attribute_key(
519+
aliased_attr.internal_name,
520+
attribute_type,
521+
trace_item_type,
522+
is_proxy=isinstance(aliased_attr, ProxyResolvedAttribute),
523+
)
524+
if attr_key["name"] in attribute_keys:
525+
del attribute_keys[attr_key["name"]]
460526
for aliased_attr in aliased_attributes:
461527
attr_key = as_attribute_key(
462528
aliased_attr.internal_name,
463529
attribute_type,
464530
trace_item_type,
531+
is_proxy=isinstance(aliased_attr, ProxyResolvedAttribute),
465532
)
466533
attribute_keys[attr_key["name"]] = attr_key
467534

@@ -476,33 +543,42 @@ def serialize_trace_attributes(
476543
trace_item_type: SupportedTraceItemType,
477544
include_internal: bool,
478545
substring_match: str,
479-
aliased_attributes: list[ResolvedAttribute],
546+
aliased_attributes: list[ResolvedAttribute | ProxyResolvedAttribute],
547+
exclude_attributes: list[ResolvedAttribute | ProxyResolvedAttribute],
480548
) -> list[TraceItemAttributeKey]:
481-
attributes = list(
482-
filter(
483-
lambda x: (
484-
not is_sentry_convention_replacement_attribute(x["name"], trace_item_type)
549+
attribute_keys = {}
550+
for attribute in rpc_response.attributes:
551+
if attribute.name and can_expose_attribute(
552+
attribute.name,
553+
trace_item_type,
554+
include_internal=include_internal,
555+
):
556+
attr_key = as_attribute_key(
557+
attribute.name,
558+
attribute_type,
559+
trace_item_type,
560+
)
561+
if (
562+
not is_sentry_convention_replacement_attribute(
563+
attr_key["name"], trace_item_type
564+
)
485565
# Remove anything where the public alias doesn't match the substring
486566
# This can happen when the public alias is different, but that's handled by
487567
# aliased_attributes
488-
and (substring_match in x["name"] if substring_match else True)
489-
),
490-
[
491-
as_attribute_key(
492-
attribute.name,
493-
attribute_type,
494-
trace_item_type,
495-
)
496-
for attribute in rpc_response.attributes
497-
if attribute.name
498-
and can_expose_attribute(
499-
attribute.name,
500-
trace_item_type,
501-
include_internal=include_internal,
502-
)
503-
],
568+
and (substring_match in attr_key["name"] if substring_match else True)
569+
):
570+
attribute_keys[attr_key["key"]] = attr_key
571+
# We need to exclude any aliased attributes here since because of pagination they might have already been seen
572+
# earlier
573+
for aliased_attr in exclude_attributes:
574+
attr_key = as_attribute_key(
575+
aliased_attr.internal_name,
576+
attribute_type,
577+
trace_item_type,
578+
is_proxy=isinstance(aliased_attr, ProxyResolvedAttribute),
504579
)
505-
)
580+
if attr_key["name"] in attribute_keys:
581+
del attribute_keys[attr_key["name"]]
506582
for aliased_attr in aliased_attributes:
507583
if can_expose_attribute(
508584
aliased_attr.public_alias,
@@ -513,8 +589,11 @@ def serialize_trace_attributes(
513589
aliased_attr.internal_name,
514590
attribute_type,
515591
trace_item_type,
592+
is_proxy=isinstance(aliased_attr, ProxyResolvedAttribute),
516593
)
517-
attributes.append(attr_key)
594+
attribute_keys[attr_key["key"]] = attr_key
595+
attributes = list(attribute_keys.values())
596+
sentry_sdk.set_context("api_response", {"attributes": attributes})
518597
return attributes
519598

520599

src/sentry/search/eap/columns.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ class AttributeArgumentDefinition(BaseArgumentDefinition):
158158
@dataclass
159159
class VirtualColumnDefinition:
160160
constructor: Callable[[SnubaParams, Any], VirtualColumnContext]
161+
# Need a type for the attributes endpoint
162+
search_type: constants.SearchType
163+
secondary_alias: bool = False
161164
# Allows additional processing to the term after its been resolved
162165
term_resolver: (
163166
Callable[
@@ -255,7 +258,9 @@ class ResolvedTraceMetricAggregate(ResolvedFunction):
255258
trace_filter: TraceItemFilter | None
256259

257260
@property
258-
def proto_definition(self) -> AttributeAggregation | AttributeConditionalAggregation:
261+
def proto_definition(
262+
self,
263+
) -> AttributeAggregation | AttributeConditionalAggregation:
259264
if self.trace_metric is None and self.trace_filter is None:
260265
return AttributeAggregation(
261266
aggregate=self.internal_name,
@@ -460,9 +465,11 @@ def resolve(
460465
),
461466
key=cast(AttributeKey, resolved_attribute),
462467
trace_metric=trace_metric,
463-
trace_filter=cast(TraceItemFilter, resolved_arguments[0])
464-
if isinstance(self, ConditionalTraceMetricAggregateDefinition)
465-
else None,
468+
trace_filter=(
469+
cast(TraceItemFilter, resolved_arguments[0])
470+
if isinstance(self, ConditionalTraceMetricAggregateDefinition)
471+
else None
472+
),
466473
)
467474

468475

@@ -674,7 +681,9 @@ class ColumnDefinitions:
674681
column_to_alias: Callable[[str], str | None] | None
675682

676683

677-
def attribute_key_to_tuple(attribute_key: AttributeKey) -> tuple[str, AttributeKey.Type.ValueType]:
684+
def attribute_key_to_tuple(
685+
attribute_key: AttributeKey,
686+
) -> tuple[str, AttributeKey.Type.ValueType]:
678687
return (attribute_key.name, attribute_key.type)
679688

680689

@@ -731,9 +740,11 @@ def extract_trace_metric_aggregate_arguments(
731740
return TraceMetric(
732741
metric_name=cast(str, resolved_arguments[offset + 1]),
733742
metric_type=cast(TraceMetricType, resolved_arguments[offset + 2]),
734-
metric_unit=None
735-
if resolved_arguments[offset + 3] == "-"
736-
else cast(str, resolved_arguments[offset + 3]),
743+
metric_unit=(
744+
None
745+
if resolved_arguments[offset + 3] == "-"
746+
else cast(str, resolved_arguments[offset + 3])
747+
),
737748
)
738749
elif all(resolved_argument == "" for resolved_argument in resolved_arguments[offset + 1 :]):
739750
# no metrics were specified, assume we query all metrics

src/sentry/search/eap/common_columns.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
constructor=project_context_constructor(key),
1313
term_resolver=project_term_resolver,
1414
filter_column="project.id",
15+
search_type="string",
16+
secondary_alias=key != "project",
1517
)
1618
for key in constants.PROJECT_FIELDS
1719
}

src/sentry/search/eap/spans/attributes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,14 +682,17 @@ def is_starred_segment_context_constructor(
682682
# TODO: need to change this so the VCC is using it too, but would require rewriting the term_resolver
683683
default_value="Unknown",
684684
sort_column="sentry.device.class",
685+
search_type="string",
685686
),
686687
"span.module": VirtualColumnDefinition(
687688
constructor=module_context_constructor,
689+
search_type="string",
688690
),
689691
"is_starred_transaction": VirtualColumnDefinition(
690692
constructor=is_starred_segment_context_constructor,
691693
default_value="false",
692694
processor=lambda x: True if x == "true" else False,
695+
search_type="boolean",
693696
),
694697
**project_virtual_contexts(),
695698
}

0 commit comments

Comments
 (0)