8000 feat(sanic): Refactor Sanic integration for v21.9 support (#1212) · anilktechie/sentry-python@5d357d0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5d357d0

Browse files
ahopkinsrhcarvalho
andauthored
feat(sanic): Refactor Sanic integration for v21.9 support (getsentry#1212)
This PR allows for Sanic v21.9 style error handlers to operate and provide full access to handling Blueprint specific error handlers. Co-authored-by: Rodolfo Carvalho <rhcarvalho@gmail.com>
1 parent dd0efc0 commit 5d357d0

File tree

2 files changed

+201
-108
lines changed

2 files changed

+201
-108
lines changed

sentry_sdk/integrations/sanic.py

Lines changed: 185 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from sanic.request import Request, RequestParameters
2828

2929
from sentry_sdk._types import Event, EventProcessor, Hint
30+
from sanic.router import Route
3031

3132
try:
3233
from sanic import Sanic, __version__ as SANIC_VERSION
@@ -36,19 +37,31 @@
3637
except ImportError:
3738
raise DidNotEnable("Sanic not installed")
3839

40+
old_error_handler_lookup = ErrorHandler.lookup
41+
old_handle_request = Sanic.handle_request
42+
old_router_get = Router.get
43+
44+
try:
45+
# This method was introduced in Sanic v21.9
46+
old_startup = Sanic._startup
47+
except AttributeError:
48+
pass
49+
3950

4051
class SanicIntegration(Integration):
4152
identifier = "sanic"
53+
version = (0, 0) # type: Tuple[int, ...]
4254

4355
@staticmethod
4456
def setup_once():
4557
# type: () -> None
58+
4659
try:
47-
version = tuple(map(int, SANIC_VERSION.split(".")))
60+
SanicIntegration.version = tuple(map(int, SANIC_VERSION.split(".")))
4861
except (TypeError, ValueError):
4962
raise DidNotEnable("Unparsable Sanic version: {}".format(SANIC_VERSION))
5063

51-
if version < (0, 8):
64+
if SanicIntegration.version < (0, 8):
5265
raise DidNotEnable("Sanic 0.8 or newer required.")
5366

5467
if not HAS_REAL_CONTEXTVARS:
@@ -71,89 +84,194 @@ def setup_once():
7184
# https://github.com/huge-success/sanic/issues/1332
7285
ignore_logger("root")
7386

74-
old_handle_request = Sanic.handle_request
87+
if SanicIntegration.version < (21, 9):
88+
_setup_legacy_sanic()
89+
return
7590

76-
async def sentry_handle_request(self, request, *args, **kwargs):
77-
# type: (Any, Request, *Any, **Any) -> Any
78-
hub = Hub.current
79-
if hub.get_integration(SanicIntegration) is None:
80-
return old_handle_request(self, request, *args, **kwargs)
91+
_setup_sanic()
8192

82-
weak_request = weakref.ref(request)
8393

84-
with Hub(hub) as hub:
85-
with hub.configure_scope() as scope:
86-
scope.clear_breadcrumbs()
87-
scope.add_event_processor(_make_request_processor(weak_request))
94+
class SanicRequestExtractor(RequestExtractor):
95+
def content_length(self):
96+
# type: () -> int
97+
if self.request.body is None:
98+
return 0
99+
return len(self.request.body)
88100

89-
response = old_handle_request(self, request, *args, **kwargs)
90-
if isawaitable(response):
91-
response = await response
101+
def cookies(self):
102+
# type: () -> Dict[str, str]
103+
return dict(self.request.cookies)
92104

93-
return response
105+
def raw_data(self):
106+
# type: () -> bytes
107+
return self.request.body
94108

95-
Sanic.handle_request = sentry_handle_request
109+
def form(self):
110+
# type: () -> RequestParameters
111+
return self.request.form
96112

97-
old_router_get = Router.get
113+
def is_json(self):
114+
# type: () -> bool
115+
raise NotImplementedError()
98116

99-
def sentry_router_get(self, *args):
100-
# type: (Any, Union[Any, Request]) -> Any
101-
rv = old_router_get(self, *args)
102-
hub = Hub.current
103-
if hub.get_integration(SanicIntegration) is not None:
104-
with capture_internal_exceptions():
105-
with hub.configure_scope() as scope:
106-
if version >= (21, 3):
107-
# Sanic versions above and including 21.3 append the app name to the
108-
# route name, and so we need to remove it from Route name so the
109-
# transaction name is consistent across all versions
110-
sanic_app_name = self.ctx.app.name
111-
sanic_route = rv[0].name
117+
def json(self):
118+
# type: () -> Optional[Any]
119+
return self.request.json
112120

113-
if sanic_route.startswith("%s." % sanic_app_name):
114-
# We add a 1 to the len of the sanic_app_name because there is a dot
115-
# that joins app name and the route name
116-
# Format: app_name.route_name
117-
sanic_route = sanic_route[len(sanic_app_name) + 1 :]
121+
def files(self):
122+
# type: () -> RequestParameters
123+
return self.request.files
124+
125+
def size_of_file(self, file):
126+
# type: (Any) -> int
127+
return len(file.body or ())
118128

119-
scope.transaction = sanic_route
120-
else:
121-
scope.transaction = rv[0].__name__
122-
return rv
123129

124-
Router.get = sentry_router_get
130+
def _setup_sanic():
131+
# type: () -> None
132+
Sanic._startup = _startup
133+
ErrorHandler.lookup = _sentry_error_handler_lookup
125134

126-
old_error_handler_lookup = ErrorHandler.lookup
127135

128-
def sentry_error_handler_lookup(self, exception):
129-
# type: (Any, Exception) -> Optional[object]
130-
_capture_exception(exception)
131-
old_error_handler = old_error_handler_lookup(self, exception)
136+
def _setup_legacy_sanic():
137+
# type: () -> None
138+
Sanic.handle_request = _legacy_handle_request
139+
Router.get = _legacy_router_get
140+
ErrorHandler.lookup = _sentry_error_handler_lookup
132141

133-
if old_error_handler is None:
134-
return None
135142

136-
if Hub.current.get_integration(SanicIntegration) is None:
137-
return old_error_handler
143+
async def _startup(self):
144+
# type: (Sanic) -> None
145+
# This happens about as early in the lifecycle as possible, just after the
146+
# Request object is created. The body has not yet been consumed.
147+
self.signal("http.lifecycle.request")(_hub_enter)
148+
149+
# This happens after the handler is complete. In v21.9 this signal is not
150+
# dispatched when there is an exception. Therefore we need to close out
151+
# and call _hub_exit from the custom exception handler as well.
152+
# See https://github.com/sanic-org/sanic/issues/2297
153+
self.signal("http.lifecycle.response")(_hub_exit)
154+
155+
# This happens inside of request handling immediately after the route
156+
# has been identified by the router.
157+
self.signal("http.routing.after")(_set_transaction)
158+
159+
# The above signals need to be declared before this can be called.
160+
await old_startup(self)
161+
162+
163+
async def _hub_enter(request):
164+
# type: (Request) -> None
165+
hub = Hub.current
166+
request.ctx._sentry_do_integration = (
167+
hub.get_integration(SanicIntegration) is not None
168+
)
169+
170+
if not request.ctx._sentry_do_integration:
171+
return
172+
173+
weak_request = weakref.ref(request)
174+
request.ctx._sentry_hub = Hub(hub)
175+
request.ctx._sentry_hub.__enter__()
176+
177+
with request.ctx._sentry_hub.configure_scope() as scope:
178+
scope.clear_breadcrumbs()
179+
scope.add_event_processor(_make_request_processor(weak_request))
180+
181+
182+
async def _hub_exit(request, **_):
183+
# type: (Request, **Any) -> None
184+
request.ctx._sentry_hub.__exit__(None, None, None)
185+
186+
187+
async def _set_transaction(request, route, **kwargs):
188+
# type: (Request, Route, **Any) -> None
189+
hub = Hub.current
190+
if hub.get_integration(SanicIntegration) is not None:
191+
with capture_internal_exceptions():
192+
with hub.configure_scope() as scope:
193+
route_name = route.name.replace(request.app.name, "").strip(".")
194+
scope.transaction = route_name
138195

139-
async def sentry_wrapped_error_handler(request, exception):
140-
# type: (Request, Exception) -> Any
141-
try:
142-
response = old_error_handler(request, exception)
143-
if isawaitable(response):
144-
response = await response
145-
return response
146-
except Exception:
147-
# Report errors that occur in Sanic error handler. These
148-
# exceptions will not even show up in Sanic's
149-
# `sanic.exceptions` logger.
150-
exc_info = sys.exc_info()
151-
_capture_exception(exc_info)
152-
reraise(*exc_info)
153196

154-
return sentry_wrapped_error_handler
197+
def _sentry_error_handler_lookup(self, exception, *args, **kwargs):
198+
# type: (Any, Exception, *Any, **Any) -> Optional[object]
199+
_capture_exception(exception)
200+
old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs)
155201

156-
ErrorHandler.lookup = sentry_error_handler_lookup
202+
if old_error_handler is None:
203+
return None
204+
205+
if Hub.current.get_integration(SanicIntegration) is None:
206+
return old_error_handler
207+
208+
async def sentry_wrapped_error_handler(request, exception):
209+
# type: (Request, Exception) -> Any
210+
try:
211+
response = old_error_handler(request, exception)
212+
if isawaitable(response):
213+
response = await response
214+
return response
215+
except Exception:
216+
# Report errors that occur in Sanic error handler. These
217+
# exceptions will not even show up in Sanic's
218+
# `sanic.exceptions` logger.
219+
exc_info = sys.exc_info()
220+
_capture_exception(exc_info)
221+
reraise(*exc_info)
222+
finally:
223+
# As mentioned in previous comment in _startup, this can be removed
224+
# after https://github.com/sanic-org/sanic/issues/2297 is resolved
225+
if SanicIntegration.version >= (21, 9):
226+
await _hub_exit(request)
227+
228+
return sentry_wrapped_error_handler
229+
230+
231+
async def _legacy_handle_request(self, request, *args, **kwargs):
232+
# type: (Any, Request, *Any, **Any) -> Any
233+
hub = Hub.current
234+
if hub.get_integration(SanicIntegration) is None:
235+
return old_handle_request(self, request, *args, **kwargs)
236+
237+
weak_request = weakref.ref(request)
238+
239+
with Hub(hub) as hub:
240+
with hub.configure_scope() as scope:
241+
scope.clear_breadcrumbs()
242+
scope.add_event_processor(_make_request_processor(weak_request))
243+
244+
response = old_handle_request(self, request, *args, **kwargs)
245+
if isawaitable(response):
246+
response = await response
247+
248+
return response
249+
250+
251+
def _legacy_router_get(self, *args):
252+
# type: (Any, Union[Any, Request]) -> Any
253+
rv = old_router_get(self, *args)
254+
hub = Hub.current
255+
if hub.get_integration(SanicIntegration) is not None:
256+
with capture_internal_exceptions():
257+
with hub.configure_scope() as scope:
258+
if SanicIntegration.version and SanicIntegration.version >= (21, 3):
259+
# Sanic versions above and including 21.3 append the app name to the
260+
# route name, and so we need to remove it from Route name so the
261+
# transaction name is consistent across all versions
262+
sanic_app_name = self.ctx.app.name
263+
sanic_route = rv[0].name
264+
265+
if sanic_route.startswith("%s." % sanic_app_name):
266+
# We add a 1 to the len of the sanic_app_name because there is a dot
267+
# that joins app name and the route name
268+
# Format: app_name.route_name
269+
sanic_route = sanic_route[len(sanic_app_name) + 1 :]
270+
271+
scope.transaction = sanic_route
272+
else:
273+
scope.transaction = rv[0].__name__
274+
return rv
157275

158276

159277
def _capture_exception(exception):
@@ -211,39 +329,3 @@ def sanic_processor(event, hint):
211329
return event
212330

213331
return sanic_processor
214-
215-
216-
class SanicRequestExtractor(RequestExtractor):
217-
def content_length(self):
218-
# type: () -> int
219-
if self.request.body is None:
220-
return 0
221< D7AE /td>-
return len(self.request.body)
222-
223-
def cookies(self):
224-
# type: () -> Dict[str, str]
225-
return dict(self.request.cookies)
226-
227-
def raw_data(self):
228-
# type: () -> bytes
229-
return self.request.body
230-
231-
def form(self):
232-
# type: () -> RequestParameters
233-
return self.request.form
234-
235-
def is_json(self):
236-
# type: () -> bool
237-
raise NotImplementedError()
238-
239-
def json(self):
240-
# type: () -> Optional[Any]
241-
return self.request.json
242-
243-
def files(self):
244-
# type: () -> RequestParameters
245-
return self.request.files
246-
247-
def size_of_file(self, file):
248-
# type: (Any) -> int
249-
return len(file.body or ())

tests/integrations/sanic/test_sanic.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,6 @@ async def task(i):
173173
kwargs["app"] = app
174174

175175
if SANIC_VERSION >= (21, 3):
176-
try:
177-
app.router.reset()
178-
app.router.finalize()
179-
except AttributeError:
180-
...
181176

182177
class MockAsyncStreamer:
183178
def __init__(self, request_body):
@@ -203,6 +198,13 @@ async def __anext__(self):
203198
patched_request = request.Request(**kwargs)
204199
patched_request.stream = MockAsyncStreamer([b"hello", b"foo"])
205200

201+
if SANIC_VERSION >= (21, 9):
202+
await app.dispatch(
203+
"http.lifecycle.request",
204+
context={"request": patched_request},
205+
inline=True,
206+
)
207+
206208
await app.handle_request(
207209
patched_request,
208210
)
@@ -217,6 +219,15 @@ async def __anext__(self):
217219
assert r.status == 200
218220

219221
async def runner():
222+
if SANIC_VERSION >= (21, 3):
223+
if SANIC_VERSION >= (21, 9):
224+
await app._startup()
225+
else:
226+
try:
227+
app.router.reset()
228+
app.router.finalize()
229+
except AttributeError:
230+
...
220231
await asyncio.gather(*(task(i) for i in range(1000)))
221232

222233
if sys.version_info < (3, 7):

0 commit comments

Comments
 (0)
0