aiohttp.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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, add_http_request_source
  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: str = "handler_name",
  66. *,
  67. failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES,
  68. ) -> None:
  69. if transaction_style not in TRANSACTION_STYLE_VALUES:
  70. raise ValueError(
  71. "Invalid value for transaction_style: %s (must be in %s)"
  72. % (transaction_style, TRANSACTION_STYLE_VALUES)
  73. )
  74. self.transaction_style = transaction_style
  75. self._failed_request_status_codes = failed_request_status_codes
  76. @staticmethod
  77. def setup_once() -> None:
  78. version = parse_version(AIOHTTP_VERSION)
  79. _check_minimum_version(AioHttpIntegration, version)
  80. if not HAS_REAL_CONTEXTVARS:
  81. # We better have contextvars or we're going to leak state between
  82. # requests.
  83. raise DidNotEnable(
  84. "The aiohttp integration for Sentry requires Python 3.7+ "
  85. " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE
  86. )
  87. ignore_logger("aiohttp.server")
  88. old_handle = Application._handle
  89. async def sentry_app_handle(
  90. self: "Any", request: "Request", *args: "Any", **kwargs: "Any"
  91. ) -> "Any":
  92. integration = sentry_sdk.get_client().get_integration(AioHttpIntegration)
  93. if integration is None:
  94. return await old_handle(self, request, *args, **kwargs)
  95. weak_request = weakref.ref(request)
  96. with sentry_sdk.isolation_scope() as scope:
  97. with track_session(scope, session_mode="request"):
  98. # Scope data will not leak between requests because aiohttp
  99. # create a task to wrap each request.
  100. scope.generate_propagation_context()
  101. scope.clear_breadcrumbs()
  102. scope.add_event_processor(_make_request_processor(weak_request))
  103. headers = dict(request.headers)
  104. transaction = continue_trace(
  105. headers,
  106. op=OP.HTTP_SERVER,
  107. # If this transaction name makes it to the UI, AIOHTTP's
  108. # URL resolver did not find a route or died trying.
  109. name="generic AIOHTTP request",
  110. source=TransactionSource.ROUTE,
  111. origin=AioHttpIntegration.origin,
  112. )
  113. with sentry_sdk.start_transaction(
  114. transaction,
  115. custom_sampling_context={"aiohttp_request": request},
  116. ):
  117. try:
  118. response = await old_handle(self, request)
  119. except HTTPException as e:
  120. transaction.set_http_status(e.status_code)
  121. if (
  122. e.status_code
  123. in integration._failed_request_status_codes
  124. ):
  125. _capture_exception()
  126. raise
  127. except (asyncio.CancelledError, ConnectionResetError):
  128. transaction.set_status(SPANSTATUS.CANCELLED)
  129. raise
  130. except Exception:
  131. # This will probably map to a 500 but seems like we
  132. # have no way to tell. Do not set span status.
  133. reraise(*_capture_exception())
  134. try:
  135. # A valid response handler will return a valid response with a status. But, if the handler
  136. # returns an invalid response (e.g. None), the line below will raise an AttributeError.
  137. # Even though this is likely invalid, we need to handle this case to ensure we don't break
  138. # the application.
  139. response_status = response.status
  140. except AttributeError:
  141. pass
  142. else:
  143. transaction.set_http_status(response_status)
  144. return response
  145. Application._handle = sentry_app_handle
  146. old_urldispatcher_resolve = UrlDispatcher.resolve
  147. @wraps(old_urldispatcher_resolve)
  148. async def sentry_urldispatcher_resolve(
  149. self: "UrlDispatcher", request: "Request"
  150. ) -> "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: "Any", **kwargs: "Any") -> None:
  175. client_trace_configs = list(kwargs.get("trace_configs") or ())
  176. trace_config = create_trace_config()
  177. client_trace_configs.append(trace_config)
  178. kwargs["trace_configs"] = client_trace_configs
  179. return old_client_session_init(*args, **kwargs)
  180. ClientSession.__init__ = init
  181. def create_trace_config() -> "TraceConfig":
  182. async def on_request_start(
  183. session: "ClientSession",
  184. trace_config_ctx: "SimpleNamespace",
  185. params: "TraceRequestStartParams",
  186. ) -> None:
  187. if sentry_sdk.get_client().get_integration(AioHttpIntegration) is None:
  188. return
  189. method = params.method.upper()
  190. parsed_url = None
  191. with capture_internal_exceptions():
  192. parsed_url = parse_url(str(params.url), sanitize=False)
  193. span = sentry_sdk.start_span(
  194. op=OP.HTTP_CLIENT,
  195. name="%s %s"
  196. % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
  197. origin=AioHttpIntegration.origin,
  198. )
  199. span.set_data(SPANDATA.HTTP_METHOD, method)
  200. if parsed_url is not None:
  201. span.set_data("url", parsed_url.url)
  202. span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
  203. span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
  204. client = sentry_sdk.get_client()
  205. if should_propagate_trace(client, str(params.url)):
  206. for (
  207. key,
  208. value,
  209. ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
  210. span=span
  211. ):
  212. logger.debug(
  213. "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format(
  214. key=key, value=value, url=params.url
  215. )
  216. )
  217. if key == BAGGAGE_HEADER_NAME and params.headers.get(
  218. BAGGAGE_HEADER_NAME
  219. ):
  220. # do not overwrite any existing baggage, just append to it
  221. params.headers[key] += "," + value
  222. else:
  223. params.headers[key] = value
  224. trace_config_ctx.span = span
  225. async def on_request_end(
  226. session: "ClientSession",
  227. trace_config_ctx: "SimpleNamespace",
  228. params: "TraceRequestEndParams",
  229. ) -> None:
  230. if trace_config_ctx.span is None:
  231. return
  232. span = trace_config_ctx.span
  233. span.set_http_status(int(params.response.status))
  234. span.set_data("reason", params.response.reason)
  235. span.finish()
  236. with capture_internal_exceptions():
  237. add_http_request_source(span)
  238. trace_config = TraceConfig()
  239. trace_config.on_request_start.append(on_request_start)
  240. trace_config.on_request_end.append(on_request_end)
  241. return trace_config
  242. def _make_request_processor(
  243. weak_request: "weakref.ReferenceType[Request]",
  244. ) -> "EventProcessor":
  245. def aiohttp_processor(
  246. event: "Event",
  247. hint: "dict[str, Tuple[type, BaseException, Any]]",
  248. ) -> "Event":
  249. request = weak_request()
  250. if request is None:
  251. return event
  252. with capture_internal_exceptions():
  253. request_info = event.setdefault("request", {})
  254. request_info["url"] = "%s://%s%s" % (
  255. request.scheme,
  256. request.host,
  257. request.path,
  258. )
  259. request_info["query_string"] = request.query_string
  260. request_info["method"] = request.method
  261. request_info["env"] = {"REMOTE_ADDR": request.remote}
  262. request_info["headers"] = _filter_headers(dict(request.headers))
  263. # Just attach raw data here if it is within bounds, if available.
  264. # Unfortunately there's no way to get structured data from aiohttp
  265. # without awaiting on some coroutine.
  266. request_info["data"] = get_aiohttp_request_data(request)
  267. return event
  268. return aiohttp_processor
  269. def _capture_exception() -> "ExcInfo":
  270. exc_info = sys.exc_info()
  271. event, hint = event_from_exception(
  272. exc_info,
  273. client_options=sentry_sdk.get_client().options,
  274. mechanism={"type": "aiohttp", "handled": False},
  275. )
  276. sentry_sdk.capture_event(event, hint=hint)
  277. return exc_info
  278. BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]"
  279. def get_aiohttp_request_data(
  280. request: "Request",
  281. ) -> "Union[Optional[str], AnnotatedValue]":
  282. bytes_body = request._read_bytes
  283. if bytes_body is not None:
  284. # we have body to show
  285. if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)):
  286. return AnnotatedValue.removed_because_over_size_limit()
  287. encoding = request.charset or "utf-8"
  288. return bytes_body.decode(encoding, "replace")
  289. if request.can_read_body:
  290. # body exists but we can't show it
  291. return BODY_NOT_READ_MESSAGE
  292. # request has no body
  293. return None