Skip to content

Commit 24c9048

Browse files
authored
chore(perf): Remove useSpans param from events trace (#95381)
This param has been removed from the frontend, so we can remove it from the backend as well.
1 parent 275354d commit 24c9048

File tree

2 files changed

+10
-496
lines changed

2 files changed

+10
-496
lines changed

src/sentry/api/endpoints/organization_events_trace.py

Lines changed: 9 additions & 273 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from rest_framework.request import Request
1515
from rest_framework.response import Response
1616
from sentry_relay.consts import SPAN_STATUS_CODE_TO_NAME
17-
from snuba_sdk import Column, Condition, Function, Op
17+
from snuba_sdk import Column, Function
1818

1919
from sentry import constants, eventstore, features, options
2020
from sentry.api.api_publish_status import ApiPublishStatus
@@ -29,12 +29,10 @@
2929
from sentry.models.project import Project
3030
from sentry.organizations.services.organization import RpcOrganization
3131
from sentry.search.events.builder.discover import DiscoverQueryBuilder
32-
from sentry.search.events.builder.spans_indexed import SpansIndexedQueryBuilder
3332
from sentry.search.events.types import QueryBuilderConfig, SnubaParams
3433
from sentry.snuba.dataset import Dataset
3534
from sentry.snuba.query_sources import QuerySource
3635
from sentry.snuba.referrer import Referrer
37-
from sentry.utils.iterators import chunked
3836
from sentry.utils.numbers import base32_encode, format_grouped_length
3937
from sentry.utils.sdk import set_span_attribute
4038
from sentry.utils.snuba import bulk_snuba_queries
@@ -594,7 +592,6 @@ def query_trace_data(
594592
transaction_params: SnubaParams,
595593
limit: int,
596594
event_id: str | None,
597-
use_spans: bool,
598595
query_source: QuerySource | None = QuerySource.SENTRY_BACKEND,
599596
) -> tuple[Sequence[SnubaTransaction], Sequence[SnubaError]]:
600597
transaction_columns = [
@@ -627,13 +624,6 @@ def query_trace_data(
627624
# Target is the event_id the frontend plans to render, we try to sort it to the top so it loads even if its not
628625
# within the query limit, needs to be the first orderby cause it takes precedence over finding the root
629626
transaction_orderby.insert(0, "-target")
630-
if use_spans:
631-
transaction_columns.extend(
632-
[
633-
"measurements.key",
634-
"measurements.value",
635-
]
636-
)
637627
transaction_query = DiscoverQueryBuilder(
638628
Dataset.Transactions,
639629
params={},
@@ -715,14 +705,6 @@ def query_trace_data(
715705
result["issue.ids"] = occurrence_issue_ids.get(result["id"], [])
716706
result["occurrence_id"] = occurrence_ids.get(result["id"], [])
717707
result["trace.parent_transaction"] = None
718-
if use_spans:
719-
result["measurements"] = {
720-
key: {
721-
"value": value,
722-
"type": transaction_query.get_field_type(f"measurements.{key}"),
723-
}
724-
for key, value in zip(result["measurements.key"], result["measurements.value"])
725-
}
726708

727709
# Snuba responses aren't typed
728710
return cast(Sequence[SnubaTransaction], transformed_results[0]), cast(
@@ -743,44 +725,6 @@ def strip_span_id(span_id):
743725
return result
744726

745727

746-
def build_span_query(trace_id: str, spans_params: SnubaParams, query_spans: list[str]):
747-
parents_query = SpansIndexedQueryBuilder(
748-
Dataset.SpansIndexed,
749-
params={},
750-
snuba_params=spans_params,
751-
query=f"trace:{trace_id}",
752-
selected_columns=[
753-
"transaction.id",
754-
"span_id",
755-
"precise.start_ts",
756-
"precise.finish_ts",
757-
],
758-
# Don't add an orderby here that way if clickhouse hits the # of span_ids we've asked for it'll exit early
759-
limit=len(query_spans),
760-
)
761-
# Performance improvement, snuba's parser is extremely slow when we're sending thousands of
762-
# span_ids here, using a `splitByChar` means that snuba will not parse the giant list of spans
763-
span_minimum = options.get("performance.traces.span_query_minimum_spans")
764-
set_span_attribute("trace_view.spans.span_minimum", span_minimum)
765-
sentry_sdk.set_tag("trace_view.split_by_char.optimization", len(query_spans) > span_minimum)
766-
if len(query_spans) > span_minimum:
767-
# TODO: because we're not doing an IN on a list of literals, snuba will not optimize the query with the HexInt
768-
# column processor which means we won't be taking advantage of the span_id index but if we only do this when we
769-
# have a lot of query_spans we should have a great performance improvement still once we do that we can simplify
770-
# this code and always apply this optimization
771-
span_condition_value = Function(
772-
"splitByChar", [",", ",".join(strip_span_id(span_id) for span_id in query_spans)]
773-
)
774-
else:
775-
span_condition_value = Function("tuple", list(query_spans))
776-
# Building the condition manually, a performance optimization since we might put thousands of span ids
777-
# and this way we skip both parsimonious and the builder
778-
parents_query.add_conditions(
779-
[Condition(Column(parents_query.resolve_column_name("id")), Op.IN, span_condition_value)]
780-
)
781-
return parents_query
782-
783-
784728
def pad_span_id(span: str | None) -> str:
785729
"""Snuba might return the span id without leading 0s since they're stored as UInt64
786730
which means a span like 0011 gets converted to an int, then back so we'll get `11` instead"""
@@ -789,171 +733,6 @@ def pad_span_id(span: str | None) -> str:
789733
return span.rjust(16, "0")
790734

791735

792-
@sentry_sdk.tracing.trace
793-
def augment_transactions_with_spans(
794-
transactions: Sequence[SnubaTransaction],
795-
errors: Sequence[SnubaError],
796-
trace_id: str,
797-
params: SnubaParams,
798-
query_source: QuerySource | None = QuerySource.SENTRY_BACKEND,
799-
) -> Sequence[SnubaTransaction]:
800-
"""Augment the list of transactions with parent, error and problem data"""
801-
with sentry_sdk.start_span(op="augment.transactions", name="setup"):
802-
trace_parent_spans = set() # parent span ids of segment spans
803-
transaction_problem_map: dict[str, SnubaTransaction] = {}
804-
problem_project_map: dict[int, list[str]] = {}
805-
issue_occurrences = []
806-
occurrence_spans: set[str] = set()
807-
error_spans = set()
808-
projects = set()
809-
for error in errors:
810-
if "trace.span" in error:
811-
error["trace.span"] = pad_span_id(error["trace.span"])
812-
error_spans.add(error["trace.span"])
813-
projects.add(error["project.id"])
814-
ts_params = find_timestamp_params(transactions)
815-
time_buffer = options.get("performance.traces.span_query_timebuffer_hours")
816-
set_span_attribute("trace_view.spans.time_buffer", time_buffer)
817-
if ts_params["min"]:
818-
params.start = ts_params["min"] - timedelta(hours=time_buffer)
819-
if ts_params["max"]:
820-
params.end = ts_params["max"] + timedelta(hours=time_buffer)
821-
822-
if ts_params["max"] and ts_params["min"]:
823-
set_span_attribute(
824-
"trace_view.trace_duration", (ts_params["max"] - ts_params["min"]).total_seconds()
825-
)
826-
sentry_sdk.set_tag("trace_view.missing_timestamp_constraints", False)
827-
else:
828-
sentry_sdk.set_tag("trace_view.missing_timestamp_constraints", True)
829-
830-
with sentry_sdk.start_span(op="augment.transactions", name="get transaction span ids"):
831-
for index, transaction in enumerate(transactions):
832-
transaction["occurrence_spans"] = []
833-
transaction["issue_occurrences"] = []
834-
835-
project = transaction["project.id"]
836-
projects.add(project)
837-
838-
# Pull out occurrence data
839-
transaction_problem_map[transaction["id"]] = transaction
840-
if project not in problem_project_map:
841-
problem_project_map[project] = []
842-
if transaction["occurrence_id"] is not None:
843-
problem_project_map[project].extend(transaction["occurrence_id"])
844-
845-
if not transaction["trace.parent_span"]:
846-
continue
847-
# parent span ids of the segment spans
848-
trace_parent_spans.add(transaction["trace.parent_span"])
849-
850-
with sentry_sdk.start_span(op="augment.transactions", name="get perf issue span ids"):
851-
for problem_project, occurrences in problem_project_map.items():
852-
if occurrences:
853-
issue_occurrences.extend(
854-
[
855-
occurrence
856-
for occurrence in IssueOccurrence.fetch_multi(occurrences, problem_project)
857-
if occurrence is not None
858-
]
859-
)
860-
861-
for problem in issue_occurrences:
862-
occurrence_spans = occurrence_spans.union(
863-
set(problem.evidence_data["offender_span_ids"])
864-
)
865-
866-
with sentry_sdk.start_span(op="augment.transactions", name="create query params"):
867-
unfiltered_spans = {*trace_parent_spans, *error_spans, *occurrence_spans}
868-
query_spans: set[str] = {
869-
span for span in unfiltered_spans if span is not None and span != ""
870-
}
871-
# If there are no spans to query just return transactions as is
872-
if len(query_spans) == 0:
873-
return transactions
874-
875-
# Fetch parent span ids of segment spans and their corresponding
876-
# transaction id so we can link parent/child transactions in
877-
# a trace.
878-
spans_params = params.copy()
879-
spans_params.projects = [p for p in params.projects if p.id in projects]
880-
881-
# If we're querying over 100 span ids, lets split the query into 3
882-
sentry_sdk.set_tag("trace_view.use_spans.span_len", len(query_spans))
883-
884-
# Whether any of the span queries hit their query limit, which means that clickhouse would've exited early
885-
# this is for tagging so we can see the performance difference
886-
hit_limit = False
887-
# The max query size according to snuba/clickhouse docs is 256KiB, or about 256 thousand characters
888-
# Each span id maps to being 20 characters; 16 characters turned back into a number maxes out at 20
889-
# which at 10k transaction span ids, and even another 10k error span ids (which would only happen if there's no
890-
# crossover) that's 20k * 20 = 400k bytes, with 3 queries that should be 133,333 bytes which shouldn't be anywhere
891-
# near the 256KiB max even with the other parts of the query.
892-
# Experimentally (running queries in snuba admin) we've found the max query size is actually 131,535 bytes or
893-
# 128.45KiB. Taking that into account, (131,535-10,000(for projects etc))/20 = 6000, means that we can fit at most
894-
# 6000 span ids per query, Adding a bit of a buffer, if the query is for more than 12,500 spans we'll do 4 chunks
895-
# instead of 3
896-
if len(query_spans) > 100:
897-
list_spans = list(query_spans)
898-
if len(query_spans) < 12_500:
899-
total_chunks = 3
900-
else:
901-
total_chunks = 4
902-
set_span_attribute("trace_view.span_query.total_chunks", total_chunks)
903-
chunks = chunked(list_spans, (len(list_spans) // total_chunks) + 1)
904-
queries = [build_span_query(trace_id, spans_params, chunk) for chunk in chunks]
905-
results = bulk_snuba_queries(
906-
[query.get_snql_query() for query in queries],
907-
referrer=Referrer.API_TRACE_VIEW_GET_PARENTS.value,
908-
query_source=query_source,
909-
)
910-
parents_results = results[0]
911-
for result, query in zip(results, queries):
912-
if len(result["data"]) == query.limit.limit:
913-
hit_limit = True
914-
for result in results[1:]:
915-
parents_results["data"].extend(result["data"])
916-
else:
917-
parents_query = build_span_query(trace_id, spans_params, list(query_spans))
918-
parents_results = parents_query.run_query(
919-
referrer=Referrer.API_TRACE_VIEW_GET_PARENTS.value,
920-
query_source=query_source,
921-
)
922-
if len(parents_results) == parents_query.limit.limit:
923-
hit_limit = True
924-
sentry_sdk.set_tag("trace_view.span_query.hit_limit", hit_limit)
925-
926-
parent_map = {}
927-
if "data" in parents_results:
928-
for parent in parents_results["data"]:
929-
parent["span_id"] = pad_span_id(parent["span_id"])
930-
parent_map[parent["span_id"]] = parent
931-
932-
with sentry_sdk.start_span(op="augment.transactions", name="linking transactions"):
933-
for transaction in transactions:
934-
# For a given transaction, if parent span id exists in the tranaction (so this is
935-
# not a root span), see if the indexed spans data can tell us what the parent
936-
# transaction id is.
937-
if "trace.parent_span" in transaction:
938-
parent = parent_map.get(transaction["trace.parent_span"])
939-
if parent is not None:
940-
transaction["trace.parent_transaction"] = parent["transaction.id"]
941-
with sentry_sdk.start_span(op="augment.transactions", name="linking perf issues"):
942-
for problem in issue_occurrences:
943-
for span_id in problem.evidence_data["offender_span_ids"]:
944-
parent = parent_map.get(span_id)
945-
if parent is not None:
946-
transaction_problem = transaction_problem_map[problem.event_id]
947-
occurrence = parent.copy()
948-
occurrence["problem"] = problem
949-
transaction_problem["occurrence_spans"].append(occurrence)
950-
with sentry_sdk.start_span(op="augment.transactions", name="linking errors"):
951-
for error in errors:
952-
parent = parent_map.get(error["trace.span"])
953-
error["trace.transaction"] = parent["transaction.id"] if parent is not None else None
954-
return transactions
955-
956-
957736
class OrganizationEventsTraceEndpointBase(OrganizationEventsV2EndpointBase):
958737
publish_status = {
959738
"GET": ApiPublishStatus.PRIVATE,
@@ -1072,13 +851,8 @@ def get(self, request: Request, organization: Organization, trace_id: str) -> Ht
1072851

1073852
# Detailed is deprecated now that we want to use spans instead
1074853
detailed = request.GET.get("detailed", "0") == "1"
1075-
# Temporary url params until we finish migrating the frontend
1076-
use_spans = request.GET.get("useSpans", "0") == "1"
1077854
update_snuba_params_with_timestamp(request, snuba_params)
1078855

1079-
sentry_sdk.set_tag("trace_view.using_spans", str(use_spans))
1080-
if detailed and use_spans:
1081-
raise ParseError("Cannot return a detailed response while using spans")
1082856
limit = min(int(request.GET.get("limit", MAX_TRACE_SIZE)), 10_000)
1083857
event_id = (
1084858
request.GET.get("targetId") or request.GET.get("event_id") or request.GET.get("eventId")
@@ -1094,33 +868,14 @@ def get(self, request: Request, organization: Organization, trace_id: str) -> Ht
1094868
trace_id, snuba_params, query_source=query_source
1095869
)
1096870

1097-
if use_spans:
1098-
transactions, errors = query_trace_data(
1099-
trace_id,
1100-
snuba_params,
1101-
transaction_params,
1102-
limit,
1103-
event_id,
1104-
use_spans,
1105-
query_source=query_source,
1106-
)
1107-
transactions = augment_transactions_with_spans(
1108-
transactions,
1109-
errors,
1110-
trace_id,
1111-
snuba_params,
1112-
query_source=query_source,
1113-
)
1114-
else:
1115-
transactions, errors = query_trace_data(
1116-
trace_id,
1117-
snuba_params,
1118-
transaction_params,
1119-
limit,
1120-
None,
1121-
False,
1122-
query_source=query_source,
1123-
)
871+
transactions, errors = query_trace_data(
872+
trace_id,
873+
snuba_params,
874+
transaction_params,
875+
limit,
876+
None,
877+
query_source=query_source,
878+
)
1124879
self.record_analytics(transactions, trace_id, request.user.id, organization.id)
1125880

1126881
warning_extra: dict[str, str] = {"trace": trace_id, "organization": organization.slug}
@@ -1150,7 +905,6 @@ def get(self, request: Request, organization: Organization, trace_id: str) -> Ht
1150905
warning_extra,
1151906
event_id,
1152907
detailed,
1153-
use_spans,
1154908
query_source=self.get_request_source(request),
1155909
)
1156910
)
@@ -1165,7 +919,6 @@ def serialize(
1165919
warning_extra: dict[str, str],
1166920
event_id: str | None,
1167921
detailed: bool = False,
1168-
use_spans: bool = False,
1169922
query_source: QuerySource | None = None,
1170923
) -> Any:
1171924
raise NotImplementedError
@@ -1238,12 +991,9 @@ def serialize(
1238991
warning_extra: dict[str, str],
1239992
event_id: str | None,
1240993
detailed: bool = False,
1241-
use_spans: bool = False,
1242994
query_source: QuerySource | None = None,
1243995
) -> dict[str, list[LightResponse | TraceError]]:
1244996
"""Because the light endpoint could potentially have gaps between root and event we return a flattened list"""
1245-
if use_spans:
1246-
raise ParseError(detail="useSpans isn't supported on the trace-light")
1247997
if event_id is None:
1248998
raise ParseError(detail="An event_id is required for the light trace")
1249999
snuba_event, nodestore_event = self.get_current_transaction(transactions, errors, event_id)
@@ -1413,27 +1163,13 @@ def serialize(
14131163
warning_extra: dict[str, str],
14141164
event_id: str | None,
14151165
detailed: bool = False,
1416-
use_spans: bool = False,
14171166
query_source: QuerySource | None = None,
14181167
) -> SerializedTrace:
14191168
"""For the full event trace, we return the results as a graph instead of a flattened list
14201169
14211170
if event_id is passed, we prune any potential branches of the trace to make as few nodestore calls as
14221171
possible
14231172
"""
1424-
if use_spans:
1425-
results = self.serialize_with_spans(
1426-
limit,
1427-
transactions,
1428-
errors,
1429-
roots,
1430-
warning_extra,
1431-
event_id,
1432-
detailed,
1433-
query_source=query_source,
1434-
)
1435-
return results
1436-
14371173
# Code past here is deprecated, but must continue to exist until sentry installs in every possible environment
14381174
# are storing span data, since that's the only way serialize_with_spans will work
14391175
event_id_to_nodestore_event = self.nodestore_event_map(transactions)

0 commit comments

Comments
 (0)