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

1from __future__ import absolute_import 

2 

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) 

12 

13from sentry_sdk._types import MYPY 

14 

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 

19 

20 from sentry_sdk._types import EventProcessor 

21 

22try: 

23 import falcon # type: ignore 

24 import falcon.api_helpers # type: ignore 

25 

26 from falcon import __version__ as FALCON_VERSION 

27except ImportError: 

28 raise DidNotEnable("Falcon not installed") 

29 

30 

31class FalconRequestExtractor(RequestExtractor): 

32 def env(self): 

33 # type: () -> Dict[str, Any] 

34 return self.request.env 

35 

36 def cookies(self): 

37 # type: () -> Dict[str, Any] 

38 return self.request.cookies 

39 

40 def form(self): 

41 # type: () -> None 

42 return None # No such concept in Falcon 

43 

44 def files(self): 

45 # type: () -> None 

46 return None # No such concept in Falcon 

47 

48 def raw_data(self): 

49 # type: () -> Optional[str] 

50 

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 

60 

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 

71 

72 

73class SentryFalconMiddleware(object): 

74 """Captures exceptions in Falcon requests and send to Sentry""" 

75 

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 

82 

83 with hub.configure_scope() as scope: 

84 scope._name = "falcon" 

85 scope.add_event_processor(_make_request_event_processor(req, integration)) 

86 

87 

88TRANSACTION_STYLE_VALUES = ("uri_template", "path") 

89 

90 

91class FalconIntegration(Integration): 

92 identifier = "falcon" 

93 

94 transaction_style = "" 

95 

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 

104 

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)) 

112 

113 if version < (1, 4): 

114 raise DidNotEnable("Falcon 1.4 or newer required.") 

115 

116 _patch_wsgi_app() 

117 _patch_handle_exception() 

118 _patch_prepare_middleware() 

119 

120 

121def _patch_wsgi_app(): 

122 # type: () -> None 

123 original_wsgi_app = falcon.API.__call__ 

124 

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) 

131 

132 sentry_wrapped = SentryWsgiMiddleware( 

133 lambda envi, start_resp: original_wsgi_app(self, envi, start_resp) 

134 ) 

135 

136 return sentry_wrapped(env, start_response) 

137 

138 falcon.API.__call__ = sentry_patched_wsgi_app 

139 

140 

141def _patch_handle_exception(): 

142 # type: () -> None 

143 original_handle_exception = falcon.API._handle_exception 

144 

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] 

154 

155 was_handled = original_handle_exception(self, *args) 

156 

157 hub = Hub.current 

158 integration = hub.get_integration(FalconIntegration) 

159 

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 

163 

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) 

170 

171 return was_handled 

172 

173 falcon.API._handle_exception = sentry_patched_handle_exception 

174 

175 

176def _patch_prepare_middleware(): 

177 # type: () -> None 

178 original_prepare_middleware = falcon.api_helpers.prepare_middleware 

179 

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) 

189 

190 falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware 

191 

192 

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 

202 

203 

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]} 

212 

213 

214def _make_request_event_processor(req, integration): 

215 # type: (falcon.Request, FalconIntegration) -> EventProcessor 

216 

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) 

220 

221 with capture_internal_exceptions(): 

222 FalconRequestExtractor(req).extract_into_event(event) 

223 

224 return event 

225 

226 return event_processor