asgi.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. """
  2. Instrumentation for Django 3.0
  3. Since this file contains `async def` it is conditionally imported in
  4. `sentry_sdk.integrations.django` (depending on the existence of
  5. `django.core.handlers.asgi`.
  6. """
  7. import asyncio
  8. import functools
  9. import inspect
  10. from django.core.handlers.wsgi import WSGIRequest
  11. import sentry_sdk
  12. from sentry_sdk.consts import OP
  13. from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
  14. from sentry_sdk.scope import should_send_default_pii
  15. from sentry_sdk.utils import (
  16. capture_internal_exceptions,
  17. ensure_integration_enabled,
  18. )
  19. from typing import TYPE_CHECKING
  20. if TYPE_CHECKING:
  21. from typing import Any, Callable, Union, TypeVar
  22. from django.core.handlers.asgi import ASGIRequest
  23. from django.http.response import HttpResponse
  24. from sentry_sdk._types import Event, EventProcessor
  25. _F = TypeVar("_F", bound=Callable[..., Any])
  26. # Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for
  27. # inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker.
  28. # The latter is replaced with the inspect.markcoroutinefunction decorator.
  29. # Until 3.12 is the minimum supported Python version, provide a shim.
  30. # This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py
  31. if hasattr(inspect, "markcoroutinefunction"):
  32. iscoroutinefunction = inspect.iscoroutinefunction
  33. markcoroutinefunction = inspect.markcoroutinefunction
  34. else:
  35. iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment]
  36. def markcoroutinefunction(func: "_F") -> "_F":
  37. func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore
  38. return func
  39. def _make_asgi_request_event_processor(request: "ASGIRequest") -> "EventProcessor":
  40. def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event":
  41. # if the request is gone we are fine not logging the data from
  42. # it. This might happen if the processor is pushed away to
  43. # another thread.
  44. from sentry_sdk.integrations.django import (
  45. DjangoRequestExtractor,
  46. _set_user_info,
  47. )
  48. if request is None:
  49. return event
  50. if type(request) == WSGIRequest:
  51. return event
  52. with capture_internal_exceptions():
  53. DjangoRequestExtractor(request).extract_into_event(event)
  54. if should_send_default_pii():
  55. with capture_internal_exceptions():
  56. _set_user_info(request, event)
  57. return event
  58. return asgi_request_event_processor
  59. def patch_django_asgi_handler_impl(cls: "Any") -> None:
  60. from sentry_sdk.integrations.django import DjangoIntegration
  61. old_app = cls.__call__
  62. async def sentry_patched_asgi_handler(
  63. self: "Any", scope: "Any", receive: "Any", send: "Any"
  64. ) -> "Any":
  65. integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
  66. if integration is None:
  67. return await old_app(self, scope, receive, send)
  68. middleware = SentryAsgiMiddleware(
  69. old_app.__get__(self, cls),
  70. unsafe_context_data=True,
  71. span_origin=DjangoIntegration.origin,
  72. http_methods_to_capture=integration.http_methods_to_capture,
  73. )._run_asgi3
  74. return await middleware(scope, receive, send)
  75. cls.__call__ = sentry_patched_asgi_handler
  76. modern_django_asgi_support = hasattr(cls, "create_request")
  77. if modern_django_asgi_support:
  78. old_create_request = cls.create_request
  79. @ensure_integration_enabled(DjangoIntegration, old_create_request)
  80. def sentry_patched_create_request(
  81. self: "Any", *args: "Any", **kwargs: "Any"
  82. ) -> "Any":
  83. request, error_response = old_create_request(self, *args, **kwargs)
  84. scope = sentry_sdk.get_isolation_scope()
  85. scope.add_event_processor(_make_asgi_request_event_processor(request))
  86. return request, error_response
  87. cls.create_request = sentry_patched_create_request
  88. def patch_get_response_async(cls: "Any", _before_get_response: "Any") -> None:
  89. old_get_response_async = cls.get_response_async
  90. async def sentry_patched_get_response_async(
  91. self: "Any", request: "Any"
  92. ) -> "Union[HttpResponse, BaseException]":
  93. _before_get_response(request)
  94. return await old_get_response_async(self, request)
  95. cls.get_response_async = sentry_patched_get_response_async
  96. def patch_channels_asgi_handler_impl(cls: "Any") -> None:
  97. import channels # type: ignore
  98. from sentry_sdk.integrations.django import DjangoIntegration
  99. if channels.__version__ < "3.0.0":
  100. old_app = cls.__call__
  101. async def sentry_patched_asgi_handler(
  102. self: "Any", receive: "Any", send: "Any"
  103. ) -> "Any":
  104. integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
  105. if integration is None:
  106. return await old_app(self, receive, send)
  107. middleware = SentryAsgiMiddleware(
  108. lambda _scope: old_app.__get__(self, cls),
  109. unsafe_context_data=True,
  110. span_origin=DjangoIntegration.origin,
  111. http_methods_to_capture=integration.http_methods_to_capture,
  112. )
  113. return await middleware(self.scope)(receive, send) # type: ignore
  114. cls.__call__ = sentry_patched_asgi_handler
  115. else:
  116. # The ASGI handler in Channels >= 3 has the same signature as
  117. # the Django handler.
  118. patch_django_asgi_handler_impl(cls)
  119. def wrap_async_view(callback: "Any") -> "Any":
  120. from sentry_sdk.integrations.django import DjangoIntegration
  121. @functools.wraps(callback)
  122. async def sentry_wrapped_callback(
  123. request: "Any", *args: "Any", **kwargs: "Any"
  124. ) -> "Any":
  125. current_scope = sentry_sdk.get_current_scope()
  126. if current_scope.transaction is not None:
  127. current_scope.transaction.update_active_thread()
  128. sentry_scope = sentry_sdk.get_isolation_scope()
  129. if sentry_scope.profile is not None:
  130. sentry_scope.profile.update_active_thread_id()
  131. integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
  132. if not integration or not integration.middleware_spans:
  133. return await callback(request, *args, **kwargs)
  134. with sentry_sdk.start_span(
  135. op=OP.VIEW_RENDER,
  136. name=request.resolver_match.view_name,
  137. origin=DjangoIntegration.origin,
  138. ):
  139. return await callback(request, *args, **kwargs)
  140. return sentry_wrapped_callback
  141. def _asgi_middleware_mixin_factory(
  142. _check_middleware_span: "Callable[..., Any]",
  143. ) -> "Any":
  144. """
  145. Mixin class factory that generates a middleware mixin for handling requests
  146. in async mode.
  147. """
  148. class SentryASGIMixin:
  149. if TYPE_CHECKING:
  150. _inner = None
  151. def __init__(self, get_response: "Callable[..., Any]") -> None:
  152. self.get_response = get_response
  153. self._acall_method = None
  154. self._async_check()
  155. def _async_check(self) -> None:
  156. """
  157. If get_response is a coroutine function, turns us into async mode so
  158. a thread is not consumed during a whole request.
  159. Taken from django.utils.deprecation::MiddlewareMixin._async_check
  160. """
  161. if iscoroutinefunction(self.get_response):
  162. markcoroutinefunction(self)
  163. def async_route_check(self) -> bool:
  164. """
  165. Function that checks if we are in async mode,
  166. and if we are forwards the handling of requests to __acall__
  167. """
  168. return iscoroutinefunction(self.get_response)
  169. async def __acall__(self, *args: "Any", **kwargs: "Any") -> "Any":
  170. f = self._acall_method
  171. if f is None:
  172. if hasattr(self._inner, "__acall__"):
  173. self._acall_method = f = self._inner.__acall__ # type: ignore
  174. else:
  175. self._acall_method = f = self._inner
  176. middleware_span = _check_middleware_span(old_method=f)
  177. if middleware_span is None:
  178. return await f(*args, **kwargs) # type: ignore
  179. with middleware_span:
  180. return await f(*args, **kwargs) # type: ignore
  181. return SentryASGIMixin