Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/integrations/tornado.py: 10%
115 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import weakref
2import contextlib
3from inspect import iscoroutinefunction
5from sentry_sdk.hub import Hub, _should_send_default_pii
6from sentry_sdk.tracing import TRANSACTION_SOURCE_COMPONENT, Transaction
7from sentry_sdk.utils import (
8 HAS_REAL_CONTEXTVARS,
9 CONTEXTVARS_ERROR_MESSAGE,
10 event_from_exception,
11 capture_internal_exceptions,
12 transaction_from_function,
13)
14from sentry_sdk.integrations import Integration, DidNotEnable
15from sentry_sdk.integrations._wsgi_common import (
16 RequestExtractor,
17 _filter_headers,
18 _is_json_content_type,
19)
20from sentry_sdk.integrations.logging import ignore_logger
21from sentry_sdk._compat import iteritems
23try:
24 from tornado import version_info as TORNADO_VERSION
25 from tornado.web import RequestHandler, HTTPError
26 from tornado.gen import coroutine
27except ImportError:
28 raise DidNotEnable("Tornado not installed")
30from sentry_sdk._types import MYPY
32if MYPY:
33 from typing import Any
34 from typing import Optional
35 from typing import Dict
36 from typing import Callable
37 from typing import Generator
39 from sentry_sdk._types import EventProcessor
42class TornadoIntegration(Integration):
43 identifier = "tornado"
45 @staticmethod
46 def setup_once():
47 # type: () -> None
48 if TORNADO_VERSION < (5, 0):
49 raise DidNotEnable("Tornado 5+ required")
51 if not HAS_REAL_CONTEXTVARS:
52 # Tornado is async. We better have contextvars or we're going to leak
53 # state between requests.
54 raise DidNotEnable(
55 "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package"
56 + CONTEXTVARS_ERROR_MESSAGE
57 )
59 ignore_logger("tornado.access")
61 old_execute = RequestHandler._execute
63 awaitable = iscoroutinefunction(old_execute)
65 if awaitable:
66 # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await)
67 # In that case our method should be a coroutine function too
68 async def sentry_execute_request_handler(self, *args, **kwargs):
69 # type: (RequestHandler, *Any, **Any) -> Any
70 with _handle_request_impl(self):
71 return await old_execute(self, *args, **kwargs)
73 else:
75 @coroutine # type: ignore
76 def sentry_execute_request_handler(self, *args, **kwargs): # type: ignore
77 # type: (RequestHandler, *Any, **Any) -> Any
78 with _handle_request_impl(self):
79 result = yield from old_execute(self, *args, **kwargs)
80 return result
82 RequestHandler._execute = sentry_execute_request_handler
84 old_log_exception = RequestHandler.log_exception
86 def sentry_log_exception(self, ty, value, tb, *args, **kwargs):
87 # type: (Any, type, BaseException, Any, *Any, **Any) -> Optional[Any]
88 _capture_exception(ty, value, tb)
89 return old_log_exception(self, ty, value, tb, *args, **kwargs)
91 RequestHandler.log_exception = sentry_log_exception
94@contextlib.contextmanager
95def _handle_request_impl(self):
96 # type: (RequestHandler) -> Generator[None, None, None]
97 hub = Hub.current
98 integration = hub.get_integration(TornadoIntegration)
100 if integration is None:
101 yield
103 weak_handler = weakref.ref(self)
105 with Hub(hub) as hub:
106 with hub.configure_scope() as scope:
107 scope.clear_breadcrumbs()
108 processor = _make_event_processor(weak_handler)
109 scope.add_event_processor(processor)
111 transaction = Transaction.continue_from_headers(
112 self.request.headers,
113 op="http.server",
114 # Like with all other integrations, this is our
115 # fallback transaction in case there is no route.
116 # sentry_urldispatcher_resolve is responsible for
117 # setting a transaction name later.
118 name="generic Tornado request",
119 )
121 with hub.start_transaction(
122 transaction, custom_sampling_context={"tornado_request": self.request}
123 ):
124 yield
127def _capture_exception(ty, value, tb):
128 # type: (type, BaseException, Any) -> None
129 hub = Hub.current
130 if hub.get_integration(TornadoIntegration) is None:
131 return
132 if isinstance(value, HTTPError):
133 return
135 # If an integration is there, a client has to be there.
136 client = hub.client # type: Any
138 event, hint = event_from_exception(
139 (ty, value, tb),
140 client_options=client.options,
141 mechanism={"type": "tornado", "handled": False},
142 )
144 hub.capture_event(event, hint=hint)
147def _make_event_processor(weak_handler):
148 # type: (Callable[[], RequestHandler]) -> EventProcessor
149 def tornado_processor(event, hint):
150 # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
151 handler = weak_handler()
152 if handler is None:
153 return event
155 request = handler.request
157 with capture_internal_exceptions():
158 method = getattr(handler, handler.request.method.lower())
159 event["transaction"] = transaction_from_function(method)
160 event["transaction_info"] = {"source": TRANSACTION_SOURCE_COMPONENT}
162 with capture_internal_exceptions():
163 extractor = TornadoRequestExtractor(request)
164 extractor.extract_into_event(event)
166 request_info = event["request"]
168 request_info["url"] = "%s://%s%s" % (
169 request.protocol,
170 request.host,
171 request.path,
172 )
174 request_info["query_string"] = request.query
175 request_info["method"] = request.method
176 request_info["env"] = {"REMOTE_ADDR": request.remote_ip}
177 request_info["headers"] = _filter_headers(dict(request.headers))
179 with capture_internal_exceptions():
180 if handler.current_user and _should_send_default_pii():
181 event.setdefault("user", {}).setdefault("is_authenticated", True)
183 return event
185 return tornado_processor
188class TornadoRequestExtractor(RequestExtractor):
189 def content_length(self):
190 # type: () -> int
191 if self.request.body is None:
192 return 0
193 return len(self.request.body)
195 def cookies(self):
196 # type: () -> Dict[str, str]
197 return {k: v.value for k, v in iteritems(self.request.cookies)}
199 def raw_data(self):
200 # type: () -> bytes
201 return self.request.body
203 def form(self):
204 # type: () -> Dict[str, Any]
205 return {
206 k: [v.decode("latin1", "replace") for v in vs]
207 for k, vs in iteritems(self.request.body_arguments)
208 }
210 def is_json(self):
211 # type: () -> bool
212 return _is_json_content_type(self.request.headers.get("content-type"))
214 def files(self):
215 # type: () -> Dict[str, Any]
216 return {k: v[0] for k, v in iteritems(self.request.files) if v}
218 def size_of_file(self, file):
219 # type: (Any) -> int
220 return len(file.body or ())