Skip to content

Commit cab17a4

Browse files
feat: Add source information for slow outgoing HTTP requests (#4902)
Add code source attributes to outgoing HTTP requests as described in getsentry/sentry-docs#15161. The attributes are only added if the time to receive a response to an HTTP request exceeds a configurable threshold value. Factors out functionality from SQL query source and tests that it works in the HTTP request setting. Closes #4881
1 parent f8b9069 commit cab17a4

File tree

17 files changed

+1021
-32
lines changed

17 files changed

+1021
-32
lines changed

sentry_sdk/consts.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,8 @@ def __init__(
909909
error_sampler=None, # type: Optional[Callable[[Event, Hint], Union[float, bool]]]
910910
enable_db_query_source=True, # type: bool
911911
db_query_source_threshold_ms=100, # type: int
912+
enable_http_request_source=False, # type: bool
913+
http_request_source_threshold_ms=100, # type: int
912914
spotlight=None, # type: Optional[Union[bool, str]]
913915
cert_file=None, # type: Optional[str]
914916
key_file=None, # type: Optional[str]
@@ -1264,6 +1266,13 @@ def __init__(
12641266
12651267
The query location will be added to the query for queries slower than the specified threshold.
12661268
1269+
:param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests.
1270+
1271+
:param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an
1272+
outgoing HTTP request.
1273+
1274+
The request location will be added to the request for requests slower than the specified threshold.
1275+
12671276
:param custom_repr: A custom `repr <https://docs.python.org/3/library/functions.html#repr>`_ function to run
12681277
while serializing an object.
12691278

sentry_sdk/integrations/aiohttp.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
SOURCE_FOR_STYLE,
2323
TransactionSource,
2424
)
25-
from sentry_sdk.tracing_utils import should_propagate_trace
25+
from sentry_sdk.tracing_utils import should_propagate_trace, add_http_request_source
2626
from sentry_sdk.utils import (
2727
capture_internal_exceptions,
2828
ensure_integration_enabled,
@@ -279,6 +279,9 @@ async def on_request_end(session, trace_config_ctx, params):
279279
span.set_data("reason", params.response.reason)
280280
span.finish()
281281

282+
with capture_internal_exceptions():
283+
add_http_request_source(span)
284+
282285
trace_config = TraceConfig()
283286

284287
trace_config.on_request_start.append(on_request_start)

sentry_sdk/integrations/httpx.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import sentry_sdk
2+
from sentry_sdk import start_span
23
from sentry_sdk.consts import OP, SPANDATA
34
from sentry_sdk.integrations import Integration, DidNotEnable
45
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME
5-
from sentry_sdk.tracing_utils import Baggage, should_propagate_trace
6+
from sentry_sdk.tracing_utils import (
7+
Baggage,
8+
should_propagate_trace,
9+
add_http_request_source,
10+
)
611
from sentry_sdk.utils import (
712
SENSITIVE_DATA_SUBSTITUTE,
813
capture_internal_exceptions,
@@ -52,7 +57,7 @@ def send(self, request, **kwargs):
5257
with capture_internal_exceptions():
5358
parsed_url = parse_url(str(request.url), sanitize=False)
5459

55-
with sentry_sdk.start_span(
60+
with start_span(
5661
op=OP.HTTP_CLIENT,
5762
name="%s %s"
5863
% (
@@ -88,7 +93,10 @@ def send(self, request, **kwargs):
8893
span.set_http_status(rv.status_code)
8994
span.set_data("reason", rv.reason_phrase)
9095

91-
return rv
96+
with capture_internal_exceptions():
97+
add_http_request_source(span)
98+
99+
return rv
92100

93101
Client.send = send
94102

@@ -106,7 +114,7 @@ async def send(self, request, **kwargs):
106114
with capture_internal_exceptions():
107115
parsed_url = parse_url(str(request.url), sanitize=False)
108116

109-
with sentry_sdk.start_span(
117+
with start_span(
110118
op=OP.HTTP_CLIENT,
111119
name="%s %s"
112120
% (
@@ -144,7 +152,10 @@ async def send(self, request, **kwargs):
144152
span.set_http_status(rv.status_code)
145153
span.set_data("reason", rv.reason_phrase)
146154

147-
return rv
155+
with capture_internal_exceptions():
156+
add_http_request_source(span)
157+
158+
return rv
148159

149160
AsyncClient.send = send
150161

sentry_sdk/integrations/stdlib.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
from sentry_sdk.consts import OP, SPANDATA
99
from sentry_sdk.integrations import Integration
1010
from sentry_sdk.scope import add_global_event_processor
11-
from sentry_sdk.tracing_utils import EnvironHeaders, should_propagate_trace
11+
from sentry_sdk.tracing_utils import (
12+
EnvironHeaders,
13+
should_propagate_trace,
14+
add_http_request_source,
15+
)
1216
from sentry_sdk.utils import (
1317
SENSITIVE_DATA_SUBSTITUTE,
1418
capture_internal_exceptions,
@@ -135,6 +139,9 @@ def getresponse(self, *args, **kwargs):
135139
finally:
136140
span.finish()
137141

142+
with capture_internal_exceptions():
143+
add_http_request_source(span)
144+
138145
return rv
139146

140147
HTTPConnection.putrequest = putrequest # type: ignore[method-assign]

sentry_sdk/tracing_utils.py

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -218,33 +218,11 @@ def _should_be_included(
218218
)
219219

220220

221-
def add_query_source(span):
222-
# type: (sentry_sdk.tracing.Span) -> None
221+
def add_source(span, project_root, in_app_include, in_app_exclude):
222+
# type: (sentry_sdk.tracing.Span, Optional[str], Optional[list[str]], Optional[list[str]]) -> None
223223
"""
224224
Adds OTel compatible source code information to the span
225225
"""
226-
client = sentry_sdk.get_client()
227-
if not client.is_active():
228-
return
229-
230-
if span.timestamp is None or span.start_timestamp is None:
231-
return
232-
233-
should_add_query_source = client.options.get("enable_db_query_source", True)
234-
if not should_add_query_source:
235-
return
236-
237-
duration = span.timestamp - span.start_timestamp
238-
threshold = client.options.get("db_query_source_threshold_ms", 0)
239-
slow_query = duration / timedelta(milliseconds=1) > threshold
240-
241-
if not slow_query:
242-
return
243-
244-
project_root = client.options["project_root"]
245-
in_app_include = client.options.get("in_app_include")
246-
in_app_exclude = client.options.get("in_app_exclude")
247-
248226
# Find the correct frame
249227
frame = sys._getframe() # type: Union[FrameType, None]
250228
while frame is not None:
@@ -309,6 +287,68 @@ def add_query_source(span):
309287
span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name)
310288

311289

290+
def add_query_source(span):
291+
# type: (sentry_sdk.tracing.Span) -> None
292+
"""
293+
Adds OTel compatible source code information to a database query span
294+
"""
295+
client = sentry_sdk.get_client()
296+
if not client.is_active():
297+
return
298+
299+
if span.timestamp is None or span.start_timestamp is None:
300+
return
301+
302+
should_add_query_source = client.options.get("enable_db_query_source", True)
303+
if not should_add_query_source:
304+
return
305+
306+
duration = span.timestamp - span.start_timestamp
307+
threshold = client.options.get("db_query_source_threshold_ms", 0)
308+
slow_query = duration / timedelta(milliseconds=1) > threshold
309+
310+
if not slow_query:
311+
return
312+
313+
add_source(
314+
span=span,
315+
project_root=client.options["project_root"],
316+
in_app_include=client.options.get("in_app_include"),
317+
in_app_exclude=client.options.get("in_app_exclude"),
318+
)
319+
320+
321+
def add_http_request_source(span):
322+
# type: (sentry_sdk.tracing.Span) -> None
323+
"""
324+
Adds OTel compatible source code information to a span for an outgoing HTTP request
325+
"""
326+
client = sentry_sdk.get_client()
327+
if not client.is_active():
328+
return
329+
330+
if span.timestamp is None or span.start_timestamp is None:
331+
return
332+
333+
should_add_request_source = client.options.get("enable_http_request_source", False)
334+
if not should_add_request_source:
335+
return
336+
337+
duration = span.timestamp - span.start_timestamp
338+
threshold = client.options.get("http_request_source_threshold_ms", 0)
339+
slow_query = duration / timedelta(milliseconds=1) > threshold
340+
341+
if not slow_query:
342+
return
343+
344+
add_source(
345+
span=span,
346+
project_root=client.options["project_root"],
347+
in_app_include=client.options.get("in_app_include"),
348+
in_app_exclude=client.options.get("in_app_exclude"),
349+
)
350+
351+
312352
def extract_sentrytrace_data(header):
313353
# type: (Optional[str]) -> Optional[Dict[str, Union[str, bool, None]]]
314354
"""
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import os
2+
import sys
13
import pytest
24

35
pytest.importorskip("aiohttp")
6+
7+
# Load `aiohttp_helpers` into the module search path to test request source path names relative to module. See
8+
# `test_request_source_with_module_in_search_path`
9+
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))

tests/integrations/aiohttp/aiohttp_helpers/__init__.py

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
async def get_request_with_client(client, url):
2+
await client.get(url)

0 commit comments

Comments
 (0)