Skip to content

Commit 0245011

Browse files
authored
feat(aiohttp): add instrumentation of client requests (#1761)
1 parent 6795295 commit 0245011

File tree

2 files changed

+165
-10
lines changed

2 files changed

+165
-10
lines changed

sentry_sdk/integrations/aiohttp.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from sentry_sdk.api import continue_trace
55
from sentry_sdk._compat import reraise
6-
from sentry_sdk.consts import OP
6+
from sentry_sdk.consts import OP, SPANDATA
77
from sentry_sdk.hub import Hub
88
from sentry_sdk.integrations import Integration, DidNotEnable
99
from sentry_sdk.integrations.logging import ignore_logger
@@ -13,20 +13,25 @@
1313
request_body_within_bounds,
1414
)
1515
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE
16+
from sentry_sdk.tracing_utils import should_propagate_trace
1617
from sentry_sdk.utils import (
1718
capture_internal_exceptions,
1819
event_from_exception,
20+
logger,
21+
parse_url,
1922
parse_version,
2023
transaction_from_function,
2124
HAS_REAL_CONTEXTVARS,
2225
CONTEXTVARS_ERROR_MESSAGE,
26+
SENSITIVE_DATA_SUBSTITUTE,
2327
AnnotatedValue,
2428
)
2529

2630
try:
2731
import asyncio
2832

2933
from aiohttp import __version__ as AIOHTTP_VERSION
34+
from aiohttp import ClientSession, TraceConfig
3035
from aiohttp.web import Application, HTTPException, UrlDispatcher
3136
except ImportError:
3237
raise DidNotEnable("AIOHTTP not installed")
@@ -36,6 +41,8 @@
3641
if TYPE_CHECKING:
3742
from aiohttp.web_request import Request
3843
from aiohttp.abc import AbstractMatchInfo
44+
from aiohttp import TraceRequestStartParams, TraceRequestEndParams
45+
from types import SimpleNamespace
3946
from typing import Any
4047
from typing import Dict
4148
from typing import Optional
@@ -164,6 +171,76 @@ async def sentry_urldispatcher_resolve(self, request):
164171

165172
UrlDispatcher.resolve = sentry_urldispatcher_resolve
166173

174+
old_client_session_init = ClientSession.__init__
175+
176+
def init(*args, **kwargs):
177+
# type: (Any, Any) -> ClientSession
178+
hub = Hub.current
179+
if hub.get_integration(AioHttpIntegration) is None:
180+
return old_client_session_init(*args, **kwargs)
181+
182+
client_trace_configs = list(kwargs.get("trace_configs", ()))
183+
trace_config = create_trace_config()
184+
client_trace_configs.append(trace_config)
185+
186+
kwargs["trace_configs"] = client_trace_configs
187+
return old_client_session_init(*args, **kwargs)
188+
189+
ClientSession.__init__ = init
190+
191+
192+
def create_trace_config():
193+
# type: () -> TraceConfig
194+
async def on_request_start(session, trace_config_ctx, params):
195+
# type: (ClientSession, SimpleNamespace, TraceRequestStartParams) -> None
196+
hub = Hub.current
197+
if hub.get_integration(AioHttpIntegration) is None:
198+
return
199+
200+
method = params.method.upper()
201+
202+
parsed_url = None
203+
with capture_internal_exceptions():
204+
parsed_url = parse_url(str(params.url), sanitize=False)
205+
206+
span = hub.start_span(
207+
op=OP.HTTP_CLIENT,
208+
description="%s %s"
209+
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
210+
)
211+
span.set_data(SPANDATA.HTTP_METHOD, method)
212+
span.set_data("url", parsed_url.url)
213+
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
214+
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
215+
216+
if should_propagate_trace(hub, str(params.url)):
217+
for key, value in hub.iter_trace_propagation_headers(span):
218+
logger.debug(
219+
"[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format(
220+
key=key, value=value, url=params.url
221+
)
222+
)
223+
params.headers[key] = value
224+
225+
trace_config_ctx.span = span
226+
227+
async def on_request_end(session, trace_config_ctx, params):
228+
# type: (ClientSession, SimpleNamespace, TraceRequestEndParams) -> None
229+
if trace_config_ctx.span is None:
230+
return
231+
232+
span = trace_config_ctx.span
233+
span.set_http_status(int(params.response.status))
234+
span.set_data("reason", params.response.reason)
235+
span.finish()
236+
237+
trace_config = TraceConfig()
238+
239+
trace_config.on_request_start.append(on_request_start)
240+
trace_config.on_request_end.append(on_request_end)
241+
242+
return trace_config
243+
167244

168245
def _make_request_processor(weak_request):
169246
# type: (Callable[[], Request]) -> EventProcessor

