tornado.py 7.0 KB

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