Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/integrations/asgi.py: 15%

154 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1""" 

2An ASGI middleware. 

3 

4Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi>`_. 

5""" 

6 

7import asyncio 

8import inspect 

9import urllib 

10 

11from sentry_sdk._functools import partial 

12from sentry_sdk._types import MYPY 

13from sentry_sdk.hub import Hub, _should_send_default_pii 

14from sentry_sdk.integrations._wsgi_common import _filter_headers 

15from sentry_sdk.sessions import auto_session_tracking 

16from sentry_sdk.tracing import ( 

17 SOURCE_FOR_STYLE, 

18 TRANSACTION_SOURCE_ROUTE, 

19 TRANSACTION_SOURCE_UNKNOWN, 

20) 

21from sentry_sdk.utils import ( 

22 ContextVar, 

23 event_from_exception, 

24 transaction_from_function, 

25 HAS_REAL_CONTEXTVARS, 

26 CONTEXTVARS_ERROR_MESSAGE, 

27) 

28from sentry_sdk.tracing import Transaction 

29 

30if MYPY: 30 ↛ 31line 30 didn't jump to line 31, because the condition on line 30 was never true

31 from typing import Dict 

32 from typing import Any 

33 from typing import Optional 

34 from typing import Callable 

35 

36 from typing_extensions import Literal 

37 

38 from sentry_sdk._types import Event, Hint 

39 

40 

41_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") 

42 

43_DEFAULT_TRANSACTION_NAME = "generic ASGI request" 

44 

45TRANSACTION_STYLE_VALUES = ("endpoint", "url") 

46 

47 

48def _capture_exception(hub, exc): 

49 # type: (Hub, Any) -> None 

50 

51 # Check client here as it might have been unset while streaming response 

52 if hub.client is not None: 

53 event, hint = event_from_exception( 

54 exc, 

55 client_options=hub.client.options, 

56 mechanism={"type": "asgi", "handled": False}, 

57 ) 

58 hub.capture_event(event, hint=hint) 

59 

60 

61def _looks_like_asgi3(app): 

62 # type: (Any) -> bool 

63 """ 

64 Try to figure out if an application object supports ASGI3. 

65 

66 This is how uvicorn figures out the application version as well. 

67 """ 

68 if inspect.isclass(app): 

69 return hasattr(app, "__await__") 

70 elif inspect.isfunction(app): 

71 return asyncio.iscoroutinefunction(app) 

72 else: 

73 call = getattr(app, "__call__", None) # noqa 

74 return asyncio.iscoroutinefunction(call) 

75 

76 

77class 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 

82 """ 

83 Instrument an ASGI application with Sentry. Provides HTTP/websocket 

84 data to sent events and basic handling for exceptions bubbling up 

85 through the middleware. 

86 

87 :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. 

88 """ 

89 

90 if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: 

91 # We better have contextvars or we're going to leak state between 

92 # requests. 

93 raise RuntimeError( 

94 "The ASGI middleware for Sentry requires Python 3.7+ " 

95 "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE 

96 ) 

97 if transaction_style not in TRANSACTION_STYLE_VALUES: 

98 raise ValueError( 

99 "Invalid value for transaction_style: %s (must be in %s)" 

100 % (transaction_style, TRANSACTION_STYLE_VALUES) 

101 ) 

102 self.transaction_style = transaction_style 

103 self.app = app 

104 

105 if _looks_like_asgi3(app): 

106 self.__call__ = self._run_asgi3 # type: Callable[..., Any] 

107 else: 

108 self.__call__ = self._run_asgi2 

109 

110 def _run_asgi2(self, scope): 

111 # type: (Any) -> Any 

112 async def inner(receive, send): 

113 # type: (Any, Any) -> Any 

114 return await self._run_app(scope, lambda: self.app(scope)(receive, send)) 

115 

116 return inner 

117 

118 async def _run_asgi3(self, scope, receive, send): 

119 # type: (Any, Any, Any) -> Any 

120 return await self._run_app(scope, lambda: self.app(scope, receive, send)) 

121 

122 async def _run_app(self, scope, callback): 

123 # type: (Any, Any) -> Any 

124 is_recursive_asgi_middleware = _asgi_middleware_applied.get(False) 

125 

126 if is_recursive_asgi_middleware: 

127 try: 

128 return await callback() 

129 except Exception as exc: 

130 _capture_exception(Hub.current, exc) 

131 raise exc from None 

132 

133 _asgi_middleware_applied.set(True) 

134 try: 

135 hub = Hub(Hub.current) 

136 with auto_session_tracking(hub, session_mode="request"): 

137 with hub: 

138 with hub.configure_scope() as sentry_scope: 

139 sentry_scope.clear_breadcrumbs() 

140 sentry_scope._name = "asgi" 

141 processor = partial(self.event_processor, asgi_scope=scope) 

142 sentry_scope.add_event_processor(processor) 

143 

144 ty = scope["type"] 

145 

146 if ty in ("http", "websocket"): 

147 transaction = Transaction.continue_from_headers( 

148 self._get_headers(scope), 

149 op="{}.server".format(ty), 

150 ) 

151 else: 

152 transaction = Transaction(op="asgi.server") 

153 

