aiohttp.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import sys
  2. import weakref
  3. from functools import wraps
  4. import sentry_sdk
  5. from sentry_sdk.api import continue_trace
  6. from sentry_sdk.consts import OP, SPANSTATUS, SPANDATA
  7. from sentry_sdk.integrations import (
  8. _DEFAULT_FAILED_REQUEST_STATUS_CODES,
  9. _check_minimum_version,
  10. Integration,
  11. DidNotEnable,
  12. )
  13. from sentry_sdk.integrations.logging import ignore_logger
  14. from sentry_sdk.sessions import track_session
  15. from sentry_sdk.integrations._wsgi_common import (
  16. _filter_headers,
  17. request_body_within_bounds,
  18. )
  19. from sentry_sdk.tracing import (
  20. BAGGAGE_HEADER_NAME,
  21. SOURCE_FOR_STYLE,
  22. TransactionSource,
  23. )
  24. from sentry_sdk.tracing_utils import should_propagate_trace
  25. from sentry_sdk.utils import (
  26. capture_internal_exceptions,
  27. ensure_integration_enabled,
  28. event_from_exception,
  29. logger,
  30. parse_url,
  31. parse_version,
  32. reraise,
  33. transaction_from_function,
  34. HAS_REAL_CONTEXTVARS,
  35. CONTEXTVARS_ERROR_MESSAGE,
  36. SENSITIVE_DATA_SUBSTITUTE,
  37. AnnotatedValue,
  38. )
  39. try:
  40. import asyncio
  41. from aiohttp import __version__ as AIOHTTP_VERSION
  42. from aiohttp import ClientSession, TraceConfig
  43. from aiohttp.web import Application, HTTPException, UrlDispatcher
  44. except ImportError:
  45. raise DidNotEnable("AIOHTTP not installed")
  46. from typing import TYPE_CHECKING
  47. if TYPE_CHECKING:
  48. from aiohttp.web_request import Request
  49. from aiohttp.web_urldispatcher import UrlMappingMatchInfo
  50. from aiohttp import TraceRequestStartParams, TraceRequestEndParams
  51. from collections.abc import Set
  52. from types import SimpleNamespace
  53. from typing import Any
  54. from typing import Optional
  55. from typing import Tuple
  56. from typing import Union
  57. from sentry_sdk.utils import ExcInfo
  58. from sentry_sdk._types import Event, EventProcessor
  59. TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern")
  60. class AioHttpIntegration(Integration):
  61. identifier = "aiohttp"
  62. origin = f"auto.http.{identifier}"
  63. def __init__(
  64. self,
  65. transaction_style="handler_name", # type: str
  66. *,
  67. failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Set[int]
  68. ):
  69. # type: (...) -> None
  70. if transaction_style not in TRANSACTION_STYLE_VALUES:
  71. raise ValueError(
  72. "Invalid value for transaction_style: %s (must be in %s)"
  73. % (transaction_style, TRANSACTION_STYLE_VALUES)
  74. )
  75. self.transaction_style = transaction_style
  76. self._failed_request_status_codes = failed_request_status_codes
  77. @staticmethod
  78. def setup_once():
  79. # type: () -> None
  80. version = parse_version(AIOHTTP_VERSION)
  81. _check_minimum_version(AioHttpIntegration, version)
  82. if not HAS_REAL_CONTEXTVARS:
  83. # We better have contextvars or we're going to leak state between
  84. # requests.
  85. raise DidNotEnable(
  86. "The aiohttp integration for Sentry requires Python 3.7+ "
  87. " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE
  88. )
  89. ignore_logger("aiohttp.server")
  90. old_handle = Application._handle
  91. async def sentry_app_handle(self, request, *args, **kwargs):
  92. # type: (Any, Request, *Any, **Any) -> Any
  93. integration = sentry_sdk.get_client().get_integration(AioHttpIntegration)
  94. if integration is None:
  95. return await old_handle(self, request, *args, **kwargs)
  96. weak_request = weakref.ref(request)
  97. with sentry_sdk.isolation_scope() as scope:
  98. with track_session(scope, session_mode="request"):
  99. # Scope data will not leak between requests because aiohttp
  100. # create a task to wrap each request.
  101. scope.generate_propagation_context()
  102. scope.clear_breadcrumbs()
  103. scope.add_event_processor(_make_request_processor(weak_request))
  104. headers = dict(request.headers)
  105. transaction = continue_trace(
  106. headers,
  107. op=OP.HTTP_SERVER,
  108. # If this transaction name makes it to the UI, AIOHTTP's
  109. # URL resolver did not find a route or died trying.
  110. name="generic AIOHTTP request",
  111. source=TransactionSource.ROUTE,
  112. origin=AioHttpIntegration.origin,
  113. )
  114. with sentry_sdk.start_transaction(
  115. transaction,
  116. custom_sampling_context={"aiohttp_request": request},
  117. ):
  118. try:
  119. response = await old_handle(self, request)
  120. except HTTPException as e:
  121. transaction.set_http_status(e.status_code)
  122. if (
  123. e.status_code
  124. in integration._failed_request_status_codes
  125. ):
  126. _capture_exception()
  127. raise
  128. except (asyncio.CancelledError, ConnectionResetError):
  129. transaction.set_status(SPANSTATUS.CANCELLED)
  130. raise
  131. except Exception:
  132. # This will probably map to a 500 but seems like we
  133. # have no way to tell. Do not set span status.
  134. reraise(*_capture_exception())
  135. try:
  136. # A valid response handler will return a valid response with a status. But, if the handler
  137. # returns an invalid response (e.g. None), the line below will raise an AttributeError.
  138. # Even though this is likely invalid, we need to handle this case to ensure we don't break
  139. # the application.
  140. response_status = response.status
  141. except AttributeError:
  142. pass
  143. else:
  144. transaction.set_http_status(response_status)
  145. return response
  146. Application._handle = sentry_app_handle
  147. old_urldispatcher_resolve = UrlDispatcher.resolve
  148. @wraps(old_urldispatcher_resolve)
  149. async def sentry_urldispatcher_resolve(self, request):
  150. # type: (UrlDispatcher, Request) -> UrlMappingMatchInfo
  151. rv = await old_urldispatcher_resolve(self, request)
  152. integration = sentry_sdk.get_client().get_integration(AioHttpIntegration)
  153. if integration is None:
  154. return rv
  155. name = None
  156. try:
  157. if integration.transaction_style == "handler_name":
  158. name = transaction_from_function(rv.handler)
  159. elif integration.transaction_style == "method_and_path_pattern":
  160. route_info = rv.get_info()
  161. pattern = route_info.get("path") or route_info.get("formatter")
  162. name = "{} {}".format(request.method, pattern)
  163. except Exception:
  164. pass
  165. if name is not None:
  166. sentry_sdk.get_current_scope().set_transaction_name(
  167. name,
  168. source=SOURCE_FOR_STYLE[integration.transaction_style],
  169. )
  170. return rv
  171. UrlDispatcher.resolve = sentry_urldispatcher_resolve
  172. old_client_session_init = ClientSession.__init__
  173. @ensure_integration_enabled(AioHttpIntegration, old_client_session_init)
  174. def init(*args, **kwargs):
  175. # type: (Any, Any) -> None
  176. client_trace_configs = list(kwargs.get("trace_configs") or ())
  177. trace_config = create_trace_config()
  178. client_trace_configs.append(trace_config)
  179. kwargs["trace_configs"] = client_trace_configs
  180. return old_client_session_init(*args, **kwargs)
  181. ClientSession.__init__ = init
  182. def create_trace_config():
  183. # type: () -> TraceConfig
  184. async def on_request_start(session, trace_config_ctx, params):
  185. # type: (ClientSession, SimpleNamespace, TraceRequestStartParams) -> None
  186. if sentry_sdk.get_client().get_integration(AioHttpIntegration) is None:
  187. return
  188. method = params.method.upper()
  189. parsed_url = None
  190. with capture_internal_exceptions():
  191. parsed_url = parse_url(str(params.url), sanitize=False)
  192. span = sentry_sdk.start_span(
  193. op=OP.HTTP_CLIENT,
  194. name="%s %s"
  195. % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
  196. origin=AioHttpIntegration.origin,
  197. )
  198. span.set_data(SPANDATA.HTTP_METHOD, method)
  199. if parsed_url is not None:
  200. span.set_data("url", parsed_url.url)
  201. span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
  202. span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
  203. client = sentry_sdk.get_client()
  204. if should_propagate_trace(client, str(params.url)):
  205. for (
  206. key,
  207. value,
  208. ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
  209. span=span
  210. ):
  211. logger.debug(
  212. "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format(
  213. key=key, value=value, url=params.url
  214. )
  215. )
  216. if key == BAGGAGE_HEADER_NAME and params.headers.get(
  217. BAGGAGE_HEADER_NAME
  218. ):
  219. # do not overwrite any existing baggage, just append to it
  220. params.headers[key] += "," + value
  221. else:
  222. params.headers[key] = value
  223. trace_config_ctx.span = span
  224. async def on_request_end(session, trace_config_ctx, params):
  225. # type: (ClientSession, SimpleNamespace, TraceRequestEndParams) -> None
  226. if trace_config_ctx.span is None:
  227. return
  228. span = trace_config_ctx.span
  229. span.set_http_status(int(params.response.status))
  230. span.set_data("reason", params.response.reason)
  231. span.finish()
  232. trace_config = TraceConfig()
  233. trace_config.on_request_start.append(on_request_start)
  234. trace_config.on_request_end.append(on_request_end)
  235. return trace_config
  236. def _make_request_processor(weak_request):
  237. # type: (weakref.ReferenceType[Request]) -> EventProcessor
  238. def aiohttp_processor(
  239. event, # type: Event
  240. hint, # type: dict[str, Tuple[type, BaseException, Any]]
  241. ):
  242. # type: (...) -> Event
  243. request = weak_request()
  244. if request is None:
  245. return event
  246. with capture_internal_exceptions():
  247. request_info = event.setdefault("request", {})
  248. request_info["url"] = "%s://%s%s" % (
  249. request.scheme,
  250. request.host,
  251. request.path,
  252. )
  253. request_info["query_string"] = request.query_string
  254. request_info["method"] = request.method
  255. request_info["env"] = {"REMOTE_ADDR": request.remote}
  256. request_info["headers"] = _filter_headers(dict(request.headers))
  257. # Just attach raw data here if it is within bounds, if available.
  258. # Unfortunately there's no way to get structured data from aiohttp
  259. # without awaiting on some coroutine.
  260. request_info["data"] = get_aiohttp_request_data(request)
  261. return event
  262. return aiohttp_processor
  263. def _capture_exception():
  264. # type: () -> ExcInfo
  265. exc_info = sys.exc_info()
  266. event, hint = event_from_exception(
  267. exc_info,
  268. client_options=sentry_sdk.get_client().options,
  269. mechanism={"type": "aiohttp", "handled": False},
  270. )
  271. sentry_sdk.capture_event(event, hint=hint)
  272. return exc_info
  273. BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]"
  274. def get_aiohttp_request_data(request):
  275. # type: (Request) -> Union[Optional[str], AnnotatedValue]
  276. bytes_body = request._read_bytes
  277. if bytes_body is not None:
  278. # we have body to show
  279. if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)):
  280. return AnnotatedValue.removed_because_over_size_limit()
  281. encoding = request.charset or "utf-8"
  282. return bytes_body.decode(encoding, "replace")
  283. if request.can_read_body:
  284. # body exists but we can't show it
  285. return BODY_NOT_READ_MESSAGE
  286. # request has no body
  287. return None