tests/integrations/aiohttp/test_aiohttp.py

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from aiohttp.client import ServerDisconnectedError
88
from aiohttp.web_request import Request
99

10-
from sentry_sdk import capture_message
10+
from sentry_sdk import capture_message, start_transaction
1111
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
1212

1313
try:
@@ -54,6 +54,8 @@ async def hello(request):
5454
"Accept-Encoding": "gzip, deflate",
5555
"Host": host,
5656
"User-Agent": request["headers"]["User-Agent"],
57+
"baggage": mock.ANY,
58+
"sentry-trace": mock.ANY,
5759
}
5860

5961

@@ -372,11 +374,13 @@ async def hello(request):
372374

373375
events = capture_events()
374376

375-
trace_id = "582b43a4192642f0b136d5159a501701"
376-
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
377-
377+
# The aiohttp_client is instrumented so will generate the sentry-trace header and add request.
378+
# Get the sentry-trace header from the request so we can later compare with transaction events.
378379
client = await aiohttp_client(app)
379-
resp = await client.get("/", headers={"sentry-trace": sentry_trace_header})
380+
resp = await client.get("/")
381+
sentry_trace_header = resp.request_info.headers.get("sentry-trace")
382+
trace_id = sentry_trace_header.split("-")[0]
383+
380384
assert resp.status == 500
381385

382386
msg_event, error_event, transaction_event = events
@@ -410,11 +414,13 @@ async def hello(request):
410414

411415
events = capture_events()
412416

413-
trace_id = "582b43a4192642f0b136d5159a501701"
414-
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
415-
417+
# The aiohttp_client is instrumented so will generate the sentry-trace header and add request.
418+
# Get the sentry-trace header from the request so we can later compare with transaction events.
416419
client = await aiohttp_client(app)
417-
resp = await client.get("/", headers={"sentry-trace": sentry_trace_header})
420+
resp = await client.get("/")
421+
sentry_trace_header = resp.request_info.headers.get("sentry-trace")
422+
trace_id = sentry_trace_header.split("-")[0]
423+
418424
assert resp.status == 500
419425

420426
msg_event, error_event = events
@@ -427,3 +433,75 @@ async def hello(request):
427433

428434
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
429435
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
436+
437+
438+
@pytest.mark.asyncio
439+
async def test_crumb_capture(
440+
sentry_init, aiohttp_raw_server, aiohttp_client, loop, capture_events
441+
):
442+
def before_breadcrumb(crumb, hint):
443+
crumb["data"]["extra"] = "foo"
444+
return crumb
445+
446+
sentry_init(
447+
integrations=[AioHttpIntegration()], before_breadcrumb=before_breadcrumb
448+
)
449+
450+
async def handler(request):
451+
return web.Response(text="OK")
452+
453+
raw_server = await aiohttp_raw_server(handler)
454+
455+
with start_transaction():
456+
events = capture_events()
457+
458+
client = await aiohttp_client(raw_server)
459+
resp = await client.get("/")
460+
assert resp.status == 200
461+
capture_message("Testing!")
462+
463+
(event,) = events
464+
465+
crumb = event["breadcrumbs"]["values"][0]
466+
assert crumb["type"] == "http"
467+
assert crumb["category"] == "httplib"
468+
assert crumb["data"] == {
469+
"url": "http://127.0.0.1:{}/".format(raw_server.port),
470+
"http.fragment": "",
471+
"http.method": "GET",
472+
"http.query": "",
473+
"http.response.status_code": 200,
474+
"reason": "OK",
475+
"extra": "foo",
476+
}
477+
478+
479+
@pytest.mark.asyncio
480+
async def test_outgoing_trace_headers(sentry_init, aiohttp_raw_server, aiohttp_client):
481+
sentry_init(
482+
integrations=[AioHttpIntegration()],
483+
traces_sample_rate=1.0,
484+
)
485+
486+
async def handler(request):
487+
return web.Response(text="OK")
488+
489+
raw_server = await aiohttp_raw_server(handler)
490+
491+
with start_transaction(
492+
name="/interactions/other-dogs/new-dog",
493+
op="greeting.sniff",
494+
# make trace_id difference between transactions
495+
trace_id="0123456789012345678901234567890",
496+
) as transaction:
497+
client = await aiohttp_client(raw_server)
498+
resp = await client.get("/")
499+
request_span = transaction._span_recorder.spans[-1]
500+
501+
assert resp.request_info.headers[
502+
"sentry-trace"
503+
] == "{trace_id}-{parent_span_id}-{sampled}".format(
504+
trace_id=transaction.trace_id,
505+
parent_span_id=request_span.span_id,
506+
sampled=1,
507+
)

0 commit comments

Comments
 (0)