litestar.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. from collections.abc import Set
  2. from copy import deepcopy
  3. import sentry_sdk
  4. from sentry_sdk.consts import OP
  5. from sentry_sdk.integrations import (
  6. _DEFAULT_FAILED_REQUEST_STATUS_CODES,
  7. DidNotEnable,
  8. Integration,
  9. )
  10. from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
  11. from sentry_sdk.integrations.logging import ignore_logger
  12. from sentry_sdk.scope import should_send_default_pii
  13. from sentry_sdk.tracing import TransactionSource, SOURCE_FOR_STYLE
  14. from sentry_sdk.utils import (
  15. ensure_integration_enabled,
  16. event_from_exception,
  17. transaction_from_function,
  18. )
  19. try:
  20. from litestar import Request, Litestar # type: ignore
  21. from litestar.handlers.base import BaseRouteHandler # type: ignore
  22. from litestar.middleware import DefineMiddleware # type: ignore
  23. from litestar.routes.http import HTTPRoute # type: ignore
  24. from litestar.data_extractors import ConnectionDataExtractor # type: ignore
  25. from litestar.exceptions import HTTPException # type: ignore
  26. except ImportError:
  27. raise DidNotEnable("Litestar is not installed")
  28. from typing import TYPE_CHECKING
  29. if TYPE_CHECKING:
  30. from typing import Any, Optional, Union
  31. from litestar.types.asgi_types import ASGIApp # type: ignore
  32. from litestar.types import ( # type: ignore
  33. HTTPReceiveMessage,
  34. HTTPScope,
  35. Message,
  36. Middleware,
  37. Receive,
  38. Scope as LitestarScope,
  39. Send,
  40. WebSocketReceiveMessage,
  41. )
  42. from litestar.middleware import MiddlewareProtocol
  43. from sentry_sdk._types import Event, Hint
  44. _DEFAULT_TRANSACTION_NAME = "generic Litestar request"
  45. class LitestarIntegration(Integration):
  46. identifier = "litestar"
  47. origin = f"auto.http.{identifier}"
  48. def __init__(
  49. self,
  50. failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Set[int]
  51. ) -> None:
  52. self.failed_request_status_codes = failed_request_status_codes
  53. @staticmethod
  54. def setup_once():
  55. # type: () -> None
  56. patch_app_init()
  57. patch_middlewares()
  58. patch_http_route_handle()
  59. # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`.
  60. # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following
  61. # (among other things):
  62. # 1. Logs them, some at least (such as 500s) as errors
  63. # 2. Calls after_exception hooks
  64. # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event
  65. # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will
  66. # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so.
  67. ignore_logger("litestar")
  68. class SentryLitestarASGIMiddleware(SentryAsgiMiddleware):
  69. def __init__(self, app, span_origin=LitestarIntegration.origin):
  70. # type: (ASGIApp, str) -> None
  71. super().__init__(
  72. app=app,
  73. unsafe_context_data=False,
  74. transaction_style="endpoint",
  75. mechanism_type="asgi",
  76. span_origin=span_origin,
  77. asgi_version=3,
  78. )
  79. def _capture_request_exception(self, exc):
  80. # type: (Exception) -> None
  81. """Avoid catching exceptions from request handlers.
  82. Those exceptions are already handled in Litestar.after_exception handler.
  83. We still catch exceptions from application lifespan handlers.
  84. """
  85. pass
  86. def patch_app_init():
  87. # type: () -> None
  88. """
  89. Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the
  90. `SentryLitestarASGIMiddleware` as the outmost middleware in the stack.
  91. See:
  92. - https://docs.litestar.dev/2/usage/applications.html#after-exception
  93. - https://docs.litestar.dev/2/usage/middleware/using-middleware.html
  94. """
  95. old__init__ = Litestar.__init__
  96. @ensure_integration_enabled(LitestarIntegration, old__init__)
  97. def injection_wrapper(self, *args, **kwargs):
  98. # type: (Litestar, *Any, **Any) -> None
  99. kwargs["after_exception"] = [
  100. exception_handler,
  101. *(kwargs.get("after_exception") or []),
  102. ]
  103. middleware = kwargs.get("middleware") or []
  104. kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware]
  105. old__init__(self, *args, **kwargs)
  106. Litestar.__init__ = injection_wrapper
  107. def patch_middlewares():
  108. # type: () -> None
  109. old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware
  110. @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack)
  111. def resolve_middleware_wrapper(self):
  112. # type: (BaseRouteHandler) -> list[Middleware]
  113. return [
  114. enable_span_for_middleware(middleware)
  115. for middleware in old_resolve_middleware_stack(self)
  116. ]
  117. BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper
  118. def enable_span_for_middleware(middleware):
  119. # type: (Middleware) -> Middleware
  120. if (
  121. not hasattr(middleware, "__call__") # noqa: B004
  122. or middleware is SentryLitestarASGIMiddleware
  123. ):
  124. return middleware
  125. if isinstance(middleware, DefineMiddleware):
  126. old_call = middleware.middleware.__call__ # type: ASGIApp
  127. else:
  128. old_call = middleware.__call__
  129. async def _create_span_call(self, scope, receive, send):
  130. # type: (MiddlewareProtocol, LitestarScope, Receive, Send) -> None
  131. if sentry_sdk.get_client().get_integration(LitestarIntegration) is None:
  132. return await old_call(self, scope, receive, send)
  133. middleware_name = self.__class__.__name__
  134. with sentry_sdk.start_span(
  135. op=OP.MIDDLEWARE_LITESTAR,
  136. name=middleware_name,
  137. origin=LitestarIntegration.origin,
  138. ) as middleware_span:
  139. middleware_span.set_tag("litestar.middleware_name", middleware_name)
  140. # Creating spans for the "receive" callback
  141. async def _sentry_receive(*args, **kwargs):
  142. # type: (*Any, **Any) -> Union[HTTPReceiveMessage, WebSocketReceiveMessage]
  143. if sentry_sdk.get_client().get_integration(LitestarIntegration) is None:
  144. return await receive(*args, **kwargs)
  145. with sentry_sdk.start_span(
  146. op=OP.MIDDLEWARE_LITESTAR_RECEIVE,
  147. name=getattr(receive, "__qualname__", str(receive)),
  148. origin=LitestarIntegration.origin,
  149. ) as span:
  150. span.set_tag("litestar.middleware_name", middleware_name)
  151. return await receive(*args, **kwargs)
  152. receive_name = getattr(receive, "__name__", str(receive))
  153. receive_patched = receive_name == "_sentry_receive"
  154. new_receive = _sentry_receive if not receive_patched else receive
  155. # Creating spans for the "send" callback
  156. async def _sentry_send(message):
  157. # type: (Message) -> None
  158. if sentry_sdk.get_client().get_integration(LitestarIntegration) is None:
  159. return await send(message)
  160. with sentry_sdk.start_span(
  161. op=OP.MIDDLEWARE_LITESTAR_SEND,
  162. name=getattr(send, "__qualname__", str(send)),
  163. origin=LitestarIntegration.origin,
  164. ) as span:
  165. span.set_tag("litestar.middleware_name", middleware_name)
  166. return await send(message)
  167. send_name = getattr(send, "__name__", str(send))
  168. send_patched = send_name == "_sentry_send"
  169. new_send = _sentry_send if not send_patched else send
  170. return await old_call(self, scope, new_receive, new_send)
  171. not_yet_patched = old_call.__name__ not in ["_create_span_call"]
  172. if not_yet_patched:
  173. if isinstance(middleware, DefineMiddleware):
  174. middleware.middleware.__call__ = _create_span_call
  175. else:
  176. middleware.__call__ = _create_span_call
  177. return middleware
  178. def patch_http_route_handle():
  179. # type: () -> None
  180. old_handle = HTTPRoute.handle
  181. async def handle_wrapper(self, scope, receive, send):
  182. # type: (HTTPRoute, HTTPScope, Receive, Send) -> None
  183. if sentry_sdk.get_client().get_integration(LitestarIntegration) is None:
  184. return await old_handle(self, scope, receive, send)
  185. sentry_scope = sentry_sdk.get_isolation_scope()
  186. request = scope["app"].request_class(scope=scope, receive=receive, send=send) # type: Request[Any, Any]
  187. extracted_request_data = ConnectionDataExtractor(
  188. parse_body=True, parse_query=True
  189. )(request)
  190. body = extracted_request_data.pop("body")
  191. request_data = await body
  192. def event_processor(event, _):
  193. # type: (Event, Hint) -> Event
  194. route_handler = scope.get("route_handler")
  195. request_info = event.get("request", {})
  196. request_info["content_length"] = len(scope.get("_body", b""))
  197. if should_send_default_pii():
  198. request_info["cookies"] = extracted_request_data["cookies"]
  199. if request_data is not None:
  200. request_info["data"] = request_data
  201. func = None
  202. if route_handler.name is not None:
  203. tx_name = route_handler.name
  204. # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type
  205. elif hasattr(route_handler.fn, "value"):
  206. func = route_handler.fn.value
  207. else:
  208. func = route_handler.fn
  209. if func is not None:
  210. tx_name = transaction_from_function(func)
  211. tx_info = {"source": SOURCE_FOR_STYLE["endpoint"]}
  212. if not tx_name:
  213. tx_name = _DEFAULT_TRANSACTION_NAME
  214. tx_info = {"source": TransactionSource.ROUTE}
  215. event.update(
  216. {
  217. "request": deepcopy(request_info),
  218. "transaction": tx_name,
  219. "transaction_info": tx_info,
  220. }
  221. )
  222. return event
  223. sentry_scope._name = LitestarIntegration.identifier
  224. sentry_scope.add_event_processor(event_processor)
  225. return await old_handle(self, scope, receive, send)
  226. HTTPRoute.handle = handle_wrapper
  227. def retrieve_user_from_scope(scope):
  228. # type: (LitestarScope) -> Optional[dict[str, Any]]
  229. scope_user = scope.get("user")
  230. if isinstance(scope_user, dict):
  231. return scope_user
  232. if hasattr(scope_user, "asdict"): # dataclasses
  233. return scope_user.asdict()
  234. return None
  235. @ensure_integration_enabled(LitestarIntegration)
  236. def exception_handler(exc, scope):
  237. # type: (Exception, LitestarScope) -> None
  238. user_info = None # type: Optional[dict[str, Any]]
  239. if should_send_default_pii():
  240. user_info = retrieve_user_from_scope(scope)
  241. if user_info and isinstance(user_info, dict):
  242. sentry_scope = sentry_sdk.get_isolation_scope()
  243. sentry_scope.set_user(user_info)
  244. if isinstance(exc, HTTPException):
  245. integration = sentry_sdk.get_client().get_integration(LitestarIntegration)
  246. if (
  247. integration is not None
  248. and exc.status_code not in integration.failed_request_status_codes
  249. ):
  250. return
  251. event, hint = event_from_exception(
  252. exc,
  253. client_options=sentry_sdk.get_client().options,
  254. mechanism={"type": LitestarIntegration.identifier, "handled": False},
  255. )
  256. sentry_sdk.capture_event(event, hint=hint)