Skip to content

Commit fabba69

Browse files
authored
feat(starlette): add Starlette integration (#1441)
Adds integrations for Starlette and FastAPI. The majority of functionaly is in the Starlette integration. The FastAPI integration is just setting transaction names because those are handled differently in Starlette and FastAPI.
1 parent bd48df2 commit fabba69

File tree

15 files changed

+1359
-72
lines changed

15 files changed

+1359
-72
lines changed

mypy.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,7 @@ disallow_untyped_defs = False
6363
ignore_missing_imports = True
6464
[mypy-flask.signals]
6565
ignore_missing_imports = True
66+
[mypy-starlette.*]
67+
ignore_missing_imports = True
68+
[mypy-fastapi.*]
69+
ignore_missing_imports = True

pytest.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ DJANGO_SETTINGS_MODULE = tests.integrations.django.myapp.settings
33
addopts = --tb=short
44
markers =
55
tests_internal_exceptions: Handle internal exceptions just as the SDK does, to test it. (Otherwise internal exceptions are recorded and reraised.)
6-
only: A temporary marker, to make pytest only run the tests with the mark, similar to jest's `it.only`. To use, run `pytest -v -m only`.
6+
only: A temporary marker, to make pytest only run the tests with the mark, similar to jests `it.only`. To use, run `pytest -v -m only`.
7+
asyncio_mode = strict
78

89
[pytest-watch]
910
; Enable this to drop into pdb on errors

sentry_sdk/integrations/asgi.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@
1616
from sentry_sdk.tracing import (
1717
SOURCE_FOR_STYLE,
1818
TRANSACTION_SOURCE_ROUTE,
19-
TRANSACTION_SOURCE_UNKNOWN,
2019
)
2120
from sentry_sdk.utils import (
2221
ContextVar,
2322
event_from_exception,
24-
transaction_from_function,
2523
HAS_REAL_CONTEXTVARS,
2624
CONTEXTVARS_ERROR_MESSAGE,
25+
transaction_from_function,
2726
)
2827
from sentry_sdk.tracing import Transaction
2928

@@ -45,15 +44,15 @@
4544
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
4645

4746

48-
def _capture_exception(hub, exc):
49-
# type: (Hub, Any) -> None
47+
def _capture_exception(hub, exc, mechanism_type="asgi"):
48+
# type: (Hub, Any, str) -> None
5049

5150
# Check client here as it might have been unset while streaming response
5251
if hub.client is not None:
5352
event, hint = event_from_exception(
5453
exc,
5554
client_options=hub.client.options,
56-
mechanism={"type": "asgi", "handled": False},
55+
mechanism={"type": mechanism_type, "handled": False},
5756
)
5857
hub.capture_event(event, hint=hint)
5958

@@ -75,10 +74,16 @@ def _looks_like_asgi3(app):
7574

7675

7776
class SentryAsgiMiddleware:
78-
__slots__ = ("app", "__call__", "transaction_style")
79-
80-
def __init__(self, app, unsafe_context_data=False, transaction_style="endpoint"):
81-
# type: (Any, bool, str) -> None
77+
__slots__ = ("app", "__call__", "transaction_style", "mechanism_type")
78+
79+
def __init__(
80+
self,
81+
app,
82+
unsafe_context_data=False,
83+
transaction_style="endpoint",
84+
mechanism_type="asgi",
85+
):
86+
# type: (Any, bool, str, str) -> None
8287
"""
8388
Instrument an ASGI application with Sentry. Provides HTTP/websocket
8489
data to sent events and basic handling for exceptions bubbling up
@@ -100,6 +105,7 @@ def __init__(self, app, unsafe_context_data=False, transaction_style="endpoint")
100105
% (transaction_style, TRANSACTION_STYLE_VALUES)
101106
)
102107
self.transaction_style = transaction_style
108+
self.mechanism_type = mechanism_type
103109
self.app = app
104110

105111
if _looks_like_asgi3(app):
@@ -127,7 +133,7 @@ async def _run_app(self, scope, callback):
127133
try:
128134
return await callback()
129135
except Exception as exc:
130-
_capture_exception(Hub.current, exc)
136+
_capture_exception(Hub.current, exc, mechanism_type=self.mechanism_type)
131137
raise exc from None
132138

133139
_asgi_middleware_applied.set(True)
@@ -164,7 +170,9 @@ async def _run_app(self, scope, callback):
164170
try:
165171
return await callback()
166172
except Exception as exc:
167-
_capture_exception(hub, exc)
173+
_capture_exception(
174+
hub, exc, mechanism_type=self.mechanism_type
175+
)
168176
raise exc from None
169177
finally:
170178
_asgi_middleware_applied.set(False)
@@ -203,7 +211,6 @@ def event_processor(self, event, hint, asgi_scope):
203211

204212
def _set_transaction_name_and_source(self, event, transaction_style, asgi_scope):
205213
# type: (Event, str, Any) -> None
206-
207214
transaction_name_already_set = (
208215
event.get("transaction", _DEFAULT_TRANSACTION_NAME)
209216
!= _DEFAULT_TRANSACTION_NAME
@@ -231,9 +238,8 @@ def _set_transaction_name_and_source(self, event, transaction_style, asgi_scope)
231238
name = path
232239

233240
if not name:
234-
# If no transaction name can be found set an unknown source.
235-
# This can happen when ASGI frameworks that are not yet supported well are used.
236-
event["transaction_info"] = {"source": TRANSACTION_SOURCE_UNKNOWN}
241+
event["transaction"] = _DEFAULT_TRANSACTION_NAME
242+
event["transaction_info"] = {"source": TRANSACTION_SOURCE_ROUTE}
237243
return
238244

239245
event["transaction"] = name

sentry_sdk/integrations/fastapi.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from sentry_sdk._types import MYPY
2+
from sentry_sdk.hub import Hub
3+
from sentry_sdk.integrations import DidNotEnable
4+
from sentry_sdk.integrations.starlette import (
5+
SentryStarletteMiddleware,
6+
StarletteIntegration,
7+
)
8+
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE
9+
from sentry_sdk.utils import transaction_from_function
10+
11+
if MYPY:
12+
from typing import Any, Callable, Dict
13+
14+
from sentry_sdk._types import Event
15+
16+
try:
17+
from fastapi.applications import FastAPI
18+
from fastapi.requests import Request
19+
except ImportError:
20+
raise DidNotEnable("FastAPI is not installed")
21+
22+
try:
23+
from starlette.types import ASGIApp, Receive, Scope, Send
24+
except ImportError:
25+
raise DidNotEnable("Starlette is not installed")
26+
27+
28+
_DEFAULT_TRANSACTION_NAME = "generic FastApi request"
29+
30+
31+
class FastApiIntegration(StarletteIntegration):
32+
identifier = "fastapi"
33+
34+
@staticmethod
35+
def setup_once():
36+
# type: () -> None
37+
StarletteIntegration.setup_once()
38+
patch_middlewares()
39+
40+
41+
def patch_middlewares():
42+
# type: () -> None
43+
44+
old_build_middleware_stack = FastAPI.build_middleware_stack
45+
46+
def _sentry_build_middleware_stack(self):
47+
# type: (FastAPI) -> Callable[..., Any]
48+
"""
49+
Adds `SentryStarletteMiddleware` and `SentryFastApiMiddleware` to the
50+
middleware stack of the FastAPI application.
51+
"""
52+
app = old_build_middleware_stack(self)
53+
app = SentryStarletteMiddleware(app=app)
54+
app = SentryFastApiMiddleware(app=app)
55+
return app
56+
57+
FastAPI.build_middleware_stack = _sentry_build_middleware_stack
58+
59+
60+
def _set_transaction_name_and_source(event, transaction_style, request):
61+
# type: (Event, str, Any) -> None
62+
name = ""
63+
64+
if transaction_style == "endpoint":
65+
endpoint = request.scope.get("endpoint")
66+
if endpoint:
67+
name = transaction_from_function(endpoint) or ""
68+
69+
elif transaction_style == "url":
70+
route = request.scope.get("route")
71+
if route:
72+
path = getattr(route, "path", None)
73+
if path is not None:
74+
name = path
75+
76+
if not name:
77+
event["transaction"] = _DEFAULT_TRANSACTION_NAME
78+
event["transaction_info"] = {"source": TRANSACTION_SOURCE_ROUTE}
79+
return
80+
81+
event["transaction"] = name
82+
event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]}
83+
84+
85+
class SentryFastApiMiddleware:
86+
def __init__(self, app, dispatch=None):
87+
# type: (ASGIApp, Any) -> None
88+
self.app = app
89+
90+
async def __call__(self, scope, receive, send):
91+
# type: (Scope, Receive, Send) -> Any
92+
if scope["type"] != "http":
93+
await self.app(scope, receive, send)
94+
return
95+
96+
hub = Hub.current
97+
integration = hub.get_integration(FastApiIntegration)
98+
if integration is None:
99+
return
100+
101+
with hub.configure_scope() as sentry_scope:
102+
request = Request(scope, receive=receive, send=send)
103+
104+
def _make_request_event_processor(req, integration):
105+
# type: (Any, Any) -> Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]]
106+
def event_processor(event, hint):
107+
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
108+
109+
_set_transaction_name_and_source(
110+
event, integration.transaction_style, req
111+
)
112+
113+
return event
114+
115+
return event_processor
116+
117+
sentry_scope._name = FastApiIntegration.identifier
118+
sentry_scope.add_event_processor(
119+
_make_request_event_processor(request, integration)
120+
)
121+
122+
await self.app(scope, receive, send)

0 commit comments

Comments
 (0)