diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 50d08991c6..a55f9024a6 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -649,6 +649,7 @@ def __init__( trace_propagation_targets=[ # noqa: B006 MATCH_ALL ], # type: Optional[Sequence[str]] + exclude_span_origins=None, # type: Optional[Sequence[str]] functions_to_trace=[], # type: Sequence[Dict[str, str]] # noqa: B006 event_scrubber=None, # type: Optional[sentry_sdk.scrubber.EventScrubber] max_value_length=DEFAULT_MAX_VALUE_LENGTH, # type: int @@ -980,6 +981,17 @@ def __init__( If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the instrumented client. + :param exclude_span_origins: An optional list of strings or regex patterns to disable span creation based + on span origin. When a span's origin would match any of the provided patterns, the span will not be + created. + + This can be useful to exclude automatic span creation from specific integrations without disabling the + entire integration. + + The option may contain a list of strings or regexes against which the span origins are matched. + String entries do not have to be full matches, meaning a span origin is matched when it contains + a string provided through the option. + :param functions_to_trace: An optional list of functions that should be set up for tracing. For each function in the list, a span will be created when the function is executed. diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index f15f07065a..0ac5c29a4f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -37,7 +37,10 @@ get_sentry_meta, serialize_trace_state, ) -from sentry_sdk.tracing_utils import get_span_status_from_http_code +from sentry_sdk.tracing_utils import ( + get_span_status_from_http_code, + _is_span_origin_excluded, +) from sentry_sdk.utils import ( _serialize_span_attribute, get_current_thread_meta, @@ -205,10 +208,13 @@ def __init__( not parent_span_context.is_valid or parent_span_context.is_remote ) + origin = origin or DEFAULT_SPAN_ORIGIN + if not skip_span and _is_span_origin_excluded(origin): + skip_span = True + if skip_span: self._otel_span = INVALID_SPAN else: - if start_timestamp is not None: # OTel timestamps have nanosecond precision start_timestamp = convert_to_otel_timestamp(start_timestamp) @@ -239,7 +245,7 @@ def __init__( attributes=attributes, ) - self.origin = origin or DEFAULT_SPAN_ORIGIN + self.origin = origin self.description = description self.name = span_name diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 140ce57139..4f5a4ceb6d 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -689,6 +689,22 @@ def should_propagate_trace(client, url): return match_regex_list(url, trace_propagation_targets, substring_matching=True) +def _is_span_origin_excluded(origin): + # type: (Optional[str]) -> bool + """ + Check if spans with this origin should be ignored based on the `exclude_span_origins` option. + """ + if origin is None: + return False + + client = sentry_sdk.get_client() + exclude_span_origins = client.options.get("exclude_span_origins") + if not exclude_span_origins: + return False + + return match_regex_list(origin, exclude_span_origins, substring_matching=True) + + def normalize_incoming_data(incoming_data): # type: (Dict[str, Any]) -> Dict[str, Any] """ diff --git a/tests/tracing/test_span_origin.py b/tests/tracing/test_span_origin.py index 649f704b1b..7aaaa09e9a 100644 --- a/tests/tracing/test_span_origin.py +++ b/tests/tracing/test_span_origin.py @@ -1,3 +1,4 @@ +import pytest from sentry_sdk import start_span @@ -36,3 +37,118 @@ def test_span_origin_custom(sentry_init, capture_events): assert second_transaction["contexts"]["trace"]["origin"] == "ho.ho2.ho3" assert second_transaction["spans"][0]["origin"] == "baz.baz2.baz3" + + +@pytest.mark.parametrize("excluded_origins", [None, [], "noop"]) +def test_exclude_span_origins_empty(sentry_init, capture_events, excluded_origins): + if excluded_origins in (None, []): + sentry_init(traces_sample_rate=1.0, exclude_span_origins=excluded_origins) + elif excluded_origins == "noop": + sentry_init( + traces_sample_rate=1.0, + # default is None + ) + + events = capture_events() + + with start_span(name="span1"): + pass + with start_span(name="span2", origin="auto.http.requests"): + pass + with start_span(name="span3", origin="auto.db.postgres"): + pass + + assert len(events) == 3 + + +@pytest.mark.parametrize( + "excluded_origins,origins,expected_allowed_origins", + [ + # Regexes + ( + [r"auto\.http\..*", r"auto\.db\..*"], + [ + "auto.http.requests", + "auto.db.sqlite", + "manual", + ], + ["manual"], + ), + # Substring matching + ( + ["http"], + [ + "auto.http.requests", + "http.client", + "my.http.integration", + "manual", + "auto.db.postgres", + ], + ["manual", "auto.db.postgres"], + ), + # Mix and match + ( + ["manual", r"auto\.http\..*", "db"], + [ + "manual", + "auto.http.requests", + "auto.db.postgres", + "auto.grpc.server", + ], + ["auto.grpc.server"], + ), + ], +) +def test_exclude_span_origins_patterns( + sentry_init, + capture_events, + excluded_origins, + origins, + expected_allowed_origins, +): + sentry_init( + traces_sample_rate=1.0, + exclude_span_origins=excluded_origins, + ) + + events = capture_events() + + for origin in origins: + with start_span(name="span", origin=origin): + pass + + assert len(events) == len(expected_allowed_origins) + + if len(expected_allowed_origins) > 0: + captured_origins = {event["contexts"]["trace"]["origin"] for event in events} + assert captured_origins == set(expected_allowed_origins) + + +def test_exclude_span_origins_with_child_spans(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0, exclude_span_origins=[r"auto\.http\..*"]) + events = capture_events() + + with start_span(name="parent", origin="manual"): + with start_span(name="http-child", origin="auto.http.requests"): + pass + with start_span(name="db-child", origin="auto.db.postgres"): + pass + + assert len(events) == 1 + assert events[0]["contexts"]["trace"]["origin"] == "manual" + assert len(events[0]["spans"]) == 1 + assert events[0]["spans"][0]["origin"] == "auto.db.postgres" + + +def test_exclude_span_origins_parent_with_child_spans(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0, exclude_span_origins=[r"auto\.http\..*"]) + events = capture_events() + + with start_span(name="parent", origin="auto.http.requests"): + with start_span( + name="db-child", origin="auto.db.postgres", only_if_parent=True + ): + # Note: without only_if_parent, the child span would be promoted to a transaction + pass + + assert len(events) == 0