tornado.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import weakref
  2. import contextlib
  3. from inspect import iscoroutinefunction
  4. import sentry_sdk
  5. from sentry_sdk.api import continue_trace
  6. from sentry_sdk.consts import OP
  7. from sentry_sdk.scope import should_send_default_pii
  8. from sentry_sdk.tracing import TransactionSource
  9. from sentry_sdk.utils import (
  10. HAS_REAL_CONTEXTVARS,
  11. CONTEXTVARS_ERROR_MESSAGE,
  12. ensure_integration_enabled,
  13. event_from_exception,
  14. capture_internal_exceptions,
  15. transaction_from_function,
  16. )
  17. from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
  18. from sentry_sdk.integrations._wsgi_common import (
  19. RequestExtractor,
  20. _filter_headers,
  21. _is_json_content_type,
  22. )
  23. from sentry_sdk.integrations.logging import ignore_logger
  24. try:
  25. from tornado import version_info as TORNADO_VERSION
  26. from tornado.web import RequestHandler, HTTPError
  27. from tornado.gen import coroutine
  28. except ImportError:
  29. raise DidNotEnable("Tornado not installed")
  30. from typing import TYPE_CHECKING
  31. if TYPE_CHECKING:
  32. from typing import Any
  33. from typing import Optional
  34. from typing import Dict
  35. from typing import Callable
  36. from typing import Generator
  37. from sentry_sdk._types import Event, EventProcessor
  38. class TornadoIntegration(Integration):
  39. identifier = "tornado"
  40. origin = f"auto.http.{identifier}"
  41. @staticmethod
  42. def setup_once():
  43. # type: () -> None
  44. _check_minimum_version(TornadoIntegration, TORNADO_VERSION)
  45. if not HAS_REAL_CONTEXTVARS:
  46. # Tornado is async. We better have contextvars or we're going to leak
  47. # state between requests.
  48. raise DidNotEnable(
  49. "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package"
  50. + CONTEXTVARS_ERROR_MESSAGE
  51. )
  52. ignore_logger("tornado.access")
  53. old_execute = RequestHandler._execute
  54. awaitable = iscoroutinefunction(old_execute)
  55. if awaitable:
  56. # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await)
  57. # In that case our method should be a coroutine function too
  58. async def sentry_execute_request_handler(self, *args, **kwargs):
  59. # type: (RequestHandler, *Any, **Any) -> Any
  60. with _handle_request_impl(self):
  61. return await old_execute(self, *args, **kwargs)
  62. else:
  63. @coroutine # type: ignore
  64. def sentry_execute_request_handler(self, *args, **kwargs):
  65. # type: (RequestHandler, *Any, **Any) -> Any
  66. with _handle_request_impl(self):
  67. result = yield from old_execute(self, *args, **kwargs)
  68. return result
  69. RequestHandler._execute = sentry_execute_request_handler
  70. old_log_exception = RequestHandler.log_exception
  71. def sentry_log_exception(self, ty, value, tb, *args, **kwargs):
  72. # type: (Any, type, BaseException, Any, *Any, **Any) -> Optional[Any]
  73. _capture_exception(ty, value, tb)
  74. return old_log_exception(self, ty, value, tb, *args, **kwargs)
  75. RequestHandler.log_exception = sentry_log_exception
  76. @contextlib.contextmanager
  77. def _handle_request_impl(self):
  78. # type: (RequestHandler) -> Generator[None, None, None]
  79. integration = sentry_sdk.get_client().get_integration(TornadoIntegration)
  80. if integration is None:
  81. yield
  82. weak_handler = weakref.ref(self)
  83. with sentry_sdk.isolation_scope() as scope:
  84. headers = self.request.headers
  85. scope.clear_breadcrumbs()
  86. processor = _make_event_processor(weak_handler)
  87. scope.add_event_processor(processor)
  88. transaction = continue_trace(
  89. headers,
  90. op=OP.HTTP_SERVER,
  91. # Like with all other integrations, this is our
  92. # fallback transaction in case there is no route.
  93. # sentry_urldispatcher_resolve is responsible for
  94. # setting a transaction name later.
  95. name="generic Tornado request",
  96. source=TransactionSource.ROUTE,
  97. origin=TornadoIntegration.origin,
  98. )
  99. with sentry_sdk.start_transaction(
  100. transaction, custom_sampling_context={"tornado_request": self.request}
  101. ):
  102. yield
  103. @ensure_integration_enabled(TornadoIntegration)
  104. def _capture_exception(ty, value, tb):
  105. # type: (type, BaseException, Any) -> None
  106. if isinstance(value, HTTPError):
  107. return
  108. event, hint = event_from_exception(
  109. (ty, value, tb),
  110. client_options=sentry_sdk.get_client().options,
  111. mechanism={"type": "tornado", "handled": False},
  112. )
  113. sentry_sdk.capture_event(event, hint=hint)
  114. def _make_event_processor(weak_handler):
  115. # type: (Callable[[], RequestHandler]) -> EventProcessor
  116. def tornado_processor(event, hint):
  117. # type: (Event, dict[str, Any]) -> Event
  118. handler = weak_handler()
  119. if handler is None:
  120. return event
  121. request = handler.request
  122. with capture_internal_exceptions():
  123. method = getattr(handler, handler.request.method.lower())
  124. event["transaction"] = transaction_from_function(method) or ""
  125. event["transaction_info"] = {"source": TransactionSource.COMPONENT}
  126. with capture_internal_exceptions():
  127. extractor = TornadoRequestExtractor(request)
  128. extractor.extract_into_event(event)
  129. request_info = event["request"]
  130. request_info["url"] = "%s://%s%s" % (
  131. request.protocol,
  132. request.host,
  133. request.path,
  134. )
  135. request_info["query_string"] = request.query
  136. request_info["method"] = request.method
  137. request_info["env"] = {"REMOTE_ADDR": request.remote_ip}
  138. request_info["headers"] = _filter_headers(dict(request.headers))
  139. with capture_internal_exceptions():
  140. if handler.current_user and should_send_default_pii():
  141. event.setdefault("user", {}).setdefault("is_authenticated", True)
  142. return event
  143. return tornado_processor
  144. class TornadoRequestExtractor(RequestExtractor):
  145. def content_length(self):
  146. # type: () -> int
  147. if self.request.body is None:
  148. return 0
  149. return len(self.request.body)
  150. def cookies(self):
  151. # type: () -> Dict[str, str]
  152. return {k: v.value for k, v in self.request.cookies.items()}
  153. def raw_data(self):
  154. # type: () -> bytes
  155. return self.request.body
  156. def form(self):
  157. # type: () -> Dict[str, Any]
  158. return {
  159. k: [v.decode("latin1", "replace") for v in vs]
  160. for k, vs in self.request.body_arguments.items()
  161. }
  162. def is_json(self):
  163. # type: () -> bool
  164. return _is_json_content_type(self.request.headers.get("content-type"))
  165. def files(self):
  166. # type: () -> Dict[str, Any]
  167. return {k: v[0] for k, v in self.request.files.items() if v}
  168. def size_of_file(self, file):
  169. # type: (Any) -> int
  170. return len(file.body or ())