Skip to content

Commit ffa2afd

Browse files
jacobsvanteuntitaker
authored andcommitted
feat(integration): Add Falcon integration (getsentry#346)
* feat(integration): Add Falcon integration * `if False` typing import style + lint fixes * Remove redundant scope * Fix absolute imports errors in pypy and py2.7 * Try to return falcon.Request.media in case it wasn’t consumed in the request * Send request data on non-JSON requests as well * Lint fix * Fix mypy failing * Automatically capture internal server errors when an error handler has been defined * Add falcon 2.0 to test suite
1 parent 263a0bd commit ffa2afd

File tree

4 files changed

+486
-0
lines changed

4 files changed

+486
-0
lines changed

sentry_sdk/integrations/falcon.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
from __future__ import absolute_import
2+
3+
import falcon # type: ignore
4+
import falcon.api_helpers # type: ignore
5+
from sentry_sdk.hub import Hub
6+
from sentry_sdk.integrations import Integration
7+
from sentry_sdk.integrations._wsgi_common import RequestExtractor
8+
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
9+
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
10+
11+
if False:
12+
from typing import Any
13+
from typing import Callable
14+
from typing import Dict
15+
16+
17+
class FalconRequestExtractor(RequestExtractor):
18+
def env(self):
19+
return self.request.env
20+
21+
def cookies(self):
22+
return self.request.cookies
23+
24+
def form(self):
25+
return None # No such concept in Falcon
26+
27+
def files(self):
28+
return None # No such concept in Falcon
29+
30+
def raw_data(self):
31+
# As request data can only be read once we won't make this available
32+
# to Sentry. Just send back a dummy string in case there was a
33+
# content length.
34+
# TODO(jmagnusson): Figure out if there's a way to support this
35+
content_length = self.content_length()
36+
if content_length > 0:
37+
return "[REQUEST_CONTAINING_RAW_DATA]"
38+
else:
39+
return None
40+
41+
def json(self):
42+
try:
43+
return self.request.media
44+
except falcon.errors.HTTPBadRequest:
45+
# NOTE(jmagnusson): We return `falcon.Request._media` here because
46+
# falcon 1.4 doesn't do proper type checking in
47+
# `falcon.Request.media`. This has been fixed in 2.0.
48+
# Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
49+
return self.request._media
50+
51+
52+
class SentryFalconMiddleware(object):
53+
"""Captures exceptions in Falcon requests and send to Sentry"""
54+
55+
def process_request(self, req, resp, *args, **kwargs):
56+
hub = Hub.current
57+
integration = hub.get_integration(FalconIntegration)
58+
if integration is None:
59+
return
60+
61+
with hub.configure_scope() as scope:
62+
scope._name = "falcon"
63+
scope.add_event_processor(_make_request_event_processor(req, integration))
64+
65+
66+
class FalconIntegration(Integration):
67+
identifier = "falcon"
68+
69+
transaction_style = None
70+
71+
def __init__(self, transaction_style="uri_template"):
72+
# type: (str) -> None
73+
TRANSACTION_STYLE_VALUES = ("uri_template", "path")
74+
if transaction_style not in TRANSACTION_STYLE_VALUES:
75+
raise ValueError(
76+
"Invalid value for transaction_style: %s (must be in %s)"
77+
% (transaction_style, TRANSACTION_STYLE_VALUES)
78+
)
79+
self.transaction_style = transaction_style
80+
81+
@staticmethod
82+
def setup_once():
83+
# type: () -> None
84+
_patch_wsgi_app()
85+
_patch_handle_exception()
86+
_patch_prepare_middleware()
87+
88+
89+
def _patch_wsgi_app():
90+
original_wsgi_app = falcon.API.__call__
91+
92+
def sentry_patched_wsgi_app(self, env, start_response):
93+
hub = Hub.current
94+
integration = hub.get_integration(FalconIntegration)
95+
if integration is None:
96+
return original_wsgi_app(self, env, start_response)
97+
98+
sentry_wrapped = SentryWsgiMiddleware(
99+
lambda envi, start_resp: original_wsgi_app(self, envi, start_resp)
100+
)
101+
102+
return sentry_wrapped(env, start_response)
103+
104+
falcon.API.__call__ = sentry_patched_wsgi_app
105+
106+
107+
def _patch_handle_exception():
108+
original_handle_exception = falcon.API._handle_exception
109+
110+
def sentry_patched_handle_exception(self, *args):
111+
# NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception
112+
# method signature from `(ex, req, resp, params)` to
113+
# `(req, resp, ex, params)`
114+
if isinstance(args[0], Exception):
115+
ex = args[0]
116+
else:
117+
ex = args[2]
118+
119+
was_handled = original_handle_exception(self, *args)
120+
121+
hub = Hub.current
122+
integration = hub.get_integration(FalconIntegration)
123+
124+
if integration is not None and not _is_falcon_http_error(ex):
125+
event, hint = event_from_exception(
126+
ex,
127+
client_options=hub.client.options,
128+
mechanism={"type": "falcon", "handled": False},
129+
)
130+
hub.capture_event(event, hint=hint)
131+
132+
return was_handled
133+
134+
falcon.API._handle_exception = sentry_patched_handle_exception
135+
136+
137+
def _patch_prepare_middleware():
138+
original_prepare_middleware = falcon.api_helpers.prepare_middleware
139+
140+
def sentry_patched_prepare_middleware(
141+
middleware=None, independent_middleware=False
142+
):
143+
hub = Hub.current
144+
integration = hub.get_integration(FalconIntegration)
145+
if integration is not None:
146+
middleware = [SentryFalconMiddleware()] + (middleware or [])
147+
return original_prepare_middleware(middleware, independent_middleware)
148+
149+
falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware
150+
151+
152+
def _is_falcon_http_error(ex):
153+
return isinstance(ex, (falcon.HTTPError, falcon.http_status.HTTPStatus))
154+
155+
156+
def _make_request_event_processor(req, integration):
157+
# type: (falcon.Request, FalconIntegration) -> Callable
158+
159+
def inner(event, hint):
160+
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
161+
if integration.transaction_style == "uri_template":
162+
event["transaction"] = req.uri_template
163+
elif integration.transaction_style == "path":
164+
event["transaction"] = req.path
165+
166+
with capture_internal_exceptions():
167+
FalconRequestExtractor(req).extract_into_event(event)
168+
169+
return event
170+
171+
return inner

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
extras_require={
2626
"flask": ["flask>=0.8", "blinker>=1.1"],
2727
"bottle": ["bottle>=0.12.13"],
28+
"falcon": ["falcon>=1.4"],
2829
},
2930
classifiers=[
3031
"Development Status :: 5 - Production/Stable",

0 commit comments

Comments
 (0)