Skip to content

Commit 17d866a

Browse files
authored
feat(tracing): Add option to exclude specific span origins (#4463)
Allow to turn off span creation based on span origin. This is a nice-to-have that's useful when you're double instrumenting a library with e.g. Sentry and OpenTelemetry and you want to turn off Sentry spans. (On the OTel side you can also disable specific instrumentation. By adding the possibility to do this on the Sentry side of things, we enable users to pick whichever they prefer.)
1 parent 3929143 commit 17d866a

File tree

4 files changed

+152
-3
lines changed

4 files changed

+152
-3
lines changed

sentry_sdk/consts.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,7 @@ def __init__(
817817
include_local_variables: Optional[bool] = True,
818818
include_source_context: Optional[bool] = True,
819819
trace_propagation_targets: Optional[Sequence[str]] = [MATCH_ALL], # noqa: B006
820+
exclude_span_origins: Optional[Sequence[str]] = None,
820821
functions_to_trace: Sequence[Dict[str, str]] = [], # noqa: B006
821822
event_scrubber: Optional[sentry_sdk.scrubber.EventScrubber] = None,
822823
max_value_length: int = DEFAULT_MAX_VALUE_LENGTH,
@@ -1147,6 +1148,17 @@ def __init__(
11471148
If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the
11481149
instrumented client.
11491150
1151+
:param exclude_span_origins: An optional list of strings or regex patterns to disable span creation based
1152+
on span origin. When a span's origin would match any of the provided patterns, the span will not be
1153+
created.
1154+
1155+
This can be useful to exclude automatic span creation from specific integrations without disabling the
1156+
entire integration.
1157+
1158+
The option may contain a list of strings or regexes against which the span origins are matched.
1159+
String entries do not have to be full matches, meaning a span origin is matched when it contains
1160+
a string provided through the option.
1161+
11501162
:param functions_to_trace: An optional list of functions that should be set up for tracing.
11511163
11521164
For each function in the list, a span will be created when the function is executed.

sentry_sdk/tracing.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
get_sentry_meta,
3939
serialize_trace_state,
4040
)
41-
from sentry_sdk.tracing_utils import get_span_status_from_http_code
41+
from sentry_sdk.tracing_utils import (
42+
get_span_status_from_http_code,
43+
_is_span_origin_excluded,
44+
)
4245
from sentry_sdk.utils import (
4346
_serialize_span_attribute,
4447
get_current_thread_meta,
@@ -179,10 +182,13 @@ def __init__(
179182
not parent_span_context.is_valid or parent_span_context.is_remote
180183
)
181184

185+
origin = origin or DEFAULT_SPAN_ORIGIN
186+
if not skip_span and _is_span_origin_excluded(origin):
187+
skip_span = True
188+
182189
if skip_span:
183190
self._otel_span = INVALID_SPAN
184191
else:
185-
186192
if start_timestamp is not None:
187193
# OTel timestamps have nanosecond precision
188194
start_timestamp = convert_to_otel_timestamp(start_timestamp)
@@ -213,7 +219,7 @@ def __init__(
213219
attributes=attributes,
214220
)
215221

216-
self.origin = origin or DEFAULT_SPAN_ORIGIN
222+
self.origin = origin
217223
self.description = description
218224
self.name = span_name
219225

sentry_sdk/tracing_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,21 @@ def should_propagate_trace(client: sentry_sdk.client.BaseClient, url: str) -> bo
656656
return match_regex_list(url, trace_propagation_targets, substring_matching=True)
657657

658658

659+
def _is_span_origin_excluded(origin: Optional[str]) -> bool:
660+
"""
661+
Check if spans with this origin should be ignored based on the `exclude_span_origins` option.
662+
"""
663+
if origin is None:
664+
return False
665+
666+
client = sentry_sdk.get_client()
667+
exclude_span_origins = client.options.get("exclude_span_origins")
668+
if not exclude_span_origins:
669+
return False
670+
671+
return match_regex_list(origin, exclude_span_origins, substring_matching=True)
672+
673+
659674
def normalize_incoming_data(incoming_data: Dict[str, Any]) -> Dict[str, Any]:
660675
"""
661676
Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes.

tests/tracing/test_span_origin.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
from sentry_sdk import start_span
23

34

@@ -36,3 +37,118 @@ def test_span_origin_custom(sentry_init, capture_events):
3637

3738
assert second_transaction["contexts"]["trace"]["origin"] == "ho.ho2.ho3"
3839
assert second_transaction["spans"][0]["origin"] == "baz.baz2.baz3"
40+
41+
42+
@pytest.mark.parametrize("excluded_origins", [None, [], "noop"])
43+
def test_exclude_span_origins_empty(sentry_init, capture_events, excluded_origins):
44+
if excluded_origins in (None, []):
45+
sentry_init(traces_sample_rate=1.0, exclude_span_origins=excluded_origins)
46+
elif excluded_origins == "noop":
47+
sentry_init(
48+
traces_sample_rate=1.0,
49+
# default is None
50+
)
51+
52+
events = capture_events()
53+
54+
with start_span(name="span1"):
55+
pass
56+
with start_span(name="span2", origin="auto.http.requests"):
57+
pass
58+
with start_span(name="span3", origin="auto.db.postgres"):
59+
pass
60+
61+
assert len(events) == 3
62+
63+
64+
@pytest.mark.parametrize(
65+
"excluded_origins,origins,expected_allowed_origins",
66+
[
67+
# Regexes
68+
(
69+
[r"auto\.http\..*", r"auto\.db\..*"],
70+
[
71+
"auto.http.requests",
72+
"auto.db.sqlite",
73+
"manual",
74+
],
75+
["manual"],
76+
),
77+
# Substring matching
78+
(
79+
["http"],
80+
[
81+
"auto.http.requests",
82+
"http.client",
83+
"my.http.integration",
84+
"manual",
85+
"auto.db.postgres",
86+
],
87+
["manual", "auto.db.postgres"],
88+
),
89+
# Mix and match
90+
(
91+
["manual", r"auto\.http\..*", "db"],
92+
[
93+
"manual",
94+
"auto.http.requests",
95+
"auto.db.postgres",
96+
"auto.grpc.server",
97+
],
98+
["auto.grpc.server"],
99+
),
100+
],
101+
)
102+
def test_exclude_span_origins_patterns(
103+
sentry_init,
104+
capture_events,
105+
excluded_origins,
106+
origins,
107+
expected_allowed_origins,
108+
):
109+
sentry_init(
110+
traces_sample_rate=1.0,
111+
exclude_span_origins=excluded_origins,
112+
)
113+
114+
events = capture_events()
115+
116+
for origin in origins:
117+
with start_span(name="span", origin=origin):
118+
pass
119+
120+
assert len(events) == len(expected_allowed_origins)
121+
122+
if len(expected_allowed_origins) > 0:
123+
captured_origins = {event["contexts"]["trace"]["origin"] for event in events}
124+
assert captured_origins == set(expected_allowed_origins)
125+
126+
127+
def test_exclude_span_origins_with_child_spans(sentry_init, capture_events):
128+
sentry_init(traces_sample_rate=1.0, exclude_span_origins=[r"auto\.http\..*"])
129+
events = capture_events()
130+
131+
with start_span(name="parent", origin="manual"):
132+
with start_span(name="http-child", origin="auto.http.requests"):
133+
pass
134+
with start_span(name="db-child", origin="auto.db.postgres"):
135+
pass
136+
137+
assert len(events) == 1
138+
assert events[0]["contexts"]["trace"]["origin"] == "manual"
139+
assert len(events[0]["spans"]) == 1
140+
assert events[0]["spans"][0]["origin"] == "auto.db.postgres"
141+
142+
143+
def test_exclude_span_origins_parent_with_child_spans(sentry_init, capture_events):
144+
sentry_init(traces_sample_rate=1.0, exclude_span_origins=[r"auto\.http\..*"])
145+
events = capture_events()
146+
147+
with start_span(name="parent", origin="auto.http.requests"):
148+
with start_span(
149+
name="db-child", origin="auto.db.postgres", only_if_parent=True
150+
):
151+
# Note: without only_if_parent, the child span would be promoted to a transaction
152+
pass
153+
154+
assert len(events) == 0

0 commit comments

Comments
 (0)