Skip to content

Commit 057a9cc

Browse files
authored
feat(tracemetrics): Add tracemetrics column definitions to trace-items (#101248)
The `trace-items` endpoint wasn't using the `TRACE_METRICS_DEFINITIONS` instance to resolve columns, so a number of options did not appear when using the endpoint. Added the item type to the definitions and tests to check that we can get the keys, as well as values for tracemetrics.
1 parent ae94f94 commit 057a9cc

File tree

4 files changed

+194
-16
lines changed

4 files changed

+194
-16
lines changed

src/sentry/api/endpoints/organization_trace_item_attributes.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from sentry.search.eap.ourlogs.definitions import OURLOG_DEFINITIONS
3838
from sentry.search.eap.resolver import SearchResolver
3939
from sentry.search.eap.spans.definitions import SPAN_DEFINITIONS
40+
from sentry.search.eap.trace_metrics.definitions import TRACE_METRICS_DEFINITIONS
4041
from sentry.search.eap.types import SearchResolverConfig, SupportedTraceItemType
4142
from sentry.search.eap.utils import (
4243
can_expose_attribute,
@@ -107,6 +108,7 @@ class OrganizationTraceItemAttributesEndpointBase(OrganizationEventsV2EndpointBa
107108
feature_flags = [
108109
"organizations:ourlogs-enabled",
109110
"organizations:visibility-explore-view",
111+
"organizations:tracemetrics-enabled",
110112
]
111113

112114
def has_feature(self, organization: Organization, request: Request) -> bool:
@@ -139,23 +141,36 @@ def is_valid_item_type(item_type: str) -> bool:
139141

140142

141143
def get_column_definitions(item_type: SupportedTraceItemType) -> ColumnDefinitions:
142-
return SPAN_DEFINITIONS if item_type == SupportedTraceItemType.SPANS else OURLOG_DEFINITIONS
144+
if item_type == SupportedTraceItemType.SPANS:
145+
return SPAN_DEFINITIONS
146+
elif item_type == SupportedTraceItemType.LOGS:
147+
return OURLOG_DEFINITIONS
148+
elif item_type == SupportedTraceItemType.TRACEMETRICS:
149+
return TRACE_METRICS_DEFINITIONS
150+
151+
raise ValueError(f"Invalid item type: {item_type}")
143152

144153

145154
def resolve_attribute_referrer(item_type: str, attribute_type: str) -> Referrer:
146-
return (
147-
Referrer.API_SPANS_TAG_KEYS_RPC
148-
if item_type == SupportedTraceItemType.SPANS.value
149-
else Referrer.API_LOGS_TAG_KEYS_RPC
150-
)
155+
if item_type == SupportedTraceItemType.SPANS.value:
156+
return Referrer.API_SPANS_TAG_KEYS_RPC
157+
elif item_type == SupportedTraceItemType.LOGS.value:
158+
return Referrer.API_LOGS_TAG_KEYS_RPC
159+
elif item_type == SupportedTraceItemType.TRACEMETRICS.value:
160+
return Referrer.API_TRACE_METRICS_TAG_KEYS_RPC
161+
else:
162+
raise ValueError(f"Invalid item type: {item_type}")
151163

152164

153165
def resolve_attribute_values_referrer(item_type: str) -> Referrer:
154-
return (
155-
Referrer.API_SPANS_TAG_VALUES_RPC
156-
if item_type == SupportedTraceItemType.SPANS.value
157-
else Referrer.API_LOGS_TAG_VALUES_RPC
158-
)
166+
if item_type == SupportedTraceItemType.SPANS.value:
167+
return Referrer.API_SPANS_TAG_VALUES_RPC
168+
elif item_type == SupportedTraceItemType.LOGS.value:
169+
return Referrer.API_LOGS_TAG_VALUES_RPC
170+
elif item_type == SupportedTraceItemType.TRACEMETRICS.value:
171+
return Referrer.API_TRACE_METRICS_TAG_VALUES_RPC
172+
else:
173+
raise ValueError(f"Invalid item type: {item_type}")
159174

160175

161176
def as_attribute_key(
@@ -345,11 +360,7 @@ def get(self, request: Request, organization: Organization, key: str) -> Respons
345360

346361
max_attribute_values = options.get("explore.trace-items.values.max")
347362

348-
definitions = (
349-
SPAN_DEFINITIONS
350-
if item_type == SupportedTraceItemType.SPANS.value
351-
else OURLOG_DEFINITIONS
352-
)
363+
definitions = get_column_definitions(SupportedTraceItemType(item_type))
353364

354365
def data_fn(offset: int, limit: int):
355366
executor = TraceItemAttributeValuesAutocompletionExecutor(

src/sentry/snuba/referrer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,8 @@ class Referrer(StrEnum):
548548
API_SPANS_FREQUENCY_STATS_RPC = "api.spans.fields-stats.rpc"
549549
API_SPANS_TAG_VALUES_RPC = "api.spans.tags-values.rpc"
550550
API_SPANS_TRACE_VIEW = "api.spans.trace-view"
551+
API_TRACE_METRICS_TAG_KEYS_RPC = "api.tracemetrics.tags-keys.rpc"
552+
API_TRACE_METRICS_TAG_VALUES_RPC = "api.tracemetrics.tags-values.rpc"
551553

552554
API_SPAN_SAMPLE_GET_BOUNDS = "api.spans.sample-get-bounds"
553555
API_SPAN_SAMPLE_GET_SPAN_IDS = "api.spans.sample-get-span-ids"

src/sentry/testutils/cases.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3483,6 +3483,7 @@ def create_trace_metric(
34833483
project: Project | None = None,
34843484
timestamp: datetime | None = None,
34853485
trace_id: str | None = None,
3486+
attributes: dict[str, Any] | None = None,
34863487
) -> TraceItem:
34873488
if organization is None:
34883489
organization = self.organization
@@ -3506,6 +3507,10 @@ def create_trace_metric(
35063507
"sentry.value": AnyValue(double_value=metric_value),
35073508
}
35083509

3510+
if attributes:
3511+
for k, v in attributes.items():
3512+
attributes_proto[k] = scalar_to_any_value(v)
3513+
35093514
return TraceItem(
35103515
organization_id=organization.id,
35113516
project_id=project.id,

tests/snuba/api/endpoints/test_organization_trace_item_attributes.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
OurLogTestCase,
1515
SnubaTestCase,
1616
SpanTestCase,
17+
TraceMetricsTestCase,
1718
)
1819
from sentry.testutils.helpers import parse_link_header
1920
from sentry.testutils.helpers.datetime import before_now
@@ -789,6 +790,133 @@ def test_sentry_internal_attributes(self) -> None:
789790
assert "__sentry_internal_test" in attribute_names
790791

791792

793+
class OrganizationTraceItemAttributesEndpointTraceMetricsTest(
794+
OrganizationTraceItemAttributesEndpointTestBase, TraceMetricsTestCase
795+
):
796+
feature_flags = {"organizations:tracemetrics-enabled": True}
797+
item_type = SupportedTraceItemType.TRACEMETRICS
798+
799+
def test_no_feature(self) -> None:
800+
response = self.do_request(features={})
801+
assert response.status_code == 404, response.content
802+
803+
def test_invalid_item_type(self) -> None:
804+
response = self.do_request(query={"itemType": "invalid"})
805+
assert response.status_code == 400, response.content
806+
assert response.data == {
807+
"itemType": [
808+
ErrorDetail(string='"invalid" is not a valid choice.', code="invalid_choice")
809+
],
810+
}
811+
812+
def test_trace_metrics_string_attributes(self) -> None:
813+
"""Test that we can successfully retrieve string attributes from trace metrics"""
814+
metrics = [
815+
self.create_trace_metric(
816+
metric_name="http.request.duration",
817+
metric_value=123.45,
818+
organization=self.organization,
819+
project=self.project,
820+
attributes={
821+
"http.method": "GET",
822+
"http.status_code": "200",
823+
"environment": "production",
824+
},
825+
),
826+
self.create_trace_metric(
827+
metric_name="http.request.duration",
828+
metric_value=234.56,
829+
organization=self.organization,
830+
project=self.project,
831+
attributes={
832+
"http.method": "POST",
833+
"http.status_code": "201",
834+
"environment": "staging",
835+
},
836+
),
837+
]
838+
self.store_trace_metrics(metrics)
839+
840+
response = self.do_request(query={"attributeType": "string"})
841+
842+
assert response.status_code == 200, response.content
843+
data = response.data
844+
assert len(data) > 0
845+
846+
# Verify that our custom attributes are returned
847+
attribute_keys = {item["key"] for item in data}
848+
assert "http.method" in attribute_keys
849+
assert "http.status_code" in attribute_keys
850+
# Environment may be stored as tags[environment,string]
851+
assert "environment" in attribute_keys or "tags[environment,string]" in attribute_keys
852+
853+
def test_trace_metrics_filter_by_metric_name(self) -> None:
854+
"""Test that we can filter trace metrics attributes by metric name using query parameter"""
855+
metrics = [
856+
self.create_trace_metric(
857+
metric_name="http.request.duration",
858+
metric_value=100.0,
859+
organization=self.organization,
860+
project=self.project,
861+
attributes={
862+
"http.method": "GET",
863+
"http.route": "/api/users",
864+
},
865+
),
866+
self.create_trace_metric(
867+
metric_name="database.query.duration",
868+
metric_value=50.0,
869+
organization=self.organization,
870+
project=self.project,
871+
attributes={
872+
"db.system": {"string_value": "postgresql"},
873+
"db.operation": {"string_value": "SELECT"},
874+
},
875+
),
876+
]
877+
self.store_trace_metrics(metrics)
878+
879+
# Query for http metric attributes
880+
response = self.do_request(
881+
query={
882+
"attributeType": "string",
883+
"query": 'metric.name:"http.request.duration"',
884+
}
885+
)
886+
887+
assert response.status_code == 200, response.content
888+
data = response.data
889+
attribute_keys = {item["key"] for item in data}
890+
891+
# Should include HTTP attributes
892+
assert "http.method" in attribute_keys or "http.route" in attribute_keys
893+
894+
def test_trace_metrics_number_attributes(self) -> None:
895+
"""Test that we can retrieve number attributes from trace metrics"""
896+
metrics = [
897+
self.create_trace_metric(
898+
metric_name="custom.metric",
899+
metric_value=100.0,
900+
organization=self.organization,
901+
project=self.project,
902+
attributes={
903+
"request.size": {"int_value": 1024},
904+
"response.time": {"double_value": 42.5},
905+
},
906+
),
907+
]
908+
self.store_trace_metrics(metrics)
909+
910+
response = self.do_request(query={"attributeType": "number"})
911+
912+
assert response.status_code == 200, response.content
913+
data = response.data
914+
915+
# Verify number attributes are returned
916+
# Note: The exact keys depend on how the backend processes numeric attributes
917+
assert len(data) >= 0 # May be 0 if number attributes are handled differently
918+
919+
792920
class OrganizationTraceItemAttributeValuesEndpointBaseTest(APITestCase, SnubaTestCase):
793921
feature_flags: dict[str, bool]
794922
item_type: SupportedTraceItemType
@@ -1771,3 +1899,35 @@ def test_autocomplete_timestamp(self) -> None:
17711899
response = self.do_request(key="timestamp", query={"substringMatch": "20"})
17721900
assert response.status_code == 200
17731901
assert response.data == []
1902+
1903+
1904+
class OrganizationTraceItemAttributeValuesEndpointTraceMetricsTest(
1905+
OrganizationTraceItemAttributeValuesEndpointBaseTest, TraceMetricsTestCase
1906+
):
1907+
feature_flags = {"organizations:tracemetrics-enabled": True}
1908+
item_type = SupportedTraceItemType.TRACEMETRICS
1909+
1910+
def test_no_feature(self) -> None:
1911+
response = self.do_request(features={}, key="test.attribute")
1912+
assert response.status_code == 404, response.content
1913+
1914+
def test_attribute_values(self) -> None:
1915+
metrics = [
1916+
self.create_trace_metric(
1917+
metric_name="http.request.duration",
1918+
metric_value=123.45,
1919+
attributes={"http.method": "GET"},
1920+
),
1921+
self.create_trace_metric(
1922+
metric_name="http.request.duration",
1923+
metric_value=234.56,
1924+
attributes={"http.method": "POST"},
1925+
),
1926+
]
1927+
self.store_trace_metrics(metrics)
1928+
1929+
response = self.do_request(key="http.method")
1930+
assert response.status_code == 200
1931+
values = {item["value"] for item in response.data}
1932+
assert "GET" in values
1933+
assert "POST" in values

0 commit comments

Comments
 (0)