154 transaction.name = _DEFAULT_TRANSACTION_NAME 

155 transaction.source = TRANSACTION_SOURCE_ROUTE 

156 transaction.set_tag("asgi.type", ty) 

157 

158 with hub.start_transaction( 

159 transaction, custom_sampling_context={"asgi_scope": scope} 

160 ): 

161 # XXX: Would be cool to have correct span status, but we 

162 # would have to wrap send(). That is a bit hard to do with 

163 # the current abstraction over ASGI 2/3. 

164 try: 

165 return await callback() 

166 except Exception as exc: 

167 _capture_exception(hub, exc) 

168 raise exc from None 

169 finally: 

170 _asgi_middleware_applied.set(False) 

171 

172 def event_processor(self, event, hint, asgi_scope): 

173 # type: (Event, Hint, Any) -> Optional[Event] 

174 request_info = event.get("request", {}) 

175 

176 ty = asgi_scope["type"] 

177 if ty in ("http", "websocket"): 

178 request_info["method"] = asgi_scope.get("method") 

179 request_info["headers"] = headers = _filter_headers( 

180 self._get_headers(asgi_scope) 

181 ) 

182 request_info["query_string"] = self._get_query(asgi_scope) 

183 

184 request_info["url"] = self._get_url( 

185 asgi_scope, "http" if ty == "http" else "ws", headers.get("host") 

186 ) 

187 

188 client = asgi_scope.get("client") 

189 if client and _should_send_default_pii(): 

190 request_info["env"] = {"REMOTE_ADDR": self._get_ip(asgi_scope)} 

191 

192 self._set_transaction_name_and_source(event, self.transaction_style, asgi_scope) 

193 

194 event["request"] = request_info 

195 

196 return event 

197 

198 # Helper functions for extracting request data. 

199 # 

200 # Note: Those functions are not public API. If you want to mutate request 

201 # data to your liking it's recommended to use the `before_send` callback 

202 # for that. 

203 

204 def _set_transaction_name_and_source(self, event, transaction_style, asgi_scope): 

205 # type: (Event, str, Any) -> None 

206 

207 transaction_name_already_set = ( 

208 event.get("transaction", _DEFAULT_TRANSACTION_NAME) 

209 != _DEFAULT_TRANSACTION_NAME 

210 ) 

211 if transaction_name_already_set: 

212 return 

213 

214 name = "" 

215 

216 if transaction_style == "endpoint": 

217 endpoint = asgi_scope.get("endpoint") 

218 # Webframeworks like Starlette mutate the ASGI env once routing is 

219 # done, which is sometime after the request has started. If we have 

220 # an endpoint, overwrite our generic transaction name. 

221 if endpoint: 

222 name = transaction_from_function(endpoint) or "" 

223 

224 elif transaction_style == "url": 

225 # FastAPI includes the route object in the scope to let Sentry extract the 

226 # path from it for the transaction name 

227 route = asgi_scope.get("route") 

228 if route: 

229 path = getattr(route, "path", None) 

230 if path is not None: 

231 name = path 

232 

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

237 return 

238 

239 event["transaction"] = name 

240 event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} 

241 

242 def _get_url(self, scope, default_scheme, host): 

243 # type: (Dict[str, Any], Literal["ws", "http"], Optional[str]) -> str 

244 """ 

245 Extract URL from the ASGI scope, without also including the querystring. 

246 """ 

247 scheme = scope.get("scheme", default_scheme) 

248 

249 server = scope.get("server", None) 

250 path = scope.get("root_path", "") + scope.get("path", "") 

251 

252 if host: 

253 return "%s://%s%s" % (scheme, host, path) 

254 

255 if server is not None: 

256 host, port = server 

257 default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme] 

258 if port != default_port: 

259 return "%s://%s:%s%s" % (scheme, host, port, path) 

260 return "%s://%s%s" % (scheme, host, path) 

261 return path 

262 

263 def _get_query(self, scope): 

264 # type: (Any) -> Any 

265 """ 

266 Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. 

267 """ 

268 qs = scope.get("query_string") 

269 if not qs: 

270 return None 

271 return urllib.parse.unquote(qs.decode("latin-1")) 

272 

273 def _get_ip(self, scope): 

274 # type: (Any) -> str 

275 """ 

276 Extract IP Address from the ASGI scope based on request headers with fallback to scope client. 

277 """ 

278 headers = self._get_headers(scope) 

279 try: 

280 return headers["x-forwarded-for"].split(",")[0].strip() 

281 except (KeyError, IndexError): 

282 pass 

283 

284 try: 

285 return headers["x-real-ip"] 

286 except KeyError: 

287 pass 

288 

289 return scope.get("client")[0] 

290 

291 def _get_headers(self, scope): 

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

293 """ 

294 Extract headers from the ASGI scope, in the format that the Sentry protocol expects. 

295 """ 

296 headers = {} # type: Dict[str, str] 

297 for raw_key, raw_value in scope["headers"]: 

298 key = raw_key.decode("latin-1") 

299 value = raw_value.decode("latin-1") 

300 if key in headers: 

301 headers[key] = headers[key] + ", " + value 

302 else: 

303 headers[key] = value 

304 return headers