strawberry.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import functools
  2. import hashlib
  3. from inspect import isawaitable
  4. import sentry_sdk
  5. from sentry_sdk.consts import OP
  6. from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
  7. from sentry_sdk.integrations.logging import ignore_logger
  8. from sentry_sdk.scope import should_send_default_pii
  9. from sentry_sdk.tracing import TransactionSource
  10. from sentry_sdk.utils import (
  11. capture_internal_exceptions,
  12. ensure_integration_enabled,
  13. event_from_exception,
  14. logger,
  15. package_version,
  16. _get_installed_modules,
  17. )
  18. try:
  19. from functools import cached_property
  20. except ImportError:
  21. # The strawberry integration requires Python 3.8+. functools.cached_property
  22. # was added in 3.8, so this check is technically not needed, but since this
  23. # is an auto-enabling integration, we might get to executing this import in
  24. # lower Python versions, so we need to deal with it.
  25. raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer")
  26. try:
  27. from strawberry import Schema
  28. from strawberry.extensions import SchemaExtension
  29. from strawberry.extensions.tracing.utils import (
  30. should_skip_tracing as strawberry_should_skip_tracing,
  31. )
  32. from strawberry.http import async_base_view, sync_base_view
  33. except ImportError:
  34. raise DidNotEnable("strawberry-graphql is not installed")
  35. try:
  36. from strawberry.extensions.tracing import (
  37. SentryTracingExtension as StrawberrySentryAsyncExtension,
  38. SentryTracingExtensionSync as StrawberrySentrySyncExtension,
  39. )
  40. except ImportError:
  41. StrawberrySentryAsyncExtension = None
  42. StrawberrySentrySyncExtension = None
  43. from typing import TYPE_CHECKING
  44. if TYPE_CHECKING:
  45. from typing import Any, Callable, Generator, List, Optional
  46. from graphql import GraphQLError, GraphQLResolveInfo
  47. from strawberry.http import GraphQLHTTPResponse
  48. from strawberry.types import ExecutionContext
  49. from sentry_sdk._types import Event, EventProcessor
  50. ignore_logger("strawberry.execution")
  51. class StrawberryIntegration(Integration):
  52. identifier = "strawberry"
  53. origin = f"auto.graphql.{identifier}"
  54. def __init__(self, async_execution=None):
  55. # type: (Optional[bool]) -> None
  56. if async_execution not in (None, False, True):
  57. raise ValueError(
  58. 'Invalid value for async_execution: "{}" (must be bool)'.format(
  59. async_execution
  60. )
  61. )
  62. self.async_execution = async_execution
  63. @staticmethod
  64. def setup_once():
  65. # type: () -> None
  66. version = package_version("strawberry-graphql")
  67. _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql")
  68. _patch_schema_init()
  69. _patch_views()
  70. def _patch_schema_init():
  71. # type: () -> None
  72. old_schema_init = Schema.__init__
  73. @functools.wraps(old_schema_init)
  74. def _sentry_patched_schema_init(self, *args, **kwargs):
  75. # type: (Schema, Any, Any) -> None
  76. integration = sentry_sdk.get_client().get_integration(StrawberryIntegration)
  77. if integration is None:
  78. return old_schema_init(self, *args, **kwargs)
  79. extensions = kwargs.get("extensions") or []
  80. if integration.async_execution is not None:
  81. should_use_async_extension = integration.async_execution
  82. else:
  83. # try to figure it out ourselves
  84. should_use_async_extension = _guess_if_using_async(extensions)
  85. logger.info(
  86. "Assuming strawberry is running %s. If not, initialize it as StrawberryIntegration(async_execution=%s).",
  87. "async" if should_use_async_extension else "sync",
  88. "False" if should_use_async_extension else "True",
  89. )
  90. # remove the built in strawberry sentry extension, if present
  91. extensions = [
  92. extension
  93. for extension in extensions
  94. if extension
  95. not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension)
  96. ]
  97. # add our extension
  98. extensions.append(
  99. SentryAsyncExtension if should_use_async_extension else SentrySyncExtension
  100. )
  101. kwargs["extensions"] = extensions
  102. return old_schema_init(self, *args, **kwargs)
  103. Schema.__init__ = _sentry_patched_schema_init # type: ignore[method-assign]
  104. class SentryAsyncExtension(SchemaExtension):
  105. def __init__(
  106. self,
  107. *,
  108. execution_context=None,
  109. ):
  110. # type: (Any, Optional[ExecutionContext]) -> None
  111. if execution_context:
  112. self.execution_context = execution_context
  113. @cached_property
  114. def _resource_name(self):
  115. # type: () -> str
  116. query_hash = self.hash_query(self.execution_context.query) # type: ignore
  117. if self.execution_context.operation_name:
  118. return "{}:{}".format(self.execution_context.operation_name, query_hash)
  119. return query_hash
  120. def hash_query(self, query):
  121. # type: (str) -> str
  122. return hashlib.md5(query.encode("utf-8")).hexdigest()
  123. def on_operation(self):
  124. # type: () -> Generator[None, None, None]
  125. self._operation_name = self.execution_context.operation_name
  126. operation_type = "query"
  127. op = OP.GRAPHQL_QUERY
  128. if self.execution_context.query is None:
  129. self.execution_context.query = ""
  130. if self.execution_context.query.strip().startswith("mutation"):
  131. operation_type = "mutation"
  132. op = OP.GRAPHQL_MUTATION
  133. elif self.execution_context.query.strip().startswith("subscription"):
  134. operation_type = "subscription"
  135. op = OP.GRAPHQL_SUBSCRIPTION
  136. description = operation_type
  137. if self._operation_name:
  138. description += " {}".format(self._operation_name)
  139. sentry_sdk.add_breadcrumb(
  140. category="graphql.operation",
  141. data={
  142. "operation_name": self._operation_name,
  143. "operation_type": operation_type,
  144. },
  145. )
  146. scope = sentry_sdk.get_isolation_scope()
  147. event_processor = _make_request_event_processor(self.execution_context)
  148. scope.add_event_processor(event_processor)
  149. span = sentry_sdk.get_current_span()
  150. if span:
  151. self.graphql_span = span.start_child(
  152. op=op,
  153. name=description,
  154. origin=StrawberryIntegration.origin,
  155. )
  156. else:
  157. self.graphql_span = sentry_sdk.start_span(
  158. op=op,
  159. name=description,
  160. origin=StrawberryIntegration.origin,
  161. )
  162. self.graphql_span.set_data("graphql.operation.type", operation_type)
  163. self.graphql_span.set_data("graphql.operation.name", self._operation_name)
  164. self.graphql_span.set_data("graphql.document", self.execution_context.query)
  165. self.graphql_span.set_data("graphql.resource_name", self._resource_name)
  166. yield
  167. transaction = self.graphql_span.containing_transaction
  168. if transaction and self.execution_context.operation_name:
  169. transaction.name = self.execution_context.operation_name
  170. transaction.source = TransactionSource.COMPONENT
  171. transaction.op = op
  172. self.graphql_span.finish()
  173. def on_validate(self):
  174. # type: () -> Generator[None, None, None]
  175. self.validation_span = self.graphql_span.start_child(
  176. op=OP.GRAPHQL_VALIDATE,
  177. name="validation",
  178. origin=StrawberryIntegration.origin,
  179. )
  180. yield
  181. self.validation_span.finish()
  182. def on_parse(self):
  183. # type: () -> Generator[None, None, None]
  184. self.parsing_span = self.graphql_span.start_child(
  185. op=OP.GRAPHQL_PARSE,
  186. name="parsing",
  187. origin=StrawberryIntegration.origin,
  188. )
  189. yield
  190. self.parsing_span.finish()
  191. def should_skip_tracing(self, _next, info):
  192. # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], GraphQLResolveInfo) -> bool
  193. return strawberry_should_skip_tracing(_next, info)
  194. async def _resolve(self, _next, root, info, *args, **kwargs):
  195. # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any
  196. result = _next(root, info, *args, **kwargs)
  197. if isawaitable(result):
  198. result = await result
  199. return result
  200. async def resolve(self, _next, root, info, *args, **kwargs):
  201. # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any
  202. if self.should_skip_tracing(_next, info):
  203. return await self._resolve(_next, root, info, *args, **kwargs)
  204. field_path = "{}.{}".format(info.parent_type, info.field_name)
  205. with self.graphql_span.start_child(
  206. op=OP.GRAPHQL_RESOLVE,
  207. name="resolving {}".format(field_path),
  208. origin=StrawberryIntegration.origin,
  209. ) as span:
  210. span.set_data("graphql.field_name", info.field_name)
  211. span.set_data("graphql.parent_type", info.parent_type.name)
  212. span.set_data("graphql.field_path", field_path)
  213. span.set_data("graphql.path", ".".join(map(str, info.path.as_list())))
  214. return await self._resolve(_next, root, info, *args, **kwargs)
  215. class SentrySyncExtension(SentryAsyncExtension):
  216. def resolve(self, _next, root, info, *args, **kwargs):
  217. # type: (Callable[[Any, Any, Any, Any], Any], Any, GraphQLResolveInfo, str, Any) -> Any
  218. if self.should_skip_tracing(_next, info):
  219. return _next(root, info, *args, **kwargs)
  220. field_path = "{}.{}".format(info.parent_type, info.field_name)
  221. with self.graphql_span.start_child(
  222. op=OP.GRAPHQL_RESOLVE,
  223. name="resolving {}".format(field_path),
  224. origin=StrawberryIntegration.origin,
  225. ) as span:
  226. span.set_data("graphql.field_name", info.field_name)
  227. span.set_data("graphql.parent_type", info.parent_type.name)
  228. span.set_data("graphql.field_path", field_path)
  229. span.set_data("graphql.path", ".".join(map(str, info.path.as_list())))
  230. return _next(root, info, *args, **kwargs)
  231. def _patch_views():
  232. # type: () -> None
  233. old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors
  234. old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors
  235. def _sentry_patched_async_view_handle_errors(self, errors, response_data):
  236. # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None
  237. old_async_view_handle_errors(self, errors, response_data)
  238. _sentry_patched_handle_errors(self, errors, response_data)
  239. def _sentry_patched_sync_view_handle_errors(self, errors, response_data):
  240. # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None
  241. old_sync_view_handle_errors(self, errors, response_data)
  242. _sentry_patched_handle_errors(self, errors, response_data)
  243. @ensure_integration_enabled(StrawberryIntegration)
  244. def _sentry_patched_handle_errors(self, errors, response_data):
  245. # type: (Any, List[GraphQLError], GraphQLHTTPResponse) -> None
  246. if not errors:
  247. return
  248. scope = sentry_sdk.get_isolation_scope()
  249. event_processor = _make_response_event_processor(response_data)
  250. scope.add_event_processor(event_processor)
  251. with capture_internal_exceptions():
  252. for error in errors:
  253. event, hint = event_from_exception(
  254. error,
  255. client_options=sentry_sdk.get_client().options,
  256. mechanism={
  257. "type": StrawberryIntegration.identifier,
  258. "handled": False,
  259. },
  260. )
  261. sentry_sdk.capture_event(event, hint=hint)
  262. async_base_view.AsyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign]
  263. _sentry_patched_async_view_handle_errors
  264. )
  265. sync_base_view.SyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign]
  266. _sentry_patched_sync_view_handle_errors
  267. )
  268. def _make_request_event_processor(execution_context):
  269. # type: (ExecutionContext) -> EventProcessor
  270. def inner(event, hint):
  271. # type: (Event, dict[str, Any]) -> Event
  272. with capture_internal_exceptions():
  273. if should_send_default_pii():
  274. request_data = event.setdefault("request", {})
  275. request_data["api_target"] = "graphql"
  276. if not request_data.get("data"):
  277. data = {"query": execution_context.query} # type: dict[str, Any]
  278. if execution_context.variables:
  279. data["variables"] = execution_context.variables
  280. if execution_context.operation_name:
  281. data["operationName"] = execution_context.operation_name
  282. request_data["data"] = data
  283. else:
  284. try:
  285. del event["request"]["data"]
  286. except (KeyError, TypeError):
  287. pass
  288. return event
  289. return inner
  290. def _make_response_event_processor(response_data):
  291. # type: (GraphQLHTTPResponse) -> EventProcessor
  292. def inner(event, hint):
  293. # type: (Event, dict[str, Any]) -> Event
  294. with capture_internal_exceptions():
  295. if should_send_default_pii():
  296. contexts = event.setdefault("contexts", {})
  297. contexts["response"] = {"data": response_data}
  298. return event
  299. return inner
  300. def _guess_if_using_async(extensions):
  301. # type: (List[SchemaExtension]) -> bool
  302. if StrawberrySentryAsyncExtension in extensions:
  303. return True
  304. elif StrawberrySentrySyncExtension in extensions:
  305. return False
  306. return bool(
  307. {"starlette", "starlite", "litestar", "fastapi"} & set(_get_installed_modules())
  308. )