Skip to content

feat(tracing): Add option to exclude specific span origins #4463

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: potel-base
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 9 additions & 3 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
"""
Expand Down
116 changes: 116 additions & 0 deletions tests/tracing/test_span_origin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from sentry_sdk import start_span


Expand Down Expand Up @@ -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
Loading