Skip to content

Commit 2a0814b

Browse files
wmakandrewshie-sentry
authored andcommitted
feat(eap): Add downsampling to the api (#88023)
- This updates the events/ api to accept a sampling param so we can make a preflight before trying the new best_effort - Tests are not really ready, but opening this for now so FE can start sorta testing at least
1 parent fa17e15 commit 2a0814b

File tree

12 files changed

+103
-2
lines changed

12 files changed

+103
-2
lines changed

src/sentry/api/endpoints/organization_events.py

+2
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ def get(self, request: Request, organization) -> Response:
421421

422422
dataset = self.get_dataset(request)
423423
metrics_enhanced = dataset in {metrics_performance, metrics_enhanced_performance}
424+
sampling_mode = request.GET.get("sampling")
424425

425426
sentry_sdk.set_tag("performance.metrics_enhanced", metrics_enhanced)
426427
allow_metric_aggregates = request.GET.get("preventMetricAggregates") != "1"
@@ -465,6 +466,7 @@ def _data_fn(
465466
auto_fields=True,
466467
use_aggregate_conditions=use_aggregate_conditions,
467468
),
469+
sampling_mode=sampling_mode,
468470
)
469471
query_source = self.get_request_source(request)
470472
return dataset_query(

src/sentry/incidents/logic.py

+1
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ def get_metric_issue_aggregates(
404404
offset=0,
405405
limit=1,
406406
referrer=Referrer.API_ALERTS_ALERT_RULE_CHART.value,
407+
sampling_mode=None,
407408
config=SearchResolverConfig(
408409
auto_fields=True,
409410
),

src/sentry/profiles/flamegraph.py

+1
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ def get_spans_based_candidates(self, query: str | None, limit: int) -> EAPRespon
568568
offset=0,
569569
limit=limit,
570570
referrer=Referrer.API_TRACE_EXPLORER_TRACE_SPANS_CANDIDATES_FLAMEGRAPH.value,
571+
sampling_mode=None,
571572
config=SearchResolverConfig(
572573
auto_fields=True,
573574
),

src/sentry/search/eap/constants.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Literal
22

3+
from sentry_protos.snuba.v1.downsampled_storage_pb2 import DownsampledStorageConfig
34
from sentry_protos.snuba.v1.endpoint_trace_item_table_pb2 import AggregationComparisonFilter
45
from sentry_protos.snuba.v1.request_common_pb2 import TraceItemType
56
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey
@@ -162,3 +163,8 @@
162163
],
163164
5: ["500", "501", "502", "503", "504", "505", "506", "507", "508", "509", "510", "511"],
164165
}
166+
167+
SAMPLING_MODES = {
168+
"BEST_EFFORT": DownsampledStorageConfig.MODE_BEST_EFFORT,
169+
"PREFLIGHT": DownsampledStorageConfig.MODE_PREFLIGHT,
170+
}

src/sentry/search/eap/resolver.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
VirtualColumnDefinition,
4848
)
4949
from sentry.search.eap.types import SearchResolverConfig
50+
from sentry.search.eap.utils import validate_sampling
5051
from sentry.search.events import constants as qb_constants
5152
from sentry.search.events import fields
5253
from sentry.search.events import filter as event_filter
@@ -76,7 +77,7 @@ class SearchResolver:
7677
] = field(default_factory=dict)
7778

7879
@sentry_sdk.trace
79-
def resolve_meta(self, referrer: str) -> RequestMeta:
80+
def resolve_meta(self, referrer: str, sampling_mode: str | None = None) -> RequestMeta:
8081
if self.params.organization_id is None:
8182
raise Exception("An organization is required to resolve queries")
8283
span = sentry_sdk.get_current_span()
@@ -89,6 +90,7 @@ def resolve_meta(self, referrer: str) -> RequestMeta:
8990
start_timestamp=self.params.rpc_start_date,
9091
end_timestamp=self.params.rpc_end_date,
9192
trace_item_type=self.definitions.trace_item_type,
93+
downsampled_storage_config=validate_sampling(sampling_mode),
9294
)
9395

9496
@sentry_sdk.trace

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

+1
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ def time_spent_percentage(
422422
orderby=None,
423423
offset=0,
424424
limit=1,
425+
sampling_mode=None,
425426
config=SearchResolverConfig(),
426427
)
427428

src/sentry/search/eap/utils.py

+12
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from typing import Any, Literal
44

55
from google.protobuf.timestamp_pb2 import Timestamp
6+
from sentry_protos.snuba.v1.downsampled_storage_pb2 import DownsampledStorageConfig
67
from sentry_protos.snuba.v1.endpoint_time_series_pb2 import Expression, TimeSeriesRequest
78
from sentry_protos.snuba.v1.endpoint_trace_item_table_pb2 import Column
89
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import Function
910

1011
from sentry.exceptions import InvalidSearchQuery
12+
from sentry.search.eap.constants import SAMPLING_MODES
1113
from sentry.search.eap.ourlogs.attributes import LOGS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS
1214
from sentry.search.eap.spans.attributes import SPANS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS
1315
from sentry.search.eap.types import SupportedTraceItemType
@@ -81,6 +83,16 @@ def transform_column_to_expression(column: Column) -> Expression:
8183
)
8284

8385

86+
def validate_sampling(sampling_mode: str | None) -> DownsampledStorageConfig:
87+
if sampling_mode is None:
88+
return DownsampledStorageConfig(mode=DownsampledStorageConfig.MODE_UNSPECIFIED)
89+
sampling_mode = sampling_mode.upper()
90+
if sampling_mode not in SAMPLING_MODES:
91+
raise InvalidSearchQuery(f"sampling mode: {sampling_mode} is not supported")
92+
else:
93+
return DownsampledStorageConfig(mode=SAMPLING_MODES[sampling_mode])
94+
95+
8496
INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS: dict[
8597
SupportedTraceItemType, dict[Literal["string", "number"], dict[str, str]]
8698
] = {

src/sentry/snuba/ourlogs.py

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def query(
5858
offset=offset or 0,
5959
limit=limit,
6060
referrer=referrer or "referrer unset",
61+
sampling_mode=None,
6162
resolver=get_resolver(
6263
params=snuba_params,
6364
config=SearchResolverConfig(

src/sentry/snuba/rpc_dataset_common.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ def run_table_query(
4343
offset: int,
4444
limit: int,
4545
referrer: str,
46+
sampling_mode: str | None,
4647
resolver: SearchResolver,
4748
debug: bool = False,
4849
) -> EAPResponse:
4950
"""Make the query"""
50-
meta = resolver.resolve_meta(referrer=referrer)
51+
sentry_sdk.set_tag("query.sampling_mode", sampling_mode)
52+
meta = resolver.resolve_meta(referrer=referrer, sampling_mode=sampling_mode)
5153
where, having, query_contexts = resolver.resolve_query(query_string)
5254
columns, column_contexts = resolver.resolve_columns(selected_columns)
5355
contexts = resolver.resolve_contexts(query_contexts + column_contexts)
@@ -98,6 +100,7 @@ def run_table_query(
98100
virtual_column_contexts=[context for context in contexts if context is not None],
99101
)
100102
rpc_response = snuba_rpc.table_rpc([rpc_request])[0]
103+
sentry_sdk.set_tag("query.storage_meta.tier", rpc_response.meta.downsampled_storage_meta.tier)
101104

102105
"""Process the results"""
103106
final_data: SnubaData = []

src/sentry/snuba/spans_rpc.py

+3
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def run_table_query(
8484
limit: int,
8585
referrer: str,
8686
config: SearchResolverConfig,
87+
sampling_mode: str | None,
8788
search_resolver: SearchResolver | None = None,
8889
debug: bool = False,
8990
) -> EAPResponse:
@@ -94,6 +95,7 @@ def run_table_query(
9495
offset,
9596
limit,
9697
referrer,
98+
sampling_mode,
9799
search_resolver or get_resolver(params, config),
98100
debug,
99101
)
@@ -298,6 +300,7 @@ def run_top_events_timeseries_query(
298300
limit,
299301
referrer,
300302
config,
303+
None,
301304
search_resolver,
302305
)
303306
if len(top_events["data"]) == 0:

src/sentry/snuba/uptime_checks.py

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def query(
5757
offset=offset or 0,
5858
limit=limit,
5959
referrer=referrer or "referrer unset",
60+
sampling_mode=None,
6061
resolver=get_resolver(
6162
params=snuba_params,
6263
config=SearchResolverConfig(

tests/snuba/api/endpoints/test_organization_events_span_indexed.py

+68
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
from sentry.testutils.helpers import parse_link_header
99
from tests.snuba.api.endpoints.test_organization_events import OrganizationEventsEndpointTestBase
1010

11+
# Downsampling is deterministic, so unless the algorithm changes we can find a known id that will appear in the
12+
# preflight and it will always show up
13+
# If we need to get a new ID just query for event ids after loading 100s of events and use any of the ids that come back
14+
KNOWN_PREFLIGHT_ID = "ca056dd858a24299"
15+
1116

1217
class OrganizationEventsSpanIndexedEndpointTest(OrganizationEventsEndpointTestBase):
1318
is_eap = False
@@ -3522,3 +3527,66 @@ def test_filtering_null_numeric_attr(self):
35223527
},
35233528
]
35243529
assert meta["dataset"] == self.dataset
3530+
3531+
def test_preflight_request(self):
3532+
span = self.create_span(
3533+
{"description": "foo", "sentry_tags": {"status": "success"}},
3534+
start_ts=self.ten_mins_ago,
3535+
)
3536+
span["span_id"] = KNOWN_PREFLIGHT_ID
3537+
span2 = self.create_span(
3538+
{"description": "zoo", "sentry_tags": {"status": "success"}},
3539+
start_ts=self.ten_mins_ago,
3540+
)
3541+
span2["span_id"] = "b" * 16
3542+
self.store_spans(
3543+
[span, span2],
3544+
is_eap=self.is_eap,
3545+
)
3546+
response = self.do_request(
3547+
{
3548+
"field": ["id", "description", "count()"],
3549+
"query": "",
3550+
"orderby": "description",
3551+
"project": self.project.id,
3552+
"dataset": self.dataset,
3553+
"statsPeriod": "1h",
3554+
"sampling": "PREFLIGHT",
3555+
}
3556+
)
3557+
3558+
assert response.status_code == 200, response.content
3559+
assert len(response.data["data"]) == 1
3560+
assert response.data["data"][0]["id"] == KNOWN_PREFLIGHT_ID
3561+
3562+
def test_best_effort_request(self):
3563+
span = self.create_span(
3564+
{"description": "foo", "sentry_tags": {"status": "success"}},
3565+
start_ts=self.ten_mins_ago,
3566+
)
3567+
span["span_id"] = KNOWN_PREFLIGHT_ID
3568+
span2 = self.create_span(
3569+
{"description": "zoo", "sentry_tags": {"status": "success"}},
3570+
start_ts=self.ten_mins_ago,
3571+
)
3572+
span2["span_id"] = "b" * 16
3573+
self.store_spans(
3574+
[span, span2],
3575+
is_eap=self.is_eap,
3576+
)
3577+
response = self.do_request(
3578+
{
3579+
"field": ["id", "description", "count()"],
3580+
"query": "",
3581+
"orderby": "description",
3582+
"project": self.project.id,
3583+
"dataset": self.dataset,
3584+
"statsPeriod": "1h",
3585+
"sampling": "BEST_EFFORT",
3586+
}
3587+
)
3588+
3589+
assert response.status_code == 200, response.content
3590+
assert len(response.data["data"]) == 2
3591+
assert response.data["data"][0]["id"] == KNOWN_PREFLIGHT_ID
3592+
assert response.data["data"][1]["id"] == "b" * 16

0 commit comments

Comments
 (0)