starlite.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. from copy import deepcopy
  2. import sentry_sdk
  3. from sentry_sdk.consts import OP
  4. from sentry_sdk.integrations import DidNotEnable, Integration
  5. from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
  6. from sentry_sdk.scope import should_send_default_pii
  7. from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
  8. from sentry_sdk.utils import (
  9. ensure_integration_enabled,
  10. event_from_exception,
  11. transaction_from_function,
  12. )
  13. try:
  14. from starlite import Request, Starlite, State # type: ignore
  15. from starlite.handlers.base import BaseRouteHandler # type: ignore
  16. from starlite.middleware import DefineMiddleware # type: ignore
  17. from starlite.plugins.base import get_plugin_for_value # type: ignore
  18. from starlite.routes.http import HTTPRoute # type: ignore
  19. from starlite.utils import ConnectionDataExtractor, is_async_callable, Ref # type: ignore
  20. from pydantic import BaseModel # type: ignore
  21. except ImportError:
  22. raise DidNotEnable("Starlite is not installed")
  23. from typing import TYPE_CHECKING
  24. if TYPE_CHECKING:
  25. from typing import Any, Optional, Union
  26. from starlite.types import ( # type: ignore
  27. ASGIApp,
  28. Hint,
  29. HTTPReceiveMessage,
  30. HTTPScope,
  31. Message,
  32. Middleware,
  33. Receive,
  34. Scope as StarliteScope,
  35. Send,
  36. WebSocketReceiveMessage,
  37. )
  38. from starlite import MiddlewareProtocol
  39. from sentry_sdk._types import Event
  40. _DEFAULT_TRANSACTION_NAME = "generic Starlite request"
  41. class StarliteIntegration(Integration):
  42. identifier = "starlite"
  43. origin = f"auto.http.{identifier}"
  44. @staticmethod
  45. def setup_once():
  46. # type: () -> None
  47. patch_app_init()
  48. patch_middlewares()
  49. patch_http_route_handle()
  50. class SentryStarliteASGIMiddleware(SentryAsgiMiddleware):
  51. def __init__(self, app, span_origin=StarliteIntegration.origin):
  52. # type: (ASGIApp, str) -> None
  53. super().__init__(
  54. app=app,
  55. unsafe_context_data=False,
  56. transaction_style="endpoint",
  57. mechanism_type="asgi",
  58. span_origin=span_origin,
  59. asgi_version=3,
  60. )
  61. def patch_app_init():
  62. # type: () -> None
  63. """
  64. Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the
  65. `SentryStarliteASGIMiddleware` as the outmost middleware in the stack.
  66. See:
  67. - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception
  68. - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/
  69. """
  70. old__init__ = Starlite.__init__
  71. @ensure_integration_enabled(StarliteIntegration, old__init__)
  72. def injection_wrapper(self, *args, **kwargs):
  73. # type: (Starlite, *Any, **Any) -> None
  74. after_exception = kwargs.pop("after_exception", [])
  75. kwargs.update(
  76. after_exception=[
  77. exception_handler,
  78. *(
  79. after_exception
  80. if isinstance(after_exception, list)
  81. else [after_exception]
  82. ),
  83. ]
  84. )
  85. middleware = kwargs.get("middleware") or []
  86. kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware]
  87. old__init__(self, *args, **kwargs)
  88. Starlite.__init__ = injection_wrapper
  89. def patch_middlewares():
  90. # type: () -> None
  91. old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware
  92. @ensure_integration_enabled(StarliteIntegration, old_resolve_middleware_stack)
  93. def resolve_middleware_wrapper(self):
  94. # type: (BaseRouteHandler) -> list[Middleware]
  95. return [
  96. enable_span_for_middleware(middleware)
  97. for middleware in old_resolve_middleware_stack(self)
  98. ]
  99. BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper
  100. def enable_span_for_middleware(middleware):
  101. # type: (Middleware) -> Middleware
  102. if (
  103. not hasattr(middleware, "__call__") # noqa: B004
  104. or middleware is SentryStarliteASGIMiddleware
  105. ):
  106. return middleware
  107. if isinstance(middleware, DefineMiddleware):
  108. old_call = middleware.middleware.__call__ # type: ASGIApp
  109. else:
  110. old_call = middleware.__call__
  111. async def _create_span_call(self, scope, receive, send):
  112. # type: (MiddlewareProtocol, StarliteScope, Receive, Send) -> None
  113. if sentry_sdk.get_client().get_integration(StarliteIntegration) is None:
  114. return await old_call(self, scope, receive, send)
  115. middleware_name = self.__class__.__name__
  116. with sentry_sdk.start_span(
  117. op=OP.MIDDLEWARE_STARLITE,
  118. name=middleware_name,
  119. origin=StarliteIntegration.origin,
  120. ) as middleware_span:
  121. middleware_span.set_tag("starlite.middleware_name", middleware_name)
  122. # Creating spans for the "receive" callback
  123. async def _sentry_receive(*args, **kwargs):
  124. # type: (*Any, **Any) -> Union[HTTPReceiveMessage, WebSocketReceiveMessage]
  125. if sentry_sdk.get_client().get_integration(StarliteIntegration) is None:
  126. return await receive(*args, **kwargs)
  127. with sentry_sdk.start_span(
  128. op=OP.MIDDLEWARE_STARLITE_RECEIVE,
  129. name=getattr(receive, "__qualname__", str(receive)),
  130. origin=StarliteIntegration.origin,
  131. ) as span:
  132. span.set_tag("starlite.middleware_name", middleware_name)
  133. return await receive(*args, **kwargs)
  134. receive_name = getattr(receive, "__name__", str(receive))
  135. receive_patched = receive_name == "_sentry_receive"
  136. new_receive = _sentry_receive if not receive_patched else receive
  137. # Creating spans for the "send" callback
  138. async def _sentry_send(message):
  139. # type: (Message) -> None
  140. if sentry_sdk.get_client().get_integration(StarliteIntegration) is None:
  141. return await send(message)
  142. with sentry_sdk.start_span(
  143. op=OP.MIDDLEWARE_STARLITE_SEND,
  144. name=getattr(send, "__qualname__", str(send)),
  145. origin=StarliteIntegration.origin,
  146. ) as span:
  147. span.set_tag("starlite.middleware_name", middleware_name)
  148. return await send(message)
  149. send_name = getattr(send, "__name__", str(send))
  150. send_patched = send_name == "_sentry_send"
  151. new_send = _sentry_send if not send_patched else send
  152. return await old_call(self, scope, new_receive, new_send)
  153. not_yet_patched = old_call.__name__ not in ["_create_span_call"]
  154. if not_yet_patched:
  155. if isinstance(middleware, DefineMiddleware):
  156. middleware.middleware.__call__ = _create_span_call
  157. else:
  158. middleware.__call__ = _create_span_call
  159. return middleware
  160. def patch_http_route_handle():
  161. # type: () -> None
  162. old_handle = HTTPRoute.handle
  163. async def handle_wrapper(self, scope, receive, send):
  164. # type: (HTTPRoute, HTTPScope, Receive, Send) -> None
  165. if sentry_sdk.get_client().get_integration(StarliteIntegration) is None:
  166. return await old_handle(self, scope, receive, send)
  167. sentry_scope = sentry_sdk.get_isolation_scope()
  168. request = scope["app"].request_class(scope=scope, receive=receive, send=send) # type: Request[Any, Any]
  169. extracted_request_data = ConnectionDataExtractor(
  170. parse_body=True, parse_query=True
  171. )(request)
  172. body = extracted_request_data.pop("body")
  173. request_data = await body
  174. def event_processor(event, _):
  175. # type: (Event, Hint) -> Event
  176. route_handler = scope.get("route_handler")
  177. request_info = event.get("request", {})
  178. request_info["content_length"] = len(scope.get("_body", b""))
  179. if should_send_default_pii():
  180. request_info["cookies"] = extracted_request_data["cookies"]
  181. if request_data is not None:
  182. request_info["data"] = request_data
  183. func = None
  184. if route_handler.name is not None:
  185. tx_name = route_handler.name
  186. elif isinstance(route_handler.fn, Ref):
  187. func = route_handler.fn.value
  188. else:
  189. func = route_handler.fn
  190. if func is not None:
  191. tx_name = transaction_from_function(func)
  192. tx_info = {"source": SOURCE_FOR_STYLE["endpoint"]}
  193. if not tx_name:
  194. tx_name = _DEFAULT_TRANSACTION_NAME
  195. tx_info = {"source": TransactionSource.ROUTE}
  196. event.update(
  197. {
  198. "request": deepcopy(request_info),
  199. "transaction": tx_name,
  200. "transaction_info": tx_info,
  201. }
  202. )
  203. return event
  204. sentry_scope._name = StarliteIntegration.identifier
  205. sentry_scope.add_event_processor(event_processor)
  206. return await old_handle(self, scope, receive, send)
  207. HTTPRoute.handle = handle_wrapper
  208. def retrieve_user_from_scope(scope):
  209. # type: (StarliteScope) -> Optional[dict[str, Any]]
  210. scope_user = scope.get("user")
  211. if not scope_user:
  212. return None
  213. if isinstance(scope_user, dict):
  214. return scope_user
  215. if isinstance(scope_user, BaseModel):
  216. return scope_user.dict()
  217. if hasattr(scope_user, "asdict"): # dataclasses
  218. return scope_user.asdict()
  219. plugin = get_plugin_for_value(scope_user)
  220. if plugin and not is_async_callable(plugin.to_dict):
  221. return plugin.to_dict(scope_user)
  222. return None
  223. @ensure_integration_enabled(StarliteIntegration)
  224. def exception_handler(exc, scope, _):
  225. # type: (Exception, StarliteScope, State) -> None
  226. user_info = None # type: Optional[dict[str, Any]]
  227. if should_send_default_pii():
  228. user_info = retrieve_user_from_scope(scope)
  229. if user_info and isinstance(user_info, dict):
  230. sentry_scope = sentry_sdk.get_isolation_scope()
  231. sentry_scope.set_user(user_info)
  232. event, hint = event_from_exception(
  233. exc,
  234. client_options=sentry_sdk.get_client().options,
  235. mechanism={"type": StarliteIntegration.identifier, "handled": False},
  236. )
  237. sentry_sdk.capture_event(event, hint=hint)