middleware.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. """
  2. Create spans from Django middleware invocations
  3. """
  4. from functools import wraps
  5. from django import VERSION as DJANGO_VERSION
  6. import sentry_sdk
  7. from sentry_sdk.consts import OP
  8. from sentry_sdk.utils import (
  9. ContextVar,
  10. transaction_from_function,
  11. capture_internal_exceptions,
  12. )
  13. from typing import TYPE_CHECKING
  14. if TYPE_CHECKING:
  15. from typing import Any
  16. from typing import Callable
  17. from typing import Optional
  18. from typing import TypeVar
  19. from sentry_sdk.tracing import Span
  20. F = TypeVar("F", bound=Callable[..., Any])
  21. _import_string_should_wrap_middleware = ContextVar(
  22. "import_string_should_wrap_middleware"
  23. )
  24. DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1)
  25. if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE:
  26. _asgi_middleware_mixin_factory = lambda _: object
  27. else:
  28. from .asgi import _asgi_middleware_mixin_factory
  29. def patch_django_middlewares() -> None:
  30. from django.core.handlers import base
  31. old_import_string = base.import_string
  32. def sentry_patched_import_string(dotted_path: str) -> "Any":
  33. rv = old_import_string(dotted_path)
  34. if _import_string_should_wrap_middleware.get(None):
  35. rv = _wrap_middleware(rv, dotted_path)
  36. return rv
  37. base.import_string = sentry_patched_import_string
  38. old_load_middleware = base.BaseHandler.load_middleware
  39. def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any":
  40. _import_string_should_wrap_middleware.set(True)
  41. try:
  42. return old_load_middleware(*args, **kwargs)
  43. finally:
  44. _import_string_should_wrap_middleware.set(False)
  45. base.BaseHandler.load_middleware = sentry_patched_load_middleware
  46. def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any":
  47. from sentry_sdk.integrations.django import DjangoIntegration
  48. def _check_middleware_span(old_method: "Callable[..., Any]") -> "Optional[Span]":
  49. integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
  50. if integration is None or not integration.middleware_spans:
  51. return None
  52. function_name = transaction_from_function(old_method)
  53. description = middleware_name
  54. function_basename = getattr(old_method, "__name__", None)
  55. if function_basename:
  56. description = "{}.{}".format(description, function_basename)
  57. middleware_span = sentry_sdk.start_span(
  58. op=OP.MIDDLEWARE_DJANGO,
  59. name=description,
  60. origin=DjangoIntegration.origin,
  61. )
  62. middleware_span.set_tag("django.function_name", function_name)
  63. middleware_span.set_tag("django.middleware_name", middleware_name)
  64. return middleware_span
  65. def _get_wrapped_method(old_method: "F") -> "F":
  66. with capture_internal_exceptions():
  67. def sentry_wrapped_method(*args: "Any", **kwargs: "Any") -> "Any":
  68. middleware_span = _check_middleware_span(old_method)
  69. if middleware_span is None:
  70. return old_method(*args, **kwargs)
  71. with middleware_span:
  72. return old_method(*args, **kwargs)
  73. try:
  74. # fails for __call__ of function on Python 2 (see py2.7-django-1.11)
  75. sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method)
  76. # Necessary for Django 3.1
  77. sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore
  78. except Exception:
  79. pass
  80. return sentry_wrapped_method # type: ignore
  81. return old_method
  82. class SentryWrappingMiddleware(
  83. _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore
  84. ):
  85. sync_capable = getattr(middleware, "sync_capable", True)
  86. async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr(
  87. middleware, "async_capable", False
  88. )
  89. def __init__(
  90. self,
  91. get_response: "Optional[Callable[..., Any]]" = None,
  92. *args: "Any",
  93. **kwargs: "Any",
  94. ) -> None:
  95. if get_response:
  96. self._inner = middleware(get_response, *args, **kwargs)
  97. else:
  98. self._inner = middleware(*args, **kwargs)
  99. self.get_response = get_response
  100. self._call_method = None
  101. if self.async_capable:
  102. super().__init__(get_response)
  103. # We need correct behavior for `hasattr()`, which we can only determine
  104. # when we have an instance of the middleware we're wrapping.
  105. def __getattr__(self, method_name: str) -> "Any":
  106. if method_name not in (
  107. "process_request",
  108. "process_view",
  109. "process_template_response",
  110. "process_response",
  111. "process_exception",
  112. ):
  113. raise AttributeError()
  114. old_method = getattr(self._inner, method_name)
  115. rv = _get_wrapped_method(old_method)
  116. self.__dict__[method_name] = rv
  117. return rv
  118. def __call__(self, *args: "Any", **kwargs: "Any") -> "Any":
  119. if hasattr(self, "async_route_check") and self.async_route_check():
  120. return self.__acall__(*args, **kwargs)
  121. f = self._call_method
  122. if f is None:
  123. self._call_method = f = self._inner.__call__
  124. middleware_span = _check_middleware_span(old_method=f)
  125. if middleware_span is None:
  126. return f(*args, **kwargs)
  127. with middleware_span:
  128. return f(*args, **kwargs)
  129. for attr in (
  130. "__name__",
  131. "__module__",
  132. "__qualname__",
  133. ):
  134. if hasattr(middleware, attr):
  135. setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr))
  136. return SentryWrappingMiddleware