Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/integrations/falcon.py: 10%
115 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1from __future__ import absolute_import
3from sentry_sdk.hub import Hub
4from sentry_sdk.integrations import Integration, DidNotEnable
5from sentry_sdk.integrations._wsgi_common import RequestExtractor
6from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
7from sentry_sdk.tracing import SOURCE_FOR_STYLE
8from sentry_sdk.utils import (
9 capture_internal_exceptions,
10 event_from_exception,
11)
13from sentry_sdk._types import MYPY
15if MYPY: 15 ↛ 16line 15 didn't jump to line 16, because the condition on line 15 was never true
16 from typing import Any
17 from typing import Dict
18 from typing import Optional
20 from sentry_sdk._types import EventProcessor
22try:
23 import falcon # type: ignore
24 import falcon.api_helpers # type: ignore
26 from falcon import __version__ as FALCON_VERSION
27except ImportError:
28 raise DidNotEnable("Falcon not installed")
31class FalconRequestExtractor(RequestExtractor):
32 def env(self):
33 # type: () -> Dict[str, Any]
34 return self.request.env
36 def cookies(self):
37 # type: () -> Dict[str, Any]
38 return self.request.cookies
40 def form(self):
41 # type: () -> None
42 return None # No such concept in Falcon
44 def files(self):
45 # type: () -> None
46 return None # No such concept in Falcon
48 def raw_data(self):
49 # type: () -> Optional[str]
51 # As request data can only be read once we won't make this available
52 # to Sentry. Just send back a dummy string in case there was a
53 # content length.
54 # TODO(jmagnusson): Figure out if there's a way to support this
55 content_length = self.content_length()
56 if content_length > 0:
57 return "[REQUEST_CONTAINING_RAW_DATA]"
58 else:
59 return None
61 def json(self):
62 # type: () -> Optional[Dict[str, Any]]
63 try:
64 return self.request.media
65 except falcon.errors.HTTPBadRequest:
66 # NOTE(jmagnusson): We return `falcon.Request._media` here because
67 # falcon 1.4 doesn't do proper type checking in
68 # `falcon.Request.media`. This has been fixed in 2.0.
69 # Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
70 return self.request._media
73class SentryFalconMiddleware(object):
74 """Captures exceptions in Falcon requests and send to Sentry"""
76 def process_request(self, req, resp, *args, **kwargs):
77 # type: (Any, Any, *Any, **Any) -> None
78 hub = Hub.current
79 integration = hub.get_integration(FalconIntegration)
80 if integration is None:
81 return
83 with hub.configure_scope() as scope:
84 scope._name = "falcon"
85 scope.add_event_processor(_make_request_event_processor(req, integration))
88TRANSACTION_STYLE_VALUES = ("uri_template", "path")
91class FalconIntegration(Integration):
92 identifier = "falcon"
94 transaction_style = ""
96 def __init__(self, transaction_style="uri_template"):
97 # type: (str) -> None
98 if transaction_style not in TRANSACTION_STYLE_VALUES:
99 raise ValueError(
100 "Invalid value for transaction_style: %s (must be in %s)"
101 % (transaction_style, TRANSACTION_STYLE_VALUES)
102 )
103 self.transaction_style = transaction_style
105 @staticmethod
106 def setup_once():
107 # type: () -> None
108 try:
109 version = tuple(map(int, FALCON_VERSION.split(".")))
110 except (ValueError, TypeError):
111 raise DidNotEnable("Unparsable Falcon version: {}".format(FALCON_VERSION))
113 if version < (1, 4):
114 raise DidNotEnable("Falcon 1.4 or newer required.")
116 _patch_wsgi_app()
117 _patch_handle_exception()
118 _patch_prepare_middleware()
121def _patch_wsgi_app():
122 # type: () -> None
123 original_wsgi_app = falcon.API.__call__
125 def sentry_patched_wsgi_app(self, env, start_response):
126 # type: (falcon.API, Any, Any) -> Any
127 hub = Hub.current
128 integration = hub.get_integration(FalconIntegration)
129 if integration is None:
130 return original_wsgi_app(self, env, start_response)
132 sentry_wrapped = SentryWsgiMiddleware(
133 lambda envi, start_resp: original_wsgi_app(self, envi, start_resp)
134 )
136 return sentry_wrapped(env, start_response)
138 falcon.API.__call__ = sentry_patched_wsgi_app
141def _patch_handle_exception():
142 # type: () -> None
143 original_handle_exception = falcon.API._handle_exception
145 def sentry_patched_handle_exception(self, *args):
146 # type: (falcon.API, *Any) -> Any
147 # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception
148 # method signature from `(ex, req, resp, params)` to
149 # `(req, resp, ex, params)`
150 if isinstance(args[0], Exception):
151 ex = args[0]
152 else:
153 ex = args[2]
155 was_handled = original_handle_exception(self, *args)
157 hub = Hub.current
158 integration = hub.get_integration(FalconIntegration)
160 if integration is not None and _exception_leads_to_http_5xx(ex):
161 # If an integration is there, a client has to be there.
162 client = hub.client # type: Any
164 event, hint = event_from_exception(
165 ex,
166 client_options=client.options,
167 mechanism={"type": "falcon", "handled": False},
168 )
169 hub.capture_event(event, hint=hint)
171 return was_handled
173 falcon.API._handle_exception = sentry_patched_handle_exception
176def _patch_prepare_middleware():
177 # type: () -> None
178 original_prepare_middleware = falcon.api_helpers.prepare_middleware
180 def sentry_patched_prepare_middleware(
181 middleware=None, independent_middleware=False
182 ):
183 # type: (Any, Any) -> Any
184 hub = Hub.current
185 integration = hub.get_integration(FalconIntegration)
186 if integration is not None:
187 middleware = [SentryFalconMiddleware()] + (middleware or [])
188 return original_prepare_middleware(middleware, independent_middleware)
190 falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware
193def _exception_leads_to_http_5xx(ex):
194 # type: (Exception) -> bool
195 is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith(
196 "5"
197 )
198 is_unhandled_error = not isinstance(
199 ex, (falcon.HTTPError, falcon.http_status.HTTPStatus)
200 )
201 return is_server_error or is_unhandled_error
204def _set_transaction_name_and_source(event, transaction_style, request):
205 # type: (Dict[str, Any], str, falcon.Request) -> None
206 name_for_style = {
207 "uri_template": request.uri_template,
208 "path": request.path,
209 }
210 event["transaction"] = name_for_style[transaction_style]
211 event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]}
214def _make_request_event_processor(req, integration):
215 # type: (falcon.Request, FalconIntegration) -> EventProcessor
217 def event_processor(event, hint):
218 # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
219 _set_transaction_name_and_source(event, integration.transaction_style, req)
221 with capture_internal_exceptions():
222 FalconRequestExtractor(req).extract_into_event(event)
224 return event
226 return event_processor