diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/python/sentry-sdk/sentry_sdk/integrations | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/python/sentry-sdk/sentry_sdk/integrations')
36 files changed, 5785 insertions, 0 deletions
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/__init__.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/__init__.py new file mode 100644 index 0000000000..f264bc4855 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/__init__.py @@ -0,0 +1,183 @@ +"""This package""" +from __future__ import absolute_import + +from threading import Lock + +from sentry_sdk._compat import iteritems +from sentry_sdk.utils import logger + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Callable + from typing import Dict + from typing import Iterator + from typing import List + from typing import Set + from typing import Tuple + from typing import Type + + +_installer_lock = Lock() +_installed_integrations = set() # type: Set[str] + + +def _generate_default_integrations_iterator(integrations, auto_enabling_integrations): + # type: (Tuple[str, ...], Tuple[str, ...]) -> Callable[[bool], Iterator[Type[Integration]]] + + def iter_default_integrations(with_auto_enabling_integrations): + # type: (bool) -> Iterator[Type[Integration]] + """Returns an iterator of the default integration classes: + """ + from importlib import import_module + + if with_auto_enabling_integrations: + all_import_strings = integrations + auto_enabling_integrations + else: + all_import_strings = integrations + + for import_string in all_import_strings: + try: + module, cls = import_string.rsplit(".", 1) + yield getattr(import_module(module), cls) + except (DidNotEnable, SyntaxError) as e: + logger.debug( + "Did not import default integration %s: %s", import_string, e + ) + + if isinstance(iter_default_integrations.__doc__, str): + for import_string in integrations: + iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) + + return iter_default_integrations + + +_AUTO_ENABLING_INTEGRATIONS = ( + "sentry_sdk.integrations.django.DjangoIntegration", + "sentry_sdk.integrations.flask.FlaskIntegration", + "sentry_sdk.integrations.bottle.BottleIntegration", + "sentry_sdk.integrations.falcon.FalconIntegration", + "sentry_sdk.integrations.sanic.SanicIntegration", + "sentry_sdk.integrations.celery.CeleryIntegration", + "sentry_sdk.integrations.rq.RqIntegration", + "sentry_sdk.integrations.aiohttp.AioHttpIntegration", + "sentry_sdk.integrations.tornado.TornadoIntegration", + "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", +) + + +iter_default_integrations = _generate_default_integrations_iterator( + integrations=( + # stdlib/base runtime integrations + "sentry_sdk.integrations.logging.LoggingIntegration", + "sentry_sdk.integrations.stdlib.StdlibIntegration", + "sentry_sdk.integrations.excepthook.ExcepthookIntegration", + "sentry_sdk.integrations.dedupe.DedupeIntegration", + "sentry_sdk.integrations.atexit.AtexitIntegration", + "sentry_sdk.integrations.modules.ModulesIntegration", + "sentry_sdk.integrations.argv.ArgvIntegration", + "sentry_sdk.integrations.threading.ThreadingIntegration", + ), + auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, +) + +del _generate_default_integrations_iterator + + +def setup_integrations( + integrations, with_defaults=True, with_auto_enabling_integrations=False +): + # type: (List[Integration], bool, bool) -> Dict[str, Integration] + """Given a list of integration instances this installs them all. When + `with_defaults` is set to `True` then all default integrations are added + unless they were already provided before. + """ + integrations = dict( + (integration.identifier, integration) for integration in integrations or () + ) + + logger.debug("Setting up integrations (with default = %s)", with_defaults) + + # Integrations that are not explicitly set up by the user. + used_as_default_integration = set() + + if with_defaults: + for integration_cls in iter_default_integrations( + with_auto_enabling_integrations + ): + if integration_cls.identifier not in integrations: + instance = integration_cls() + integrations[instance.identifier] = instance + used_as_default_integration.add(instance.identifier) + + for identifier, integration in iteritems(integrations): + with _installer_lock: + if identifier not in _installed_integrations: + logger.debug( + "Setting up previously not enabled integration %s", identifier + ) + try: + type(integration).setup_once() + except NotImplementedError: + if getattr(integration, "install", None) is not None: + logger.warning( + "Integration %s: The install method is " + "deprecated. Use `setup_once`.", + identifier, + ) + integration.install() + else: + raise + except DidNotEnable as e: + if identifier not in used_as_default_integration: + raise + + logger.debug( + "Did not enable default integration %s: %s", identifier, e + ) + + _installed_integrations.add(identifier) + + for identifier in integrations: + logger.debug("Enabling integration %s", identifier) + + return integrations + + +class DidNotEnable(Exception): + """ + The integration could not be enabled due to a trivial user error like + `flask` not being installed for the `FlaskIntegration`. + + This exception is silently swallowed for default integrations, but reraised + for explicitly enabled integrations. + """ + + +class Integration(object): + """Baseclass for all integrations. + + To accept options for an integration, implement your own constructor that + saves those options on `self`. + """ + + install = None + """Legacy method, do not implement.""" + + identifier = None # type: str + """String unique ID of integration type""" + + @staticmethod + def setup_once(): + # type: () -> None + """ + Initialize the integration. + + This function is only called once, ever. Configuration is not available + at this point, so the only thing to do here is to hook into exception + handlers, and perhaps do monkeypatches. + + Inside those hooks `Integration.current` can be used to access the + instance again. + """ + raise NotImplementedError() diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/_wsgi_common.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/_wsgi_common.py new file mode 100644 index 0000000000..f874663883 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/_wsgi_common.py @@ -0,0 +1,180 @@ +import json + +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.utils import AnnotatedValue +from sentry_sdk._compat import text_type, iteritems + +from sentry_sdk._types import MYPY + +if MYPY: + import sentry_sdk + + from typing import Any + from typing import Dict + from typing import Optional + from typing import Union + + +SENSITIVE_ENV_KEYS = ( + "REMOTE_ADDR", + "HTTP_X_FORWARDED_FOR", + "HTTP_SET_COOKIE", + "HTTP_COOKIE", + "HTTP_AUTHORIZATION", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_REAL_IP", +) + +SENSITIVE_HEADERS = tuple( + x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") +) + + +def request_body_within_bounds(client, content_length): + # type: (Optional[sentry_sdk.Client], int) -> bool + if client is None: + return False + + bodies = client.options["request_bodies"] + return not ( + bodies == "never" + or (bodies == "small" and content_length > 10 ** 3) + or (bodies == "medium" and content_length > 10 ** 4) + ) + + +class RequestExtractor(object): + def __init__(self, request): + # type: (Any) -> None + self.request = request + + def extract_into_event(self, event): + # type: (Dict[str, Any]) -> None + client = Hub.current.client + if client is None: + return + + data = None # type: Optional[Union[AnnotatedValue, Dict[str, Any]]] + + content_length = self.content_length() + request_info = event.get("request", {}) + + if _should_send_default_pii(): + request_info["cookies"] = dict(self.cookies()) + + if not request_body_within_bounds(client, content_length): + data = AnnotatedValue( + "", + {"rem": [["!config", "x", 0, content_length]], "len": content_length}, + ) + else: + parsed_body = self.parsed_body() + if parsed_body is not None: + data = parsed_body + elif self.raw_data(): + data = AnnotatedValue( + "", + {"rem": [["!raw", "x", 0, content_length]], "len": content_length}, + ) + else: + data = None + + if data is not None: + request_info["data"] = data + + event["request"] = request_info + + def content_length(self): + # type: () -> int + try: + return int(self.env().get("CONTENT_LENGTH", 0)) + except ValueError: + return 0 + + def cookies(self): + # type: () -> Dict[str, Any] + raise NotImplementedError() + + def raw_data(self): + # type: () -> Optional[Union[str, bytes]] + raise NotImplementedError() + + def form(self): + # type: () -> Optional[Dict[str, Any]] + raise NotImplementedError() + + def parsed_body(self): + # type: () -> Optional[Dict[str, Any]] + form = self.form() + files = self.files() + if form or files: + data = dict(iteritems(form)) + for k, v in iteritems(files): + size = self.size_of_file(v) + data[k] = AnnotatedValue( + "", {"len": size, "rem": [["!raw", "x", 0, size]]} + ) + + return data + + return self.json() + + def is_json(self): + # type: () -> bool + return _is_json_content_type(self.env().get("CONTENT_TYPE")) + + def json(self): + # type: () -> Optional[Any] + try: + if not self.is_json(): + return None + + raw_data = self.raw_data() + if raw_data is None: + return None + + if isinstance(raw_data, text_type): + return json.loads(raw_data) + else: + return json.loads(raw_data.decode("utf-8")) + except ValueError: + pass + + return None + + def files(self): + # type: () -> Optional[Dict[str, Any]] + raise NotImplementedError() + + def size_of_file(self, file): + # type: (Any) -> int + raise NotImplementedError() + + def env(self): + # type: () -> Dict[str, Any] + raise NotImplementedError() + + +def _is_json_content_type(ct): + # type: (Optional[str]) -> bool + mt = (ct or "").split(";", 1)[0] + return ( + mt == "application/json" + or (mt.startswith("application/")) + and mt.endswith("+json") + ) + + +def _filter_headers(headers): + # type: (Dict[str, str]) -> Dict[str, str] + if _should_send_default_pii(): + return headers + + return { + k: ( + v + if k.upper().replace("-", "_") not in SENSITIVE_HEADERS + else AnnotatedValue("", {"rem": [["!config", "x", 0, len(v)]]}) + ) + for k, v in iteritems(headers) + } diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/aiohttp.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/aiohttp.py new file mode 100644 index 0000000000..02c76df7ef --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/aiohttp.py @@ -0,0 +1,211 @@ +import sys +import weakref + +from sentry_sdk._compat import reraise +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations._wsgi_common import ( + _filter_headers, + request_body_within_bounds, +) +from sentry_sdk.tracing import Span +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + transaction_from_function, + HAS_REAL_CONTEXTVARS, + AnnotatedValue, +) + +try: + import asyncio + + from aiohttp import __version__ as AIOHTTP_VERSION + from aiohttp.web import Application, HTTPException, UrlDispatcher +except ImportError: + raise DidNotEnable("AIOHTTP not installed") + +from sentry_sdk._types import MYPY + +if MYPY: + from aiohttp.web_request import Request + from aiohttp.abc import AbstractMatchInfo + from typing import Any + from typing import Dict + from typing import Optional + from typing import Tuple + from typing import Callable + from typing import Union + + from sentry_sdk.utils import ExcInfo + from sentry_sdk._types import EventProcessor + + +class AioHttpIntegration(Integration): + identifier = "aiohttp" + + @staticmethod + def setup_once(): + # type: () -> None + + try: + version = tuple(map(int, AIOHTTP_VERSION.split("."))) + except (TypeError, ValueError): + raise DidNotEnable("AIOHTTP version unparseable: {}".format(version)) + + if version < (3, 4): + raise DidNotEnable("AIOHTTP 3.4 or newer required.") + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise RuntimeError( + "The aiohttp integration for Sentry requires Python 3.7+ " + " or aiocontextvars package" + ) + + ignore_logger("aiohttp.server") + + old_handle = Application._handle + + async def sentry_app_handle(self, request, *args, **kwargs): + # type: (Any, Request, *Any, **Any) -> Any + async def inner(): + # type: () -> Any + hub = Hub.current + if hub.get_integration(AioHttpIntegration) is None: + return await old_handle(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + + with Hub(Hub.current) as hub: + with hub.configure_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + span = Span.continue_from_headers(request.headers) + span.op = "http.server" + # If this transaction name makes it to the UI, AIOHTTP's + # URL resolver did not find a route or died trying. + span.transaction = "generic AIOHTTP request" + + with hub.start_span(span): + try: + response = await old_handle(self, request) + except HTTPException as e: + span.set_http_status(e.status_code) + raise + except asyncio.CancelledError: + span.set_status("cancelled") + raise + except Exception: + # This will probably map to a 500 but seems like we + # have no way to tell. Do not set span status. + reraise(*_capture_exception(hub)) + + span.set_http_status(response.status) + return response + + # Explicitly wrap in task such that current contextvar context is + # copied. Just doing `return await inner()` will leak scope data + # between requests. + return await asyncio.get_event_loop().create_task(inner()) + + Application._handle = sentry_app_handle + + old_urldispatcher_resolve = UrlDispatcher.resolve + + async def sentry_urldispatcher_resolve(self, request): + # type: (UrlDispatcher, Request) -> AbstractMatchInfo + rv = await old_urldispatcher_resolve(self, request) + + name = None + + try: + name = transaction_from_function(rv.handler) + except Exception: + pass + + if name is not None: + with Hub.current.configure_scope() as scope: + scope.transaction = name + + return rv + + UrlDispatcher.resolve = sentry_urldispatcher_resolve + + +def _make_request_processor(weak_request): + # type: (Callable[[], Request]) -> EventProcessor + def aiohttp_processor( + event, # type: Dict[str, Any] + hint, # type: Dict[str, Tuple[type, BaseException, Any]] + ): + # type: (...) -> Dict[str, Any] + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + request_info = event.setdefault("request", {}) + + request_info["url"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + + request_info["query_string"] = request.query_string + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote} + + hub = Hub.current + request_info["headers"] = _filter_headers(dict(request.headers)) + + # Just attach raw data here if it is within bounds, if available. + # Unfortunately there's no way to get structured data from aiohttp + # without awaiting on some coroutine. + request_info["data"] = get_aiohttp_request_data(hub, request) + + return event + + return aiohttp_processor + + +def _capture_exception(hub): + # type: (Hub) -> ExcInfo + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=hub.client.options, # type: ignore + mechanism={"type": "aiohttp", "handled": False}, + ) + hub.capture_event(event, hint=hint) + return exc_info + + +BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]" + + +def get_aiohttp_request_data(hub, request): + # type: (Hub, Request) -> Union[Optional[str], AnnotatedValue] + bytes_body = request._read_bytes + + if bytes_body is not None: + # we have body to show + if not request_body_within_bounds(hub.client, len(bytes_body)): + + return AnnotatedValue( + "", + {"rem": [["!config", "x", 0, len(bytes_body)]], "len": len(bytes_body)}, + ) + encoding = request.charset or "utf-8" + return bytes_body.decode(encoding, "replace") + + if request.can_read_body: + # body exists but we can't show it + return BODY_NOT_READ_MESSAGE + + # request has no body + return None diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/argv.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/argv.py new file mode 100644 index 0000000000..f005521d32 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/argv.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import + +import sys + +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class ArgvIntegration(Integration): + identifier = "argv" + + @staticmethod + def setup_once(): + # type: () -> None + @add_global_event_processor + def processor(event, hint): + # type: (Event, Optional[Hint]) -> Optional[Event] + if Hub.current.get_integration(ArgvIntegration) is not None: + extra = event.setdefault("extra", {}) + # If some event processor decided to set extra to e.g. an + # `int`, don't crash. Not here. + if isinstance(extra, dict): + extra["sys.argv"] = sys.argv + + return event diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/asgi.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/asgi.py new file mode 100644 index 0000000000..762634f82f --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/asgi.py @@ -0,0 +1,194 @@ +""" +An ASGI middleware. + +Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi>`_. +""" + +import asyncio +import functools +import inspect +import urllib + +from sentry_sdk._types import MYPY +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.utils import ContextVar, event_from_exception, transaction_from_function +from sentry_sdk.tracing import Span + +if MYPY: + from typing import Dict + from typing import Any + from typing import Optional + from typing import Callable + + from sentry_sdk._types import Event, Hint + + +_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") + + +def _capture_exception(hub, exc): + # type: (Hub, Any) -> None + + # Check client here as it might have been unset while streaming response + if hub.client is not None: + event, hint = event_from_exception( + exc, + client_options=hub.client.options, + mechanism={"type": "asgi", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + +def _looks_like_asgi3(app): + # type: (Any) -> bool + """ + Try to figure out if an application object supports ASGI3. + + This is how uvicorn figures out the application version as well. + """ + if inspect.isclass(app): + return hasattr(app, "__await__") + elif inspect.isfunction(app): + return asyncio.iscoroutinefunction(app) + else: + call = getattr(app, "__call__", None) # noqa + return asyncio.iscoroutinefunction(call) + + +class SentryAsgiMiddleware: + __slots__ = ("app", "__call__") + + def __init__(self, app): + # type: (Any) -> None + self.app = app + + if _looks_like_asgi3(app): + self.__call__ = self._run_asgi3 # type: Callable[..., Any] + else: + self.__call__ = self._run_asgi2 + + def _run_asgi2(self, scope): + # type: (Any) -> Any + async def inner(receive, send): + # type: (Any, Any) -> Any + return await self._run_app(scope, lambda: self.app(scope)(receive, send)) + + return inner + + async def _run_asgi3(self, scope, receive, send): + # type: (Any, Any, Any) -> Any + return await self._run_app(scope, lambda: self.app(scope, receive, send)) + + async def _run_app(self, scope, callback): + # type: (Any, Any) -> Any + if _asgi_middleware_applied.get(False): + return await callback() + + _asgi_middleware_applied.set(True) + try: + hub = Hub(Hub.current) + with hub: + with hub.configure_scope() as sentry_scope: + sentry_scope.clear_breadcrumbs() + sentry_scope._name = "asgi" + processor = functools.partial( + self.event_processor, asgi_scope=scope + ) + sentry_scope.add_event_processor(processor) + + if scope["type"] in ("http", "websocket"): + span = Span.continue_from_headers(dict(scope["headers"])) + span.op = "{}.server".format(scope["type"]) + else: + span = Span() + span.op = "asgi.server" + + span.set_tag("asgi.type", scope["type"]) + span.transaction = "generic ASGI request" + + with hub.start_span(span) as span: + # XXX: Would be cool to have correct span status, but we + # would have to wrap send(). That is a bit hard to do with + # the current abstraction over ASGI 2/3. + try: + return await callback() + except Exception as exc: + _capture_exception(hub, exc) + raise exc from None + finally: + _asgi_middleware_applied.set(False) + + def event_processor(self, event, hint, asgi_scope): + # type: (Event, Hint, Any) -> Optional[Event] + request_info = event.get("request", {}) + + if asgi_scope["type"] in ("http", "websocket"): + request_info["url"] = self.get_url(asgi_scope) + request_info["method"] = asgi_scope["method"] + request_info["headers"] = _filter_headers(self.get_headers(asgi_scope)) + request_info["query_string"] = self.get_query(asgi_scope) + + if asgi_scope.get("client") and _should_send_default_pii(): + request_info["env"] = {"REMOTE_ADDR": asgi_scope["client"][0]} + + if asgi_scope.get("endpoint"): + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our path-based transaction name. + event["transaction"] = self.get_transaction(asgi_scope) + + event["request"] = request_info + + return event + + def get_url(self, scope): + # type: (Any) -> str + """ + Extract URL from the ASGI scope, without also including the querystring. + """ + scheme = scope.get("scheme", "http") + server = scope.get("server", None) + path = scope.get("root_path", "") + scope["path"] + + for key, value in scope["headers"]: + if key == b"host": + host_header = value.decode("latin-1") + return "%s://%s%s" % (scheme, host_header, path) + + if server is not None: + host, port = server + default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme] + if port != default_port: + return "%s://%s:%s%s" % (scheme, host, port, path) + return "%s://%s%s" % (scheme, host, path) + return path + + def get_query(self, scope): + # type: (Any) -> Any + """ + Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. + """ + return urllib.parse.unquote(scope["query_string"].decode("latin-1")) + + def get_headers(self, scope): + # type: (Any) -> Dict[str, Any] + """ + Extract headers from the ASGI scope, in the format that the Sentry protocol expects. + """ + headers = {} # type: Dict[str, str] + for raw_key, raw_value in scope["headers"]: + key = raw_key.decode("latin-1") + value = raw_value.decode("latin-1") + if key in headers: + headers[key] = headers[key] + ", " + value + else: + headers[key] = value + return headers + + def get_transaction(self, scope): + # type: (Any) -> Optional[str] + """ + Return a transaction string to identify the routed endpoint. + """ + return transaction_from_function(scope["endpoint"]) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/atexit.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/atexit.py new file mode 100644 index 0000000000..18fe657bff --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/atexit.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import + +import os +import sys +import atexit + +from sentry_sdk.hub import Hub +from sentry_sdk.utils import logger +from sentry_sdk.integrations import Integration + +from sentry_sdk._types import MYPY + +if MYPY: + + from typing import Any + from typing import Optional + + +def default_callback(pending, timeout): + # type: (int, int) -> None + """This is the default shutdown callback that is set on the options. + It prints out a message to stderr that informs the user that some events + are still pending and the process is waiting for them to flush out. + """ + + def echo(msg): + # type: (str) -> None + sys.stderr.write(msg + "\n") + + echo("Sentry is attempting to send %i pending error messages" % pending) + echo("Waiting up to %s seconds" % timeout) + echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C")) + sys.stderr.flush() + + +class AtexitIntegration(Integration): + identifier = "atexit" + + def __init__(self, callback=None): + # type: (Optional[Any]) -> None + if callback is None: + callback = default_callback + self.callback = callback + + @staticmethod + def setup_once(): + # type: () -> None + @atexit.register + def _shutdown(): + # type: () -> None + logger.debug("atexit: got shutdown signal") + hub = Hub.main + integration = hub.get_integration(AtexitIntegration) + if integration is not None: + logger.debug("atexit: shutting down client") + + # If there is a session on the hub, close it now. + hub.end_session() + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + client.close(callback=integration.callback) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/aws_lambda.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/aws_lambda.py new file mode 100644 index 0000000000..3a08d998db --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/aws_lambda.py @@ -0,0 +1,254 @@ +from datetime import datetime, timedelta +from os import environ +import sys + +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk._compat import reraise +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + event_from_exception, + logger, +) +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import TypeVar + from typing import Callable + from typing import Optional + + from sentry_sdk._types import EventProcessor, Event, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + + +def _wrap_handler(handler): + # type: (F) -> F + def sentry_handler(event, context, *args, **kwargs): + # type: (Any, Any, *Any, **Any) -> Any + hub = Hub.current + integration = hub.get_integration(AwsLambdaIntegration) + if integration is None: + return handler(event, context, *args, **kwargs) + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + with hub.push_scope() as scope: + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope.transaction = context.function_name + scope.add_event_processor(_make_request_event_processor(event, context)) + + try: + return handler(event, context, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + hub.capture_event(event, hint=hint) + reraise(*exc_info) + + return sentry_handler # type: ignore + + +def _drain_queue(): + # type: () -> None + with capture_internal_exceptions(): + hub = Hub.current + integration = hub.get_integration(AwsLambdaIntegration) + if integration is not None: + # Flush out the event queue before AWS kills the + # process. + hub.flush() + + +class AwsLambdaIntegration(Integration): + identifier = "aws_lambda" + + @staticmethod + def setup_once(): + # type: () -> None + import __main__ as lambda_bootstrap # type: ignore + + pre_37 = True # Python 3.6 or 2.7 + + if not hasattr(lambda_bootstrap, "handle_http_request"): + try: + import bootstrap as lambda_bootstrap # type: ignore + + pre_37 = False # Python 3.7 + except ImportError: + pass + + if not hasattr(lambda_bootstrap, "handle_event_request"): + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled" + ) + return + + if pre_37: + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request(request_handler, *args, **kwargs): + # type: (Any, *Any, **Any) -> Any + request_handler = _wrap_handler(request_handler) + return old_handle_event_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + old_handle_http_request = lambda_bootstrap.handle_http_request + + def sentry_handle_http_request(request_handler, *args, **kwargs): + # type: (Any, *Any, **Any) -> Any + request_handler = _wrap_handler(request_handler) + return old_handle_http_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_http_request = sentry_handle_http_request + + # Patch to_json to drain the queue. This should work even when the + # SDK is initialized inside of the handler + + old_to_json = lambda_bootstrap.to_json + + def sentry_to_json(*args, **kwargs): + # type: (*Any, **Any) -> Any + _drain_queue() + return old_to_json(*args, **kwargs) + + lambda_bootstrap.to_json = sentry_to_json + else: + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( # type: ignore + lambda_runtime_client, request_handler, *args, **kwargs + ): + request_handler = _wrap_handler(request_handler) + return old_handle_event_request( + lambda_runtime_client, request_handler, *args, **kwargs + ) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + # Patch the runtime client to drain the queue. This should work + # even when the SDK is initialized inside of the handler + + def _wrap_post_function(f): + # type: (F) -> F + def inner(*args, **kwargs): + # type: (*Any, **Any) -> Any + _drain_queue() + return f(*args, **kwargs) + + return inner # type: ignore + + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result + ) + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error + ) + + +def _make_request_event_processor(aws_event, aws_context): + # type: (Any, Any) -> EventProcessor + start_time = datetime.now() + + def event_processor(event, hint, start_time=start_time): + # type: (Event, Hint, datetime) -> Optional[Event] + extra = event.setdefault("extra", {}) + extra["lambda"] = { + "function_name": aws_context.function_name, + "function_version": aws_context.function_version, + "invoked_function_arn": aws_context.invoked_function_arn, + "remaining_time_in_millis": aws_context.get_remaining_time_in_millis(), + "aws_request_id": aws_context.aws_request_id, + } + + extra["cloudwatch logs"] = { + "url": _get_cloudwatch_logs_url(aws_context, start_time), + "log_group": aws_context.log_group_name, + "log_stream": aws_context.log_stream_name, + } + + request = event.get("request", {}) + + if "httpMethod" in aws_event: + request["method"] = aws_event["httpMethod"] + + request["url"] = _get_url(aws_event, aws_context) + + if "queryStringParameters" in aws_event: + request["query_string"] = aws_event["queryStringParameters"] + + if "headers" in aws_event: + request["headers"] = _filter_headers(aws_event["headers"]) + + if aws_event.get("body", None): + # Unfortunately couldn't find a way to get structured body from AWS + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue("", {"rem": [["!raw", "x", 0, 0]]}) + + if _should_send_default_pii(): + user_info = event.setdefault("user", {}) + + id = aws_event.get("identity", {}).get("userArn") + if id is not None: + user_info.setdefault("id", id) + + ip = aws_event.get("identity", {}).get("sourceIp") + if ip is not None: + user_info.setdefault("ip_address", ip) + + event["request"] = request + + return event + + return event_processor + + +def _get_url(event, context): + # type: (Any, Any) -> str + path = event.get("path", None) + headers = event.get("headers", {}) + host = headers.get("Host", None) + proto = headers.get("X-Forwarded-Proto", None) + if proto and host and path: + return "{}://{}{}".format(proto, host, path) + return "awslambda:///{}".format(context.function_name) + + +def _get_cloudwatch_logs_url(context, start_time): + # type: (Any, datetime) -> str + """ + Generates a CloudWatchLogs console URL based on the context object + + Arguments: + context {Any} -- context from lambda handler + + Returns: + str -- AWS Console URL to logs. + """ + formatstring = "%Y-%m-%dT%H:%M:%S" + + url = ( + "https://console.aws.amazon.com/cloudwatch/home?region={region}" + "#logEventViewer:group={log_group};stream={log_stream}" + ";start={start_time};end={end_time}" + ).format( + region=environ.get("AWS_REGION"), + log_group=context.log_group_name, + log_stream=context.log_stream_name, + start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), + end_time=(datetime.now() + timedelta(seconds=2)).strftime(formatstring), + ) + + return url diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/beam.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/beam.py new file mode 100644 index 0000000000..7252746a7f --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/beam.py @@ -0,0 +1,184 @@ +from __future__ import absolute_import + +import sys +import types +from functools import wraps + +from sentry_sdk.hub import Hub +from sentry_sdk._compat import reraise +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Iterator + from typing import TypeVar + from typing import Optional + from typing import Callable + + from sentry_sdk.client import Client + from sentry_sdk._types import ExcInfo + + T = TypeVar("T") + F = TypeVar("F", bound=Callable[..., Any]) + + +WRAPPED_FUNC = "_wrapped_{}_" +INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py +USED_FUNC = "_sentry_used_" + + +class BeamIntegration(Integration): + identifier = "beam" + + @staticmethod + def setup_once(): + # type: () -> None + from apache_beam.transforms.core import DoFn, ParDo # type: ignore + + ignore_logger("root") + ignore_logger("bundle_processor.create") + + function_patches = ["process", "start_bundle", "finish_bundle", "setup"] + for func_name in function_patches: + setattr( + DoFn, + INSPECT_FUNC.format(func_name), + _wrap_inspect_call(DoFn, func_name), + ) + + old_init = ParDo.__init__ + + def sentry_init_pardo(self, fn, *args, **kwargs): + # type: (ParDo, Any, *Any, **Any) -> Any + # Do not monkey patch init twice + if not getattr(self, "_sentry_is_patched", False): + for func_name in function_patches: + if not hasattr(fn, func_name): + continue + wrapped_func = WRAPPED_FUNC.format(func_name) + + # Check to see if inspect is set and process is not + # to avoid monkey patching process twice. + # Check to see if function is part of object for + # backwards compatibility. + process_func = getattr(fn, func_name) + inspect_func = getattr(fn, INSPECT_FUNC.format(func_name)) + if not getattr(inspect_func, USED_FUNC, False) and not getattr( + process_func, USED_FUNC, False + ): + setattr(fn, wrapped_func, process_func) + setattr(fn, func_name, _wrap_task_call(process_func)) + + self._sentry_is_patched = True + old_init(self, fn, *args, **kwargs) + + ParDo.__init__ = sentry_init_pardo + + +def _wrap_inspect_call(cls, func_name): + # type: (Any, Any) -> Any + from apache_beam.typehints.decorators import getfullargspec # type: ignore + + if not hasattr(cls, func_name): + return None + + def _inspect(self): + # type: (Any) -> Any + """ + Inspect function overrides the way Beam gets argspec. + """ + wrapped_func = WRAPPED_FUNC.format(func_name) + if hasattr(self, wrapped_func): + process_func = getattr(self, wrapped_func) + else: + process_func = getattr(self, func_name) + setattr(self, func_name, _wrap_task_call(process_func)) + setattr(self, wrapped_func, process_func) + + # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults + # (which uses Signatures internally) should be used instead. + try: + from apache_beam.transforms.core import get_function_args_defaults + + return get_function_args_defaults(process_func) + except ImportError: + return getfullargspec(process_func) + + setattr(_inspect, USED_FUNC, True) + return _inspect + + +def _wrap_task_call(func): + # type: (F) -> F + """ + Wrap task call with a try catch to get exceptions. + Pass the client on to raise_exception so it can get rebinded. + """ + client = Hub.current.client + + @wraps(func) + def _inner(*args, **kwargs): + # type: (*Any, **Any) -> Any + try: + gen = func(*args, **kwargs) + except Exception: + raise_exception(client) + + if not isinstance(gen, types.GeneratorType): + return gen + return _wrap_generator_call(gen, client) + + setattr(_inner, USED_FUNC, True) + return _inner # type: ignore + + +def _capture_exception(exc_info, hub): + # type: (ExcInfo, Hub) -> None + """ + Send Beam exception to Sentry. + """ + integration = hub.get_integration(BeamIntegration) + if integration is None: + return + + client = hub.client + if client is None: + return + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "beam", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + +def raise_exception(client): + # type: (Optional[Client]) -> None + """ + Raise an exception. If the client is not in the hub, rebind it. + """ + hub = Hub.current + if hub.client is None: + hub.bind_client(client) + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info, hub) + reraise(*exc_info) + + +def _wrap_generator_call(gen, client): + # type: (Iterator[T], Optional[Client]) -> Iterator[T] + """ + Wrap the generator to handle any failures. + """ + while True: + try: + yield next(gen) + except StopIteration: + break + except Exception: + raise_exception(client) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/bottle.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/bottle.py new file mode 100644 index 0000000000..80224e4dc4 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/bottle.py @@ -0,0 +1,199 @@ +from __future__ import absolute_import + +from sentry_sdk.hub import Hub +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + transaction_from_function, +) +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.integrations._wsgi_common import RequestExtractor + +from sentry_sdk._types import MYPY + +if MYPY: + from sentry_sdk.integrations.wsgi import _ScopedResponse + from typing import Any + from typing import Dict + from typing import Callable + from typing import Optional + from bottle import FileUpload, FormsDict, LocalRequest # type: ignore + + from sentry_sdk._types import EventProcessor + +try: + from bottle import ( + Bottle, + Route, + request as bottle_request, + HTTPResponse, + __version__ as BOTTLE_VERSION, + ) +except ImportError: + raise DidNotEnable("Bottle not installed") + + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class BottleIntegration(Integration): + identifier = "bottle" + + transaction_style = None + + def __init__(self, transaction_style="endpoint"): + # type: (str) -> None + + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once(): + # type: () -> None + + try: + version = tuple(map(int, BOTTLE_VERSION.split("."))) + except (TypeError, ValueError): + raise DidNotEnable("Unparseable Bottle version: {}".format(version)) + + if version < (0, 12): + raise DidNotEnable("Bottle 0.12 or newer required.") + + # monkey patch method Bottle.__call__ + old_app = Bottle.__call__ + + def sentry_patched_wsgi_app(self, environ, start_response): + # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse + + hub = Hub.current + integration = hub.get_integration(BottleIntegration) + if integration is None: + return old_app(self, environ, start_response) + + return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))( + environ, start_response + ) + + Bottle.__call__ = sentry_patched_wsgi_app + + # monkey patch method Bottle._handle + old_handle = Bottle._handle + + def _patched_handle(self, environ): + # type: (Bottle, Dict[str, Any]) -> Any + hub = Hub.current + integration = hub.get_integration(BottleIntegration) + if integration is None: + return old_handle(self, environ) + + # create new scope + scope_manager = hub.push_scope() + + with scope_manager: + app = self + with hub.configure_scope() as scope: + scope._name = "bottle" + scope.add_event_processor( + _make_request_event_processor(app, bottle_request, integration) + ) + res = old_handle(self, environ) + + # scope cleanup + return res + + Bottle._handle = _patched_handle + + # monkey patch method Route._make_callback + old_make_callback = Route._make_callback + + def patched_make_callback(self, *args, **kwargs): + # type: (Route, *object, **object) -> Any + hub = Hub.current + integration = hub.get_integration(BottleIntegration) + prepared_callback = old_make_callback(self, *args, **kwargs) + if integration is None: + return prepared_callback + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + def wrapped_callback(*args, **kwargs): + # type: (*object, **object) -> Any + + try: + res = prepared_callback(*args, **kwargs) + except HTTPResponse: + raise + except Exception as exception: + event, hint = event_from_exception( + exception, + client_options=client.options, + mechanism={"type": "bottle", "handled": False}, + ) + hub.capture_event(event, hint=hint) + raise exception + + return res + + return wrapped_callback + + Route._make_callback = patched_make_callback + + +class BottleRequestExtractor(RequestExtractor): + def env(self): + # type: () -> Dict[str, str] + return self.request.environ + + def cookies(self): + # type: () -> Dict[str, str] + return self.request.cookies + + def raw_data(self): + # type: () -> bytes + return self.request.body.read() + + def form(self): + # type: () -> FormsDict + if self.is_json(): + return None + return self.request.forms.decode() + + def files(self): + # type: () -> Optional[Dict[str, str]] + if self.is_json(): + return None + + return self.request.files + + def size_of_file(self, file): + # type: (FileUpload) -> int + return file.content_length + + +def _make_request_event_processor(app, request, integration): + # type: (Bottle, LocalRequest, BottleIntegration) -> EventProcessor + def inner(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + + try: + if integration.transaction_style == "endpoint": + event["transaction"] = request.route.name or transaction_from_function( + request.route.callback + ) + elif integration.transaction_style == "url": + event["transaction"] = request.route.rule + except Exception: + pass + + with capture_internal_exceptions(): + BottleRequestExtractor(request).extract_into_event(event) + + return event + + return inner diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/celery.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/celery.py new file mode 100644 index 0000000000..9b58796173 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/celery.py @@ -0,0 +1,258 @@ +from __future__ import absolute_import + +import functools +import sys + +from sentry_sdk.hub import Hub +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk.tracing import Span +from sentry_sdk._compat import reraise +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import TypeVar + from typing import Callable + from typing import Optional + + from sentry_sdk._types import EventProcessor, Event, Hint, ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + + +try: + from celery import VERSION as CELERY_VERSION # type: ignore + from celery.exceptions import ( # type: ignore + SoftTimeLimitExceeded, + Retry, + Ignore, + Reject, + ) +except ImportError: + raise DidNotEnable("Celery not installed") + + +CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) + + +class CeleryIntegration(Integration): + identifier = "celery" + + def __init__(self, propagate_traces=True): + # type: (bool) -> None + self.propagate_traces = propagate_traces + + @staticmethod + def setup_once(): + # type: () -> None + if CELERY_VERSION < (3,): + raise DidNotEnable("Celery 3 or newer required.") + + import celery.app.trace as trace # type: ignore + + old_build_tracer = trace.build_tracer + + def sentry_build_tracer(name, task, *args, **kwargs): + # type: (Any, Any, *Any, **Any) -> Any + if not getattr(task, "_sentry_is_patched", False): + # Need to patch both methods because older celery sometimes + # short-circuits to task.run if it thinks it's safe. + task.__call__ = _wrap_task_call(task, task.__call__) + task.run = _wrap_task_call(task, task.run) + task.apply_async = _wrap_apply_async(task, task.apply_async) + + # `build_tracer` is apparently called for every task + # invocation. Can't wrap every celery task for every invocation + # or we will get infinitely nested wrapper functions. + task._sentry_is_patched = True + + return _wrap_tracer(task, old_build_tracer(name, task, *args, **kwargs)) + + trace.build_tracer = sentry_build_tracer + + _patch_worker_exit() + + # This logger logs every status of every task that ran on the worker. + # Meaning that every task's breadcrumbs are full of stuff like "Task + # <foo> raised unexpected <bar>". + ignore_logger("celery.worker.job") + ignore_logger("celery.app.trace") + + # This is stdout/err redirected to a logger, can't deal with this + # (need event_level=logging.WARN to reproduce) + ignore_logger("celery.redirected") + + +def _wrap_apply_async(task, f): + # type: (Any, F) -> F + @functools.wraps(f) + def apply_async(*args, **kwargs): + # type: (*Any, **Any) -> Any + hub = Hub.current + integration = hub.get_integration(CeleryIntegration) + if integration is not None and integration.propagate_traces: + headers = None + for key, value in hub.iter_trace_propagation_headers(): + if headers is None: + headers = dict(kwargs.get("headers") or {}) + headers[key] = value + if headers is not None: + kwargs["headers"] = headers + + with hub.start_span(op="celery.submit", description=task.name): + return f(*args, **kwargs) + else: + return f(*args, **kwargs) + + return apply_async # type: ignore + + +def _wrap_tracer(task, f): + # type: (Any, F) -> F + + # Need to wrap tracer for pushing the scope before prerun is sent, and + # popping it after postrun is sent. + # + # This is the reason we don't use signals for hooking in the first place. + # Also because in Celery 3, signal dispatch returns early if one handler + # crashes. + @functools.wraps(f) + def _inner(*args, **kwargs): + # type: (*Any, **Any) -> Any + hub = Hub.current + if hub.get_integration(CeleryIntegration) is None: + return f(*args, **kwargs) + + with hub.push_scope() as scope: + scope._name = "celery" + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) + + span = Span.continue_from_headers(args[3].get("headers") or {}) + span.op = "celery.task" + span.transaction = "unknown celery task" + + # Could possibly use a better hook than this one + span.set_status("ok") + + with capture_internal_exceptions(): + # Celery task objects are not a thing to be trusted. Even + # something such as attribute access can fail. + span.transaction = task.name + + with hub.start_span(span): + return f(*args, **kwargs) + + return _inner # type: ignore + + +def _wrap_task_call(task, f): + # type: (Any, F) -> F + + # Need to wrap task call because the exception is caught before we get to + # see it. Also celery's reported stacktrace is untrustworthy. + + # functools.wraps is important here because celery-once looks at this + # method's name. + # https://github.com/getsentry/sentry-python/issues/421 + @functools.wraps(f) + def _inner(*args, **kwargs): + # type: (*Any, **Any) -> Any + try: + return f(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(task, exc_info) + reraise(*exc_info) + + return _inner # type: ignore + + +def _make_event_processor(task, uuid, args, kwargs, request=None): + # type: (Any, Any, Any, Any, Optional[Any]) -> EventProcessor + def event_processor(event, hint): + # type: (Event, Hint) -> Optional[Event] + + with capture_internal_exceptions(): + tags = event.setdefault("tags", {}) + tags["celery_task_id"] = uuid + extra = event.setdefault("extra", {}) + extra["celery-job"] = { + "task_name": task.name, + "args": args, + "kwargs": kwargs, + } + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded): + event["fingerprint"] = [ + "celery", + "SoftTimeLimitExceeded", + getattr(task, "name", task), + ] + + return event + + return event_processor + + +def _capture_exception(task, exc_info): + # type: (Any, ExcInfo) -> None + hub = Hub.current + + if hub.get_integration(CeleryIntegration) is None: + return + if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS): + # ??? Doesn't map to anything + _set_status(hub, "aborted") + return + + _set_status(hub, "internal_error") + + if hasattr(task, "throws") and isinstance(exc_info[1], task.throws): + return + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "celery", "handled": False}, + ) + + hub.capture_event(event, hint=hint) + + +def _set_status(hub, status): + # type: (Hub, str) -> None + with capture_internal_exceptions(): + with hub.configure_scope() as scope: + if scope.span is not None: + scope.span.set_status(status) + + +def _patch_worker_exit(): + # type: () -> None + + # Need to flush queue before worker shutdown because a crashing worker will + # call os._exit + from billiard.pool import Worker # type: ignore + + old_workloop = Worker.workloop + + def sentry_workloop(*args, **kwargs): + # type: (*Any, **Any) -> Any + try: + return old_workloop(*args, **kwargs) + finally: + with capture_internal_exceptions(): + hub = Hub.current + if hub.get_integration(CeleryIntegration) is not None: + hub.flush() + + Worker.workloop = sentry_workloop diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/dedupe.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/dedupe.py new file mode 100644 index 0000000000..b023df2042 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/dedupe.py @@ -0,0 +1,43 @@ +from sentry_sdk.hub import Hub +from sentry_sdk.utils import ContextVar +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class DedupeIntegration(Integration): + identifier = "dedupe" + + def __init__(self): + # type: () -> None + self._last_seen = ContextVar("last-seen") + + @staticmethod + def setup_once(): + # type: () -> None + @add_global_event_processor + def processor(event, hint): + # type: (Event, Optional[Hint]) -> Optional[Event] + if hint is None: + return event + + integration = Hub.current.get_integration(DedupeIntegration) + + if integration is None: + return event + + exc_info = hint.get("exc_info", None) + if exc_info is None: + return event + + exc = exc_info[1] + if integration._last_seen.get(None) is exc: + return None + integration._last_seen.set(exc) + return event diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/__init__.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/__init__.py new file mode 100644 index 0000000000..4e62fe3b74 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/__init__.py @@ -0,0 +1,484 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import sys +import threading +import weakref + +from sentry_sdk._types import MYPY +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.serializer import add_global_repr_processor +from sentry_sdk.tracing import record_sql_queries +from sentry_sdk.utils import ( + HAS_REAL_CONTEXTVARS, + logger, + capture_internal_exceptions, + event_from_exception, + transaction_from_function, + walk_exception_chain, +) +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.integrations._wsgi_common import RequestExtractor + +try: + from django import VERSION as DJANGO_VERSION + from django.core import signals + + try: + from django.urls import resolve + except ImportError: + from django.core.urlresolvers import resolve +except ImportError: + raise DidNotEnable("Django not installed") + + +from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER +from sentry_sdk.integrations.django.templates import get_template_frame_from_exception +from sentry_sdk.integrations.django.middleware import patch_django_middlewares + + +if MYPY: + from typing import Any + from typing import Callable + from typing import Dict + from typing import Optional + from typing import Union + from typing import List + + from django.core.handlers.wsgi import WSGIRequest + from django.http.response import HttpResponse + from django.http.request import QueryDict + from django.utils.datastructures import MultiValueDict + + from sentry_sdk.integrations.wsgi import _ScopedResponse + from sentry_sdk._types import Event, Hint, EventProcessor, NotImplementedType + + +if DJANGO_VERSION < (1, 10): + + def is_authenticated(request_user): + # type: (Any) -> bool + return request_user.is_authenticated() + + +else: + + def is_authenticated(request_user): + # type: (Any) -> bool + return request_user.is_authenticated + + +TRANSACTION_STYLE_VALUES = ("function_name", "url") + + +class DjangoIntegration(Integration): + identifier = "django" + + transaction_style = None + middleware_spans = None + + def __init__(self, transaction_style="url", middleware_spans=True): + # type: (str, bool) -> None + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.middleware_spans = middleware_spans + + @staticmethod + def setup_once(): + # type: () -> None + + if DJANGO_VERSION < (1, 6): + raise DidNotEnable("Django 1.6 or newer is required.") + + install_sql_hook() + # Patch in our custom middleware. + + # logs an error for every 500 + ignore_logger("django.server") + ignore_logger("django.request") + + from django.core.handlers.wsgi import WSGIHandler + + old_app = WSGIHandler.__call__ + + def sentry_patched_wsgi_handler(self, environ, start_response): + # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse + if Hub.current.get_integration(DjangoIntegration) is None: + return old_app(self, environ, start_response) + + bound_old_app = old_app.__get__(self, WSGIHandler) + + return SentryWsgiMiddleware(bound_old_app)(environ, start_response) + + WSGIHandler.__call__ = sentry_patched_wsgi_handler + + _patch_django_asgi_handler() + + # patch get_response, because at that point we have the Django request + # object + from django.core.handlers.base import BaseHandler + + old_get_response = BaseHandler.get_response + + def sentry_patched_get_response(self, request): + # type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException] + hub = Hub.current + integration = hub.get_integration(DjangoIntegration) + if integration is not None: + _patch_drf() + + with hub.configure_scope() as scope: + # Rely on WSGI middleware to start a trace + try: + if integration.transaction_style == "function_name": + scope.transaction = transaction_from_function( + resolve(request.path).func + ) + elif integration.transaction_style == "url": + scope.transaction = LEGACY_RESOLVER.resolve(request.path) + except Exception: + pass + + scope.add_event_processor( + _make_event_processor(weakref.ref(request), integration) + ) + return old_get_response(self, request) + + BaseHandler.get_response = sentry_patched_get_response + + signals.got_request_exception.connect(_got_request_exception) + + @add_global_event_processor + def process_django_templates(event, hint): + # type: (Event, Optional[Hint]) -> Optional[Event] + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_, exc_value, _) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + frame = get_template_frame_from_exception(exc_value) + if frame is not None: + frames = exception.get("stacktrace", {}).get("frames", []) + + for i in reversed(range(len(frames))): + f = frames[i] + if ( + f.get("function") in ("parse", "render") + and f.get("module") == "django.template.base" + ): + i += 1 + break + else: + i = len(frames) + + frames.insert(i, frame) + + return event + + @add_global_repr_processor + def _django_queryset_repr(value, hint): + # type: (Any, Dict[str, Any]) -> Union[NotImplementedType, str] + try: + # Django 1.6 can fail to import `QuerySet` when Django settings + # have not yet been initialized. + # + # If we fail to import, return `NotImplemented`. It's at least + # unlikely that we have a query set in `value` when importing + # `QuerySet` fails. + from django.db.models.query import QuerySet + except Exception: + return NotImplemented + + if not isinstance(value, QuerySet) or value._result_cache: + return NotImplemented + + # Do not call Hub.get_integration here. It is intentional that + # running under a new hub does not suddenly start executing + # querysets. This might be surprising to the user but it's likely + # less annoying. + + return u"<%s from %s at 0x%x>" % ( + value.__class__.__name__, + value.__module__, + id(value), + ) + + _patch_channels() + patch_django_middlewares() + + +_DRF_PATCHED = False +_DRF_PATCH_LOCK = threading.Lock() + + +def _patch_drf(): + # type: () -> None + """ + Patch Django Rest Framework for more/better request data. DRF's request + type is a wrapper around Django's request type. The attribute we're + interested in is `request.data`, which is a cached property containing a + parsed request body. Reading a request body from that property is more + reliable than reading from any of Django's own properties, as those don't + hold payloads in memory and therefore can only be accessed once. + + We patch the Django request object to include a weak backreference to the + DRF request object, such that we can later use either in + `DjangoRequestExtractor`. + + This function is not called directly on SDK setup, because importing almost + any part of Django Rest Framework will try to access Django settings (where + `sentry_sdk.init()` might be called from in the first place). Instead we + run this function on every request and do the patching on the first + request. + """ + + global _DRF_PATCHED + + if _DRF_PATCHED: + # Double-checked locking + return + + with _DRF_PATCH_LOCK: + if _DRF_PATCHED: + return + + # We set this regardless of whether the code below succeeds or fails. + # There is no point in trying to patch again on the next request. + _DRF_PATCHED = True + + with capture_internal_exceptions(): + try: + from rest_framework.views import APIView # type: ignore + except ImportError: + pass + else: + old_drf_initial = APIView.initial + + def sentry_patched_drf_initial(self, request, *args, **kwargs): + # type: (APIView, Any, *Any, **Any) -> Any + with capture_internal_exceptions(): + request._request._sentry_drf_request_backref = weakref.ref( + request + ) + pass + return old_drf_initial(self, request, *args, **kwargs) + + APIView.initial = sentry_patched_drf_initial + + +def _patch_channels(): + # type: () -> None + try: + from channels.http import AsgiHandler # type: ignore + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because channels may not be used at all in + # the current process. + logger.warning( + "We detected that you are using Django channels 2.0. To get proper " + "instrumentation for ASGI requests, the Sentry SDK requires " + "Python 3.7+ or the aiocontextvars package from PyPI." + ) + + from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl + + patch_channels_asgi_handler_impl(AsgiHandler) + + +def _patch_django_asgi_handler(): + # type: () -> None + try: + from django.core.handlers.asgi import ASGIHandler + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because Django may not be used at all in + # the current process. + logger.warning( + "We detected that you are using Django 3. To get proper " + "instrumentation for ASGI requests, the Sentry SDK requires " + "Python 3.7+ or the aiocontextvars package from PyPI." + ) + + from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl + + patch_django_asgi_handler_impl(ASGIHandler) + + +def _make_event_processor(weak_request, integration): + # type: (Callable[[], WSGIRequest], DjangoIntegration) -> EventProcessor + def event_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + request = weak_request() + if request is None: + return event + + try: + drf_request = request._sentry_drf_request_backref() + if drf_request is not None: + request = drf_request + except AttributeError: + pass + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if _should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return event_processor + + +def _got_request_exception(request=None, **kwargs): + # type: (WSGIRequest, **Any) -> None + hub = Hub.current + integration = hub.get_integration(DjangoIntegration) + if integration is not None: + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + event, hint = event_from_exception( + sys.exc_info(), + client_options=client.options, + mechanism={"type": "django", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + +class DjangoRequestExtractor(RequestExtractor): + def env(self): + # type: () -> Dict[str, str] + return self.request.META + + def cookies(self): + # type: () -> Dict[str, str] + return self.request.COOKIES + + def raw_data(self): + # type: () -> bytes + return self.request.body + + def form(self): + # type: () -> QueryDict + return self.request.POST + + def files(self): + # type: () -> MultiValueDict + return self.request.FILES + + def size_of_file(self, file): + # type: (Any) -> int + return file.size + + def parsed_body(self): + # type: () -> Optional[Dict[str, Any]] + try: + return self.request.data + except AttributeError: + return RequestExtractor.parsed_body(self) + + +def _set_user_info(request, event): + # type: (WSGIRequest, Dict[str, Any]) -> None + user_info = event.setdefault("user", {}) + + user = getattr(request, "user", None) + + if user is None or not is_authenticated(user): + return + + try: + user_info.setdefault("id", str(user.pk)) + except Exception: + pass + + try: + user_info.setdefault("email", user.email) + except Exception: + pass + + try: + user_info.setdefault("username", user.get_username()) + except Exception: + pass + + +def install_sql_hook(): + # type: () -> None + """If installed this causes Django's queries to be captured.""" + try: + from django.db.backends.utils import CursorWrapper + except ImportError: + from django.db.backends.util import CursorWrapper + + try: + real_execute = CursorWrapper.execute + real_executemany = CursorWrapper.executemany + except AttributeError: + # This won't work on Django versions < 1.6 + return + + def execute(self, sql, params=None): + # type: (CursorWrapper, Any, Optional[Any]) -> Any + hub = Hub.current + if hub.get_integration(DjangoIntegration) is None: + return real_execute(self, sql, params) + + with record_sql_queries( + hub, self.cursor, sql, params, paramstyle="format", executemany=False + ): + return real_execute(self, sql, params) + + def executemany(self, sql, param_list): + # type: (CursorWrapper, Any, List[Any]) -> Any + hub = Hub.current + if hub.get_integration(DjangoIntegration) is None: + return real_executemany(self, sql, param_list) + + with record_sql_queries( + hub, self.cursor, sql, param_list, paramstyle="format", executemany=True + ): + return real_executemany(self, sql, param_list) + + CursorWrapper.execute = execute + CursorWrapper.executemany = executemany + ignore_logger("django.db.backends") diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/asgi.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/asgi.py new file mode 100644 index 0000000000..96ae3e0809 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/asgi.py @@ -0,0 +1,47 @@ +""" +Instrumentation for Django 3.0 + +Since this file contains `async def` it is conditionally imported in +`sentry_sdk.integrations.django` (depending on the existence of +`django.core.handlers.asgi`. +""" + +from sentry_sdk import Hub +from sentry_sdk._types import MYPY + +from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware + +if MYPY: + from typing import Any + + +def patch_django_asgi_handler_impl(cls): + # type: (Any) -> None + old_app = cls.__call__ + + async def sentry_patched_asgi_handler(self, scope, receive, send): + # type: (Any, Any, Any, Any) -> Any + if Hub.current.get_integration(DjangoIntegration) is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware(old_app.__get__(self, cls))._run_asgi3 + return await middleware(scope, receive, send) + + cls.__call__ = sentry_patched_asgi_handler + + +def patch_channels_asgi_handler_impl(cls): + # type: (Any) -> None + old_app = cls.__call__ + + async def sentry_patched_asgi_handler(self, receive, send): + # type: (Any, Any, Any) -> Any + if Hub.current.get_integration(DjangoIntegration) is None: + return await old_app(self, receive, send) + + middleware = SentryAsgiMiddleware(lambda _scope: old_app.__get__(self, cls)) + + return await middleware(self.scope)(receive, send) + + cls.__call__ = sentry_patched_asgi_handler diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/middleware.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/middleware.py new file mode 100644 index 0000000000..edbeccb093 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/middleware.py @@ -0,0 +1,136 @@ +""" +Create spans from Django middleware invocations +""" + +from functools import wraps + +from django import VERSION as DJANGO_VERSION + +from sentry_sdk import Hub +from sentry_sdk.utils import ( + ContextVar, + transaction_from_function, + capture_internal_exceptions, +) + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Callable + from typing import TypeVar + + F = TypeVar("F", bound=Callable[..., Any]) + +_import_string_should_wrap_middleware = ContextVar( + "import_string_should_wrap_middleware" +) + +if DJANGO_VERSION < (1, 7): + import_string_name = "import_by_path" +else: + import_string_name = "import_string" + + +def patch_django_middlewares(): + # type: () -> None + from django.core.handlers import base + + old_import_string = getattr(base, import_string_name) + + def sentry_patched_import_string(dotted_path): + # type: (str) -> Any + rv = old_import_string(dotted_path) + + if _import_string_should_wrap_middleware.get(None): + rv = _wrap_middleware(rv, dotted_path) + + return rv + + setattr(base, import_string_name, sentry_patched_import_string) + + old_load_middleware = base.BaseHandler.load_middleware + + def sentry_patched_load_middleware(self): + # type: (base.BaseHandler) -> Any + _import_string_should_wrap_middleware.set(True) + try: + return old_load_middleware(self) + finally: + _import_string_should_wrap_middleware.set(False) + + base.BaseHandler.load_middleware = sentry_patched_load_middleware + + +def _wrap_middleware(middleware, middleware_name): + # type: (Any, str) -> Any + from sentry_sdk.integrations.django import DjangoIntegration + + def _get_wrapped_method(old_method): + # type: (F) -> F + with capture_internal_exceptions(): + + def sentry_wrapped_method(*args, **kwargs): + # type: (*Any, **Any) -> Any + hub = Hub.current + integration = hub.get_integration(DjangoIntegration) + if integration is None or not integration.middleware_spans: + return old_method(*args, **kwargs) + + function_name = transaction_from_function(old_method) + + description = middleware_name + function_basename = getattr(old_method, "__name__", None) + if function_basename: + description = "{}.{}".format(description, function_basename) + + with hub.start_span( + op="django.middleware", description=description + ) as span: + span.set_tag("django.function_name", function_name) + span.set_tag("django.middleware_name", middleware_name) + return old_method(*args, **kwargs) + + try: + # fails for __call__ of function on Python 2 (see py2.7-django-1.11) + return wraps(old_method)(sentry_wrapped_method) # type: ignore + except Exception: + return sentry_wrapped_method # type: ignore + + return old_method + + class SentryWrappingMiddleware(object): + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + self._inner = middleware(*args, **kwargs) + self._call_method = None + + # We need correct behavior for `hasattr()`, which we can only determine + # when we have an instance of the middleware we're wrapping. + def __getattr__(self, method_name): + # type: (str) -> Any + if method_name not in ( + "process_request", + "process_view", + "process_template_response", + "process_response", + "process_exception", + ): + raise AttributeError() + + old_method = getattr(self._inner, method_name) + rv = _get_wrapped_method(old_method) + self.__dict__[method_name] = rv + return rv + + def __call__(self, *args, **kwargs): + # type: (*Any, **Any) -> Any + f = self._call_method + if f is None: + self._call_method = f = _get_wrapped_method(self._inner.__call__) + return f(*args, **kwargs) + + if hasattr(middleware, "__name__"): + SentryWrappingMiddleware.__name__ = middleware.__name__ + + return SentryWrappingMiddleware diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/templates.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/templates.py new file mode 100644 index 0000000000..2285644909 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/templates.py @@ -0,0 +1,121 @@ +from django.template import TemplateSyntaxError + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Dict + from typing import Optional + from typing import Iterator + from typing import Tuple + +try: + # support Django 1.9 + from django.template.base import Origin +except ImportError: + # backward compatibility + from django.template.loader import LoaderOrigin as Origin + + +def get_template_frame_from_exception(exc_value): + # type: (Optional[BaseException]) -> Optional[Dict[str, Any]] + + # As of Django 1.9 or so the new template debug thing showed up. + if hasattr(exc_value, "template_debug"): + return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore + + # As of r16833 (Django) all exceptions may contain a + # ``django_template_source`` attribute (rather than the legacy + # ``TemplateSyntaxError.source`` check) + if hasattr(exc_value, "django_template_source"): + return _get_template_frame_from_source( + exc_value.django_template_source # type: ignore + ) + + if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): + source = exc_value.source + if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): + return _get_template_frame_from_source(source) # type: ignore + + return None + + +def _get_template_frame_from_debug(debug): + # type: (Dict[str, Any]) -> Dict[str, Any] + if debug is None: + return None + + lineno = debug["line"] + filename = debug["name"] + if filename is None: + filename = "<django template>" + + pre_context = [] + post_context = [] + context_line = None + + for i, line in debug["source_lines"]: + if i < lineno: + pre_context.append(line) + elif i > lineno: + post_context.append(line) + else: + context_line = line + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + "in_app": True, + } + + +def _linebreak_iter(template_source): + # type: (str) -> Iterator[int] + yield 0 + p = template_source.find("\n") + while p >= 0: + yield p + 1 + p = template_source.find("\n", p + 1) + + +def _get_template_frame_from_source(source): + # type: (Tuple[Origin, Tuple[int, int]]) -> Optional[Dict[str, Any]] + if not source: + return None + + origin, (start, end) = source + filename = getattr(origin, "loadname", None) + if filename is None: + filename = "<django template>" + template_source = origin.reload() + lineno = None + upto = 0 + pre_context = [] + post_context = [] + context_line = None + + for num, next in enumerate(_linebreak_iter(template_source)): + line = template_source[upto:next] + if start >= upto and end <= next: + lineno = num + context_line = line + elif lineno is None: + pre_context.append(line) + else: + post_context.append(line) + + upto = next + + if context_line is None or lineno is None: + return None + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + } diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/transactions.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/transactions.py new file mode 100644 index 0000000000..f20866ef95 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/transactions.py @@ -0,0 +1,134 @@ +""" +Copied from raven-python. Used for +`DjangoIntegration(transaction_fron="raven_legacy")`. +""" + +from __future__ import absolute_import + +import re + +from sentry_sdk._types import MYPY + +if MYPY: + from django.urls.resolvers import URLResolver + from typing import Dict + from typing import List + from typing import Optional + from django.urls.resolvers import URLPattern + from typing import Tuple + from typing import Union + from re import Pattern + +try: + from django.urls import get_resolver +except ImportError: + from django.core.urlresolvers import get_resolver + + +def get_regex(resolver_or_pattern): + # type: (Union[URLPattern, URLResolver]) -> Pattern[str] + """Utility method for django's deprecated resolver.regex""" + try: + regex = resolver_or_pattern.regex + except AttributeError: + regex = resolver_or_pattern.pattern.regex + return regex + + +class RavenResolver(object): + _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)") + _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)") + _non_named_group_matcher = re.compile(r"\([^\)]+\)") + # [foo|bar|baz] + _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") + _camel_re = re.compile(r"([A-Z]+)([a-z])") + + _cache = {} # type: Dict[URLPattern, str] + + def _simplify(self, pattern): + # type: (str) -> str + r""" + Clean up urlpattern regexes into something readable by humans: + + From: + > "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$" + + To: + > "{sport_slug}/athletes/{athlete_slug}/" + """ + # remove optional params + # TODO(dcramer): it'd be nice to change these into [%s] but it currently + # conflicts with the other rules because we're doing regexp matches + # rather than parsing tokens + result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), pattern) + + # handle named groups first + result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result) + + # handle non-named groups + result = self._non_named_group_matcher.sub("{var}", result) + + # handle optional params + result = self._either_option_matcher.sub(lambda m: m.group(1), result) + + # clean up any outstanding regex-y characters. + result = ( + result.replace("^", "") + .replace("$", "") + .replace("?", "") + .replace("//", "/") + .replace("\\", "") + ) + + return result + + def _resolve(self, resolver, path, parents=None): + # type: (URLResolver, str, Optional[List[URLResolver]]) -> Optional[str] + + match = get_regex(resolver).search(path) # Django < 2.0 + + if not match: + return None + + if parents is None: + parents = [resolver] + elif resolver not in parents: + parents = parents + [resolver] + + new_path = path[match.end() :] + for pattern in resolver.url_patterns: + # this is an include() + if not pattern.callback: + match_ = self._resolve(pattern, new_path, parents) + if match_: + return match_ + continue + elif not get_regex(pattern).search(new_path): + continue + + try: + return self._cache[pattern] + except KeyError: + pass + + prefix = "".join(self._simplify(get_regex(p).pattern) for p in parents) + result = prefix + self._simplify(get_regex(pattern).pattern) + if not result.startswith("/"): + result = "/" + result + self._cache[pattern] = result + return result + + return None + + def resolve( + self, + path, # type: str + urlconf=None, # type: Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]] + ): + # type: (...) -> str + resolver = get_resolver(urlconf) + match = self._resolve(resolver, path) + return match or path + + +LEGACY_RESOLVER = RavenResolver() diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/excepthook.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/excepthook.py new file mode 100644 index 0000000000..d8aead097a --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/excepthook.py @@ -0,0 +1,76 @@ +import sys + +from sentry_sdk.hub import Hub +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk.integrations import Integration + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Callable + from typing import Any + from typing import Type + + from types import TracebackType + + Excepthook = Callable[ + [Type[BaseException], BaseException, TracebackType], Any, + ] + + +class ExcepthookIntegration(Integration): + identifier = "excepthook" + + always_run = False + + def __init__(self, always_run=False): + # type: (bool) -> None + + if not isinstance(always_run, bool): + raise ValueError( + "Invalid value for always_run: %s (must be type boolean)" + % (always_run,) + ) + self.always_run = always_run + + @staticmethod + def setup_once(): + # type: () -> None + sys.excepthook = _make_excepthook(sys.excepthook) + + +def _make_excepthook(old_excepthook): + # type: (Excepthook) -> Excepthook + def sentry_sdk_excepthook(type_, value, traceback): + # type: (Type[BaseException], BaseException, TracebackType) -> None + hub = Hub.current + integration = hub.get_integration(ExcepthookIntegration) + + if integration is not None and _should_send(integration.always_run): + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + with capture_internal_exceptions(): + event, hint = event_from_exception( + (type_, value, traceback), + client_options=client.options, + mechanism={"type": "excepthook", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + return old_excepthook(type_, value, traceback) + + return sentry_sdk_excepthook + + +def _should_send(always_run=False): + # type: (bool) -> bool + if always_run: + return True + + if hasattr(sys, "ps1"): + # Disable the excepthook for interactive Python shells, otherwise + # every typo gets sent to Sentry. + return False + + return True diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/falcon.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/falcon.py new file mode 100644 index 0000000000..b24aac41c6 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/falcon.py @@ -0,0 +1,209 @@ +from __future__ import absolute_import + +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Dict + from typing import Optional + + from sentry_sdk._types import EventProcessor + +try: + import falcon # type: ignore + import falcon.api_helpers # type: ignore + + from falcon import __version__ as FALCON_VERSION +except ImportError: + raise DidNotEnable("Falcon not installed") + + +class FalconRequestExtractor(RequestExtractor): + def env(self): + # type: () -> Dict[str, Any] + return self.request.env + + def cookies(self): + # type: () -> Dict[str, Any] + return self.request.cookies + + def form(self): + # type: () -> None + return None # No such concept in Falcon + + def files(self): + # type: () -> None + return None # No such concept in Falcon + + def raw_data(self): + # type: () -> Optional[str] + + # As request data can only be read once we won't make this available + # to Sentry. Just send back a dummy string in case there was a + # content length. + # TODO(jmagnusson): Figure out if there's a way to support this + content_length = self.content_length() + if content_length > 0: + return "[REQUEST_CONTAINING_RAW_DATA]" + else: + return None + + def json(self): + # type: () -> Optional[Dict[str, Any]] + try: + return self.request.media + except falcon.errors.HTTPBadRequest: + # NOTE(jmagnusson): We return `falcon.Request._media` here because + # falcon 1.4 doesn't do proper type checking in + # `falcon.Request.media`. This has been fixed in 2.0. + # Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953 + return self.request._media + + +class SentryFalconMiddleware(object): + """Captures exceptions in Falcon requests and send to Sentry""" + + def process_request(self, req, resp, *args, **kwargs): + # type: (Any, Any, *Any, **Any) -> None + hub = Hub.current + integration = hub.get_integration(FalconIntegration) + if integration is None: + return + + with hub.configure_scope() as scope: + scope._name = "falcon" + scope.add_event_processor(_make_request_event_processor(req, integration)) + + +TRANSACTION_STYLE_VALUES = ("uri_template", "path") + + +class FalconIntegration(Integration): + identifier = "falcon" + + transaction_style = None + + def __init__(self, transaction_style="uri_template"): + # type: (str) -> None + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once(): + # type: () -> None + try: + version = tuple(map(int, FALCON_VERSION.split("."))) + except (ValueError, TypeError): + raise DidNotEnable("Unparseable Falcon version: {}".format(FALCON_VERSION)) + + if version < (1, 4): + raise DidNotEnable("Falcon 1.4 or newer required.") + + _patch_wsgi_app() + _patch_handle_exception() + _patch_prepare_middleware() + + +def _patch_wsgi_app(): + # type: () -> None + original_wsgi_app = falcon.API.__call__ + + def sentry_patched_wsgi_app(self, env, start_response): + # type: (falcon.API, Any, Any) -> Any + hub = Hub.current + integration = hub.get_integration(FalconIntegration) + if integration is None: + return original_wsgi_app(self, env, start_response) + + sentry_wrapped = SentryWsgiMiddleware( + lambda envi, start_resp: original_wsgi_app(self, envi, start_resp) + ) + + return sentry_wrapped(env, start_response) + + falcon.API.__call__ = sentry_patched_wsgi_app + + +def _patch_handle_exception(): + # type: () -> None + original_handle_exception = falcon.API._handle_exception + + def sentry_patched_handle_exception(self, *args): + # type: (falcon.API, *Any) -> Any + # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception + # method signature from `(ex, req, resp, params)` to + # `(req, resp, ex, params)` + if isinstance(args[0], Exception): + ex = args[0] + else: + ex = args[2] + + was_handled = original_handle_exception(self, *args) + + hub = Hub.current + integration = hub.get_integration(FalconIntegration) + + if integration is not None and not _is_falcon_http_error(ex): + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + event, hint = event_from_exception( + ex, + client_options=client.options, + mechanism={"type": "falcon", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + return was_handled + + falcon.API._handle_exception = sentry_patched_handle_exception + + +def _patch_prepare_middleware(): + # type: () -> None + original_prepare_middleware = falcon.api_helpers.prepare_middleware + + def sentry_patched_prepare_middleware( + middleware=None, independent_middleware=False + ): + # type: (Any, Any) -> Any + hub = Hub.current + integration = hub.get_integration(FalconIntegration) + if integration is not None: + middleware = [SentryFalconMiddleware()] + (middleware or []) + return original_prepare_middleware(middleware, independent_middleware) + + falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware + + +def _is_falcon_http_error(ex): + # type: (BaseException) -> bool + return isinstance(ex, (falcon.HTTPError, falcon.http_status.HTTPStatus)) + + +def _make_request_event_processor(req, integration): + # type: (falcon.Request, FalconIntegration) -> EventProcessor + + def inner(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + if integration.transaction_style == "uri_template": + event["transaction"] = req.uri_template + elif integration.transaction_style == "path": + event["transaction"] = req.path + + with capture_internal_exceptions(): + FalconRequestExtractor(req).extract_into_event(event) + + return event + + return inner diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/flask.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/flask.py new file mode 100644 index 0000000000..ef6ae0e4f0 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/flask.py @@ -0,0 +1,260 @@ +from __future__ import absolute_import + +import weakref + +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.integrations._wsgi_common import RequestExtractor + +from sentry_sdk._types import MYPY + +if MYPY: + from sentry_sdk.integrations.wsgi import _ScopedResponse + from typing import Any + from typing import Dict + from werkzeug.datastructures import ImmutableTypeConversionDict + from werkzeug.datastructures import ImmutableMultiDict + from werkzeug.datastructures import FileStorage + from typing import Union + from typing import Callable + + from sentry_sdk._types import EventProcessor + + +try: + import flask_login # type: ignore +except ImportError: + flask_login = None + +try: + from flask import ( # type: ignore + Request, + Flask, + _request_ctx_stack, + _app_ctx_stack, + __version__ as FLASK_VERSION, + ) + from flask.signals import ( + appcontext_pushed, + appcontext_tearing_down, + got_request_exception, + request_started, + ) +except ImportError: + raise DidNotEnable("Flask is not installed") + + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class FlaskIntegration(Integration): + identifier = "flask" + + transaction_style = None + + def __init__(self, transaction_style="endpoint"): + # type: (str) -> None + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once(): + # type: () -> None + try: + version = tuple(map(int, FLASK_VERSION.split(".")[:3])) + except (ValueError, TypeError): + raise DidNotEnable("Unparseable Flask version: {}".format(FLASK_VERSION)) + + if version < (0, 11): + raise DidNotEnable("Flask 0.11 or newer is required.") + + appcontext_pushed.connect(_push_appctx) + appcontext_tearing_down.connect(_pop_appctx) + request_started.connect(_request_started) + got_request_exception.connect(_capture_exception) + + old_app = Flask.__call__ + + def sentry_patched_wsgi_app(self, environ, start_response): + # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse + if Hub.current.get_integration(FlaskIntegration) is None: + return old_app(self, environ, start_response) + + return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))( + environ, start_response + ) + + Flask.__call__ = sentry_patched_wsgi_app # type: ignore + + +def _push_appctx(*args, **kwargs): + # type: (*Flask, **Any) -> None + hub = Hub.current + if hub.get_integration(FlaskIntegration) is not None: + # always want to push scope regardless of whether WSGI app might already + # have (not the case for CLI for example) + scope_manager = hub.push_scope() + scope_manager.__enter__() + _app_ctx_stack.top.sentry_sdk_scope_manager = scope_manager + with hub.configure_scope() as scope: + scope._name = "flask" + + +def _pop_appctx(*args, **kwargs): + # type: (*Flask, **Any) -> None + scope_manager = getattr(_app_ctx_stack.top, "sentry_sdk_scope_manager", None) + if scope_manager is not None: + scope_manager.__exit__(None, None, None) + + +def _request_started(sender, **kwargs): + # type: (Flask, **Any) -> None + hub = Hub.current + integration = hub.get_integration(FlaskIntegration) + if integration is None: + return + + app = _app_ctx_stack.top.app + with hub.configure_scope() as scope: + request = _request_ctx_stack.top.request + + # Rely on WSGI middleware to start a trace + try: + if integration.transaction_style == "endpoint": + scope.transaction = request.url_rule.endpoint + elif integration.transaction_style == "url": + scope.transaction = request.url_rule.rule + except Exception: + pass + + weak_request = weakref.ref(request) + evt_processor = _make_request_event_processor( + app, weak_request, integration # type: ignore + ) + scope.add_event_processor(evt_processor) + + +class FlaskRequestExtractor(RequestExtractor): + def env(self): + # type: () -> Dict[str, str] + return self.request.environ + + def cookies(self): + # type: () -> ImmutableTypeConversionDict[Any, Any] + return self.request.cookies + + def raw_data(self): + # type: () -> bytes + return self.request.get_data() + + def form(self): + # type: () -> ImmutableMultiDict[str, Any] + return self.request.form + + def files(self): + # type: () -> ImmutableMultiDict[str, Any] + return self.request.files + + def is_json(self): + # type: () -> bool + return self.request.is_json + + def json(self): + # type: () -> Any + return self.request.get_json() + + def size_of_file(self, file): + # type: (FileStorage) -> int + return file.content_length + + +def _make_request_event_processor(app, weak_request, integration): + # type: (Flask, Callable[[], Request], FlaskIntegration) -> EventProcessor + def inner(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + request = weak_request() + + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + FlaskRequestExtractor(request).extract_into_event(event) + + if _should_send_default_pii(): + with capture_internal_exceptions(): + _add_user_to_event(event) + + return event + + return inner + + +def _capture_exception(sender, exception, **kwargs): + # type: (Flask, Union[ValueError, BaseException], **Any) -> None + hub = Hub.current + if hub.get_integration(FlaskIntegration) is None: + return + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + event, hint = event_from_exception( + exception, + client_options=client.options, + mechanism={"type": "flask", "handled": False}, + ) + + hub.capture_event(event, hint=hint) + + +def _add_user_to_event(event): + # type: (Dict[str, Any]) -> None + if flask_login is None: + return + + user = flask_login.current_user + if user is None: + return + + with capture_internal_exceptions(): + # Access this object as late as possible as accessing the user + # is relatively costly + + user_info = event.setdefault("user", {}) + + try: + user_info.setdefault("id", user.get_id()) + # TODO: more configurable user attrs here + except AttributeError: + # might happen if: + # - flask_login could not be imported + # - flask_login is not configured + # - no user is logged in + pass + + # The following attribute accesses are ineffective for the general + # Flask-Login case, because the User interface of Flask-Login does not + # care about anything but the ID. However, Flask-User (based on + # Flask-Login) documents a few optional extra attributes. + # + # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names + + try: + user_info.setdefault("email", user.email) + except Exception: + pass + + try: + user_info.setdefault("username", user.username) + user_info.setdefault("username", user.email) + except Exception: + pass diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/gnu_backtrace.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/gnu_backtrace.py new file mode 100644 index 0000000000..e0ec110547 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/gnu_backtrace.py @@ -0,0 +1,107 @@ +import re + +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import capture_internal_exceptions + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Dict + + +MODULE_RE = r"[a-zA-Z0-9/._:\\-]+" +TYPE_RE = r"[a-zA-Z0-9._:<>,-]+" +HEXVAL_RE = r"[A-Fa-f0-9]+" + + +FRAME_RE = r""" +^(?P<index>\d+)\.\s +(?P<package>{MODULE_RE})\( + (?P<retval>{TYPE_RE}\ )? + ((?P<function>{TYPE_RE}) + (?P<args>\(.*\))? + )? + ((?P<constoffset>\ const)?\+0x(?P<offset>{HEXVAL_RE}))? +\)\s +\[0x(?P<retaddr>{HEXVAL_RE})\]$ +""".format( + MODULE_RE=MODULE_RE, HEXVAL_RE=HEXVAL_RE, TYPE_RE=TYPE_RE +) + +FRAME_RE = re.compile(FRAME_RE, re.MULTILINE | re.VERBOSE) + + +class GnuBacktraceIntegration(Integration): + identifier = "gnu_backtrace" + + @staticmethod + def setup_once(): + # type: () -> None + @add_global_event_processor + def process_gnu_backtrace(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + with capture_internal_exceptions(): + return _process_gnu_backtrace(event, hint) + + +def _process_gnu_backtrace(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + if Hub.current.get_integration(GnuBacktraceIntegration) is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception in values: + frames = exception.get("stacktrace", {}).get("frames", []) + if not frames: + continue + + msg = exception.get("value", None) + if not msg: + continue + + additional_frames = [] + new_msg = [] + + for line in msg.splitlines(): + match = FRAME_RE.match(line) + if match: + additional_frames.append( + ( + int(match.group("index")), + { + "package": match.group("package") or None, + "function": match.group("function") or None, + "platform": "native", + }, + ) + ) + else: + # Put garbage lines back into message, not sure what to do with them. + new_msg.append(line) + + if additional_frames: + additional_frames.sort(key=lambda x: -x[0]) + for _, frame in additional_frames: + frames.append(frame) + + new_msg.append("<stacktrace parsed and removed by GnuBacktraceIntegration>") + exception["value"] = "\n".join(new_msg) + + return event diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/logging.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/logging.py new file mode 100644 index 0000000000..6edd785e91 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/logging.py @@ -0,0 +1,237 @@ +from __future__ import absolute_import + +import logging +import datetime + +from sentry_sdk.hub import Hub +from sentry_sdk.utils import ( + to_string, + event_from_exception, + current_stacktrace, + capture_internal_exceptions, +) +from sentry_sdk.integrations import Integration +from sentry_sdk._compat import iteritems + +from sentry_sdk._types import MYPY + +if MYPY: + from logging import LogRecord + from typing import Any + from typing import Dict + from typing import Optional + +DEFAULT_LEVEL = logging.INFO +DEFAULT_EVENT_LEVEL = logging.ERROR + +_IGNORED_LOGGERS = set(["sentry_sdk.errors"]) + + +def ignore_logger( + name, # type: str +): + # type: (...) -> None + """This disables recording (both in breadcrumbs and as events) calls to + a logger of a specific name. Among other uses, many of our integrations + use this to prevent their actions being recorded as breadcrumbs. Exposed + to users as a way to quiet spammy loggers. + + :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). + """ + _IGNORED_LOGGERS.add(name) + + +class LoggingIntegration(Integration): + identifier = "logging" + + def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL): + # type: (Optional[int], Optional[int]) -> None + self._handler = None + self._breadcrumb_handler = None + + if level is not None: + self._breadcrumb_handler = BreadcrumbHandler(level=level) + + if event_level is not None: + self._handler = EventHandler(level=event_level) + + def _handle_record(self, record): + # type: (LogRecord) -> None + if self._handler is not None and record.levelno >= self._handler.level: + self._handler.handle(record) + + if ( + self._breadcrumb_handler is not None + and record.levelno >= self._breadcrumb_handler.level + ): + self._breadcrumb_handler.handle(record) + + @staticmethod + def setup_once(): + # type: () -> None + old_callhandlers = logging.Logger.callHandlers # type: ignore + + def sentry_patched_callhandlers(self, record): + # type: (Any, LogRecord) -> Any + try: + return old_callhandlers(self, record) + finally: + # This check is done twice, once also here before we even get + # the integration. Otherwise we have a high chance of getting + # into a recursion error when the integration is resolved + # (this also is slower). + if record.name not in _IGNORED_LOGGERS: + integration = Hub.current.get_integration(LoggingIntegration) + if integration is not None: + integration._handle_record(record) + + logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore + + +def _can_record(record): + # type: (LogRecord) -> bool + return record.name not in _IGNORED_LOGGERS + + +def _breadcrumb_from_record(record): + # type: (LogRecord) -> Dict[str, Any] + return { + "ty": "log", + "level": _logging_to_event_level(record.levelname), + "category": record.name, + "message": record.message, + "timestamp": datetime.datetime.utcfromtimestamp(record.created), + "data": _extra_from_record(record), + } + + +def _logging_to_event_level(levelname): + # type: (str) -> str + return {"critical": "fatal"}.get(levelname.lower(), levelname.lower()) + + +COMMON_RECORD_ATTRS = frozenset( + ( + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "levelname", + "levelno", + "linenno", + "lineno", + "message", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack", + "tags", + "thread", + "threadName", + "stack_info", + ) +) + + +def _extra_from_record(record): + # type: (LogRecord) -> Dict[str, None] + return { + k: v + for k, v in iteritems(vars(record)) + if k not in COMMON_RECORD_ATTRS + and (not isinstance(k, str) or not k.startswith("_")) + } + + +class EventHandler(logging.Handler, object): + """ + A logging handler that emits Sentry events for each log record + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def emit(self, record): + # type: (LogRecord) -> Any + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record): + # type: (LogRecord) -> None + if not _can_record(record): + return + + hub = Hub.current + if hub.client is None: + return + + client_options = hub.client.options + + # exc_info might be None or (None, None, None) + if record.exc_info is not None and record.exc_info[0] is not None: + event, hint = event_from_exception( + record.exc_info, + client_options=client_options, + mechanism={"type": "logging", "handled": True}, + ) + elif record.exc_info and record.exc_info[0] is None: + event = {} + hint = {} + with capture_internal_exceptions(): + event["threads"] = { + "values": [ + { + "stacktrace": current_stacktrace( + client_options["with_locals"] + ), + "crashed": False, + "current": True, + } + ] + } + else: + event = {} + hint = {} + + hint["log_record"] = record + + event["level"] = _logging_to_event_level(record.levelname) + event["logger"] = record.name + event["logentry"] = {"message": to_string(record.msg), "params": record.args} + event["extra"] = _extra_from_record(record) + + hub.capture_event(event, hint=hint) + + +# Legacy name +SentryHandler = EventHandler + + +class BreadcrumbHandler(logging.Handler, object): + """ + A logging handler that records breadcrumbs for each log record. + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def emit(self, record): + # type: (LogRecord) -> Any + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record): + # type: (LogRecord) -> None + if not _can_record(record): + return + + Hub.current.add_breadcrumb( + _breadcrumb_from_record(record), hint={"log_record": record} + ) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/modules.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/modules.py new file mode 100644 index 0000000000..3d78cb89bb --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/modules.py @@ -0,0 +1,56 @@ +from __future__ import absolute_import + +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Dict + from typing import Tuple + from typing import Iterator + + from sentry_sdk._types import Event + + +_installed_modules = None + + +def _generate_installed_modules(): + # type: () -> Iterator[Tuple[str, str]] + try: + import pkg_resources + except ImportError: + return + + for info in pkg_resources.working_set: + yield info.key, info.version + + +def _get_installed_modules(): + # type: () -> Dict[str, str] + global _installed_modules + if _installed_modules is None: + _installed_modules = dict(_generate_installed_modules()) + return _installed_modules + + +class ModulesIntegration(Integration): + identifier = "modules" + + @staticmethod + def setup_once(): + # type: () -> None + @add_global_event_processor + def processor(event, hint): + # type: (Event, Any) -> Dict[str, Any] + if event.get("type") == "transaction": + return event + + if Hub.current.get_integration(ModulesIntegration) is None: + return event + + event["modules"] = _get_installed_modules() + return event diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/pyramid.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/pyramid.py new file mode 100644 index 0000000000..ee9682343a --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/pyramid.py @@ -0,0 +1,217 @@ +from __future__ import absolute_import + +import os +import sys +import weakref + +from pyramid.httpexceptions import HTTPException +from pyramid.request import Request + +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk._compat import reraise, iteritems + +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware + +from sentry_sdk._types import MYPY + +if MYPY: + from pyramid.response import Response + from typing import Any + from sentry_sdk.integrations.wsgi import _ScopedResponse + from typing import Callable + from typing import Dict + from typing import Optional + from webob.cookies import RequestCookies # type: ignore + from webob.compat import cgi_FieldStorage # type: ignore + + from sentry_sdk.utils import ExcInfo + from sentry_sdk._types import EventProcessor + + +if getattr(Request, "authenticated_userid", None): + + def authenticated_userid(request): + # type: (Request) -> Optional[Any] + return request.authenticated_userid + + +else: + # bw-compat for pyramid < 1.5 + from pyramid.security import authenticated_userid # type: ignore + + +TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") + + +class PyramidIntegration(Integration): + identifier = "pyramid" + + transaction_style = None + + def __init__(self, transaction_style="route_name"): + # type: (str) -> None + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once(): + # type: () -> None + from pyramid.router import Router + from pyramid.request import Request + + old_handle_request = Router.handle_request + + def sentry_patched_handle_request(self, request, *args, **kwargs): + # type: (Any, Request, *Any, **Any) -> Response + hub = Hub.current + integration = hub.get_integration(PyramidIntegration) + if integration is not None: + with hub.configure_scope() as scope: + scope.add_event_processor( + _make_event_processor(weakref.ref(request), integration) + ) + + return old_handle_request(self, request, *args, **kwargs) + + Router.handle_request = sentry_patched_handle_request + + if hasattr(Request, "invoke_exception_view"): + old_invoke_exception_view = Request.invoke_exception_view + + def sentry_patched_invoke_exception_view(self, *args, **kwargs): + # type: (Request, *Any, **Any) -> Any + rv = old_invoke_exception_view(self, *args, **kwargs) + + if ( + self.exc_info + and all(self.exc_info) + and rv.status_int == 500 + and Hub.current.get_integration(PyramidIntegration) is not None + ): + _capture_exception(self.exc_info) + + return rv + + Request.invoke_exception_view = sentry_patched_invoke_exception_view + + old_wsgi_call = Router.__call__ + + def sentry_patched_wsgi_call(self, environ, start_response): + # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse + hub = Hub.current + integration = hub.get_integration(PyramidIntegration) + if integration is None: + return old_wsgi_call(self, environ, start_response) + + def sentry_patched_inner_wsgi_call(environ, start_response): + # type: (Dict[str, Any], Callable[..., Any]) -> Any + try: + return old_wsgi_call(self, environ, start_response) + except Exception: + einfo = sys.exc_info() + _capture_exception(einfo) + reraise(*einfo) + + return SentryWsgiMiddleware(sentry_patched_inner_wsgi_call)( + environ, start_response + ) + + Router.__call__ = sentry_patched_wsgi_call + + +def _capture_exception(exc_info): + # type: (ExcInfo) -> None + if exc_info[0] is None or issubclass(exc_info[0], HTTPException): + return + hub = Hub.current + if hub.get_integration(PyramidIntegration) is None: + return + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "pyramid", "handled": False}, + ) + + hub.capture_event(event, hint=hint) + + +class PyramidRequestExtractor(RequestExtractor): + def url(self): + # type: () -> str + return self.request.path_url + + def env(self): + # type: () -> Dict[str, str] + return self.request.environ + + def cookies(self): + # type: () -> RequestCookies + return self.request.cookies + + def raw_data(self): + # type: () -> str + return self.request.text + + def form(self): + # type: () -> Dict[str, str] + return { + key: value + for key, value in iteritems(self.request.POST) + if not getattr(value, "filename", None) + } + + def files(self): + # type: () -> Dict[str, cgi_FieldStorage] + return { + key: value + for key, value in iteritems(self.request.POST) + if getattr(value, "filename", None) + } + + def size_of_file(self, postdata): + # type: (cgi_FieldStorage) -> int + file = postdata.file + try: + return os.fstat(file.fileno()).st_size + except Exception: + return 0 + + +def _make_event_processor(weak_request, integration): + # type: (Callable[[], Request], PyramidIntegration) -> EventProcessor + def event_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + request = weak_request() + if request is None: + return event + + try: + if integration.transaction_style == "route_name": + event["transaction"] = request.matched_route.name + elif integration.transaction_style == "route_pattern": + event["transaction"] = request.matched_route.pattern + except Exception: + pass + + with capture_internal_exceptions(): + PyramidRequestExtractor(request).extract_into_event(event) + + if _should_send_default_pii(): + with capture_internal_exceptions(): + user_info = event.setdefault("user", {}) + user_info.setdefault("id", authenticated_userid(request)) + + return event + + return event_processor diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/redis.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/redis.py new file mode 100644 index 0000000000..510fdbb22c --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/redis.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import + +from sentry_sdk import Hub +from sentry_sdk.utils import capture_internal_exceptions +from sentry_sdk.integrations import Integration + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + + +class RedisIntegration(Integration): + identifier = "redis" + + @staticmethod + def setup_once(): + # type: () -> None + import redis + + patch_redis_client(redis.StrictRedis) + + try: + import rb.clients # type: ignore + except ImportError: + pass + else: + patch_redis_client(rb.clients.FanoutClient) + patch_redis_client(rb.clients.MappingClient) + patch_redis_client(rb.clients.RoutingClient) + + +def patch_redis_client(cls): + # type: (Any) -> None + """ + This function can be used to instrument custom redis client classes or + subclasses. + """ + + old_execute_command = cls.execute_command + + def sentry_patched_execute_command(self, name, *args, **kwargs): + # type: (Any, str, *Any, **Any) -> Any + hub = Hub.current + + if hub.get_integration(RedisIntegration) is None: + return old_execute_command(self, name, *args, **kwargs) + + description = name + + with capture_internal_exceptions(): + description_parts = [name] + for i, arg in enumerate(args): + if i > 10: + break + + description_parts.append(repr(arg)) + + description = " ".join(description_parts) + + with hub.start_span(op="redis", description=description) as span: + if name: + span.set_tag("redis.command", name) + + if name and args and name.lower() in ("get", "set", "setex", "setnx"): + span.set_tag("redis.key", args[0]) + + return old_execute_command(self, name, *args, **kwargs) + + cls.execute_command = sentry_patched_execute_command diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/rq.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/rq.py new file mode 100644 index 0000000000..fbe8cdda3d --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/rq.py @@ -0,0 +1,150 @@ +from __future__ import absolute_import + +import weakref + +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.tracing import Span +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception + + +try: + from rq.version import VERSION as RQ_VERSION + from rq.timeouts import JobTimeoutException + from rq.worker import Worker + from rq.queue import Queue +except ImportError: + raise DidNotEnable("RQ not installed") + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Dict + from typing import Callable + + from rq.job import Job + + from sentry_sdk.utils import ExcInfo + from sentry_sdk._types import EventProcessor + + +class RqIntegration(Integration): + identifier = "rq" + + @staticmethod + def setup_once(): + # type: () -> None + + try: + version = tuple(map(int, RQ_VERSION.split(".")[:3])) + except (ValueError, TypeError): + raise DidNotEnable("Unparseable RQ version: {}".format(RQ_VERSION)) + + if version < (0, 6): + raise DidNotEnable("RQ 0.6 or newer is required.") + + old_perform_job = Worker.perform_job + + def sentry_patched_perform_job(self, job, *args, **kwargs): + # type: (Any, Job, *Queue, **Any) -> bool + hub = Hub.current + integration = hub.get_integration(RqIntegration) + + if integration is None: + return old_perform_job(self, job, *args, **kwargs) + + client = hub.client + assert client is not None + + with hub.push_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(weakref.ref(job))) + + span = Span.continue_from_headers( + job.meta.get("_sentry_trace_headers") or {} + ) + span.op = "rq.task" + + with capture_internal_exceptions(): + span.transaction = job.func_name + + with hub.start_span(span): + rv = old_perform_job(self, job, *args, **kwargs) + + if self.is_horse: + # We're inside of a forked process and RQ is + # about to call `os._exit`. Make sure that our + # events get sent out. + client.flush() + + return rv + + Worker.perform_job = sentry_patched_perform_job + + old_handle_exception = Worker.handle_exception + + def sentry_patched_handle_exception(self, job, *exc_info, **kwargs): + # type: (Worker, Any, *Any, **Any) -> Any + _capture_exception(exc_info) # type: ignore + return old_handle_exception(self, job, *exc_info, **kwargs) + + Worker.handle_exception = sentry_patched_handle_exception + + old_enqueue_job = Queue.enqueue_job + + def sentry_patched_enqueue_job(self, job, **kwargs): + # type: (Queue, Any, **Any) -> Any + hub = Hub.current + if hub.get_integration(RqIntegration) is not None: + job.meta["_sentry_trace_headers"] = dict( + hub.iter_trace_propagation_headers() + ) + + return old_enqueue_job(self, job, **kwargs) + + Queue.enqueue_job = sentry_patched_enqueue_job + + +def _make_event_processor(weak_job): + # type: (Callable[[], Job]) -> EventProcessor + def event_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + job = weak_job() + if job is not None: + with capture_internal_exceptions(): + extra = event.setdefault("extra", {}) + extra["rq-job"] = { + "job_id": job.id, + "func": job.func_name, + "args": job.args, + "kwargs": job.kwargs, + "description": job.description, + } + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], JobTimeoutException): + event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name] + + return event + + return event_processor + + +def _capture_exception(exc_info, **kwargs): + # type: (ExcInfo, **Any) -> None + hub = Hub.current + if hub.get_integration(RqIntegration) is None: + return + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "rq", "handled": False}, + ) + + hub.capture_event(event, hint=hint) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/sanic.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/sanic.py new file mode 100644 index 0000000000..e8fdca422a --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/sanic.py @@ -0,0 +1,233 @@ +import sys +import weakref +from inspect import isawaitable + +from sentry_sdk._compat import urlparse, reraise +from sentry_sdk.hub import Hub +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + HAS_REAL_CONTEXTVARS, +) +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers +from sentry_sdk.integrations.logging import ignore_logger + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Callable + from typing import Optional + from typing import Union + from typing import Tuple + from typing import Dict + + from sanic.request import Request, RequestParameters + + from sentry_sdk._types import Event, EventProcessor, Hint + +try: + from sanic import Sanic, __version__ as SANIC_VERSION + from sanic.exceptions import SanicException + from sanic.router import Router + from sanic.handlers import ErrorHandler +except ImportError: + raise DidNotEnable("Sanic not installed") + + +class SanicIntegration(Integration): + identifier = "sanic" + + @staticmethod + def setup_once(): + # type: () -> None + try: + version = tuple(map(int, SANIC_VERSION.split("."))) + except (TypeError, ValueError): + raise DidNotEnable("Unparseable Sanic version: {}".format(SANIC_VERSION)) + + if version < (0, 8): + raise DidNotEnable("Sanic 0.8 or newer required.") + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The sanic integration for Sentry requires Python 3.7+ " + " or aiocontextvars package" + ) + + if SANIC_VERSION.startswith("0.8."): + # Sanic 0.8 and older creates a logger named "root" and puts a + # stringified version of every exception in there (without exc_info), + # which our error deduplication can't detect. + # + # We explicitly check the version here because it is a very + # invasive step to ignore this logger and not necessary in newer + # versions at all. + # + # https://github.com/huge-success/sanic/issues/1332 + ignore_logger("root") + + old_handle_request = Sanic.handle_request + + async def sentry_handle_request(self, request, *args, **kwargs): + # type: (Any, Request, *Any, **Any) -> Any + hub = Hub.current + if hub.get_integration(SanicIntegration) is None: + return old_handle_request(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + + with Hub(hub) as hub: + with hub.configure_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + response = old_handle_request(self, request, *args, **kwargs) + if isawaitable(response): + response = await response + + return response + + Sanic.handle_request = sentry_handle_request + + old_router_get = Router.get + + def sentry_router_get(self, request): + # type: (Any, Request) -> Any + rv = old_router_get(self, request) + hub = Hub.current + if hub.get_integration(SanicIntegration) is not None: + with capture_internal_exceptions(): + with hub.configure_scope() as scope: + scope.transaction = rv[0].__name__ + return rv + + Router.get = sentry_router_get + + old_error_handler_lookup = ErrorHandler.lookup + + def sentry_error_handler_lookup(self, exception): + # type: (Any, Exception) -> Optional[object] + _capture_exception(exception) + old_error_handler = old_error_handler_lookup(self, exception) + + if old_error_handler is None: + return None + + if Hub.current.get_integration(SanicIntegration) is None: + return old_error_handler + + async def sentry_wrapped_error_handler(request, exception): + # type: (Request, Exception) -> Any + try: + response = old_error_handler(request, exception) + if isawaitable(response): + response = await response + return response + except Exception: + # Report errors that occur in Sanic error handler. These + # exceptions will not even show up in Sanic's + # `sanic.exceptions` logger. + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return sentry_wrapped_error_handler + + ErrorHandler.lookup = sentry_error_handler_lookup + + +def _capture_exception(exception): + # type: (Union[Tuple[Optional[type], Optional[BaseException], Any], BaseException]) -> None + hub = Hub.current + integration = hub.get_integration(SanicIntegration) + if integration is None: + return + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + with capture_internal_exceptions(): + event, hint = event_from_exception( + exception, + client_options=client.options, + mechanism={"type": "sanic", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + +def _make_request_processor(weak_request): + # type: (Callable[[], Request]) -> EventProcessor + def sanic_processor(event, hint): + # type: (Event, Optional[Hint]) -> Optional[Event] + + try: + if hint and issubclass(hint["exc_info"][0], SanicException): + return None + except KeyError: + pass + + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + extractor = SanicRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + urlparts = urlparse.urlsplit(request.url) + + request_info["url"] = "%s://%s%s" % ( + urlparts.scheme, + urlparts.netloc, + urlparts.path, + ) + + request_info["query_string"] = urlparts.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_addr} + request_info["headers"] = _filter_headers(dict(request.headers)) + + return event + + return sanic_processor + + +class SanicRequestExtractor(RequestExtractor): + def content_length(self): + # type: () -> int + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self): + # type: () -> Dict[str, str] + return dict(self.request.cookies) + + def raw_data(self): + # type: () -> bytes + return self.request.body + + def form(self): + # type: () -> RequestParameters + return self.request.form + + def is_json(self): + # type: () -> bool + raise NotImplementedError() + + def json(self): + # type: () -> Optional[Any] + return self.request.json + + def files(self): + # type: () -> RequestParameters + return self.request.files + + def size_of_file(self, file): + # type: (Any) -> int + return len(file.body or ()) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/serverless.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/serverless.py new file mode 100644 index 0000000000..6dd90b43d0 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/serverless.py @@ -0,0 +1,87 @@ +import functools +import sys + +from sentry_sdk.hub import Hub +from sentry_sdk.utils import event_from_exception +from sentry_sdk._compat import reraise + + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Callable + from typing import TypeVar + from typing import Union + from typing import Optional + + from typing import overload + + F = TypeVar("F", bound=Callable[..., Any]) + +else: + + def overload(x): + # type: (F) -> F + return x + + +@overload +def serverless_function(f, flush=True): + # type: (F, bool) -> F + pass + + +@overload # noqa +def serverless_function(f=None, flush=True): + # type: (None, bool) -> Callable[[F], F] + pass + + +def serverless_function(f=None, flush=True): # noqa + # type: (Optional[F], bool) -> Union[F, Callable[[F], F]] + def wrapper(f): + # type: (F) -> F + @functools.wraps(f) + def inner(*args, **kwargs): + # type: (*Any, **Any) -> Any + with Hub(Hub.current) as hub: + with hub.configure_scope() as scope: + scope.clear_breadcrumbs() + + try: + return f(*args, **kwargs) + except Exception: + _capture_and_reraise() + finally: + if flush: + _flush_client() + + return inner # type: ignore + + if f is None: + return wrapper + else: + return wrapper(f) + + +def _capture_and_reraise(): + # type: () -> None + exc_info = sys.exc_info() + hub = Hub.current + if hub is not None and hub.client is not None: + event, hint = event_from_exception( + exc_info, + client_options=hub.client.options, + mechanism={"type": "serverless", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + reraise(*exc_info) + + +def _flush_client(): + # type: () -> None + hub = Hub.current + if hub is not None: + hub.flush() diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/__init__.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/__init__.py new file mode 100644 index 0000000000..10d94163c5 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/__init__.py @@ -0,0 +1,4 @@ +from sentry_sdk.integrations.spark.spark_driver import SparkIntegration +from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration + +__all__ = ["SparkIntegration", "SparkWorkerIntegration"] diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_driver.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_driver.py new file mode 100644 index 0000000000..ea43c37821 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_driver.py @@ -0,0 +1,263 @@ +from sentry_sdk import configure_scope +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import capture_internal_exceptions + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class SparkIntegration(Integration): + identifier = "spark" + + @staticmethod + def setup_once(): + # type: () -> None + patch_spark_context_init() + + +def _set_app_properties(): + # type: () -> None + """ + Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties. + This allows worker integration to have access to app_name and application_id. + """ + from pyspark import SparkContext + + spark_context = SparkContext._active_spark_context + if spark_context: + spark_context.setLocalProperty("sentry_app_name", spark_context.appName) + spark_context.setLocalProperty( + "sentry_application_id", spark_context.applicationId + ) + + +def _start_sentry_listener(sc): + # type: (Any) -> None + """ + Start java gateway server to add custom `SparkListener` + """ + from pyspark.java_gateway import ensure_callback_server_started + + gw = sc._gateway + ensure_callback_server_started(gw) + listener = SentryListener() + sc._jsc.sc().addSparkListener(listener) + + +def patch_spark_context_init(): + # type: () -> None + from pyspark import SparkContext + + spark_context_init = SparkContext._do_init + + def _sentry_patched_spark_context_init(self, *args, **kwargs): + # type: (SparkContext, *Any, **Any) -> Optional[Any] + init = spark_context_init(self, *args, **kwargs) + + if Hub.current.get_integration(SparkIntegration) is None: + return init + + _start_sentry_listener(self) + _set_app_properties() + + with configure_scope() as scope: + + @scope.add_event_processor + def process_event(event, hint): + # type: (Event, Hint) -> Optional[Event] + with capture_internal_exceptions(): + if Hub.current.get_integration(SparkIntegration) is None: + return event + + event.setdefault("user", {}).setdefault("id", self.sparkUser()) + + event.setdefault("tags", {}).setdefault( + "executor.id", self._conf.get("spark.executor.id") + ) + event["tags"].setdefault( + "spark-submit.deployMode", + self._conf.get("spark.submit.deployMode"), + ) + event["tags"].setdefault( + "driver.host", self._conf.get("spark.driver.host") + ) + event["tags"].setdefault( + "driver.port", self._conf.get("spark.driver.port") + ) + event["tags"].setdefault("spark_version", self.version) + event["tags"].setdefault("app_name", self.appName) + event["tags"].setdefault("application_id", self.applicationId) + event["tags"].setdefault("master", self.master) + event["tags"].setdefault("spark_home", self.sparkHome) + + event.setdefault("extra", {}).setdefault("web_url", self.uiWebUrl) + + return event + + return init + + SparkContext._do_init = _sentry_patched_spark_context_init + + +class SparkListener(object): + def onApplicationEnd(self, applicationEnd): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onApplicationStart(self, applicationStart): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onBlockManagerAdded(self, blockManagerAdded): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onBlockManagerRemoved(self, blockManagerRemoved): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onBlockUpdated(self, blockUpdated): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onEnvironmentUpdate(self, environmentUpdate): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onExecutorAdded(self, executorAdded): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onExecutorBlacklisted(self, executorBlacklisted): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onExecutorBlacklistedForStage( # noqa: N802 + self, executorBlacklistedForStage # noqa: N803 + ): + # type: (Any) -> None + pass + + def onExecutorMetricsUpdate(self, executorMetricsUpdate): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onExecutorRemoved(self, executorRemoved): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onJobEnd(self, jobEnd): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onJobStart(self, jobStart): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onNodeBlacklisted(self, nodeBlacklisted): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onNodeBlacklistedForStage(self, nodeBlacklistedForStage): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onNodeUnblacklisted(self, nodeUnblacklisted): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onOtherEvent(self, event): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onSpeculativeTaskSubmitted(self, speculativeTask): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onStageCompleted(self, stageCompleted): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onStageSubmitted(self, stageSubmitted): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onTaskEnd(self, taskEnd): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onTaskGettingResult(self, taskGettingResult): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onTaskStart(self, taskStart): # noqa: N802,N803 + # type: (Any) -> None + pass + + def onUnpersistRDD(self, unpersistRDD): # noqa: N802,N803 + # type: (Any) -> None + pass + + class Java: + implements = ["org.apache.spark.scheduler.SparkListenerInterface"] + + +class SentryListener(SparkListener): + def __init__(self): + # type: () -> None + self.hub = Hub.current + + def onJobStart(self, jobStart): # noqa: N802,N803 + # type: (Any) -> None + message = "Job {} Started".format(jobStart.jobId()) + self.hub.add_breadcrumb(level="info", message=message) + _set_app_properties() + + def onJobEnd(self, jobEnd): # noqa: N802,N803 + # type: (Any) -> None + level = "" + message = "" + data = {"result": jobEnd.jobResult().toString()} + + if jobEnd.jobResult().toString() == "JobSucceeded": + level = "info" + message = "Job {} Ended".format(jobEnd.jobId()) + else: + level = "warning" + message = "Job {} Failed".format(jobEnd.jobId()) + + self.hub.add_breadcrumb(level=level, message=message, data=data) + + def onStageSubmitted(self, stageSubmitted): # noqa: N802,N803 + # type: (Any) -> None + stage_info = stageSubmitted.stageInfo() + message = "Stage {} Submitted".format(stage_info.stageId()) + data = {"attemptId": stage_info.attemptId(), "name": stage_info.name()} + self.hub.add_breadcrumb(level="info", message=message, data=data) + _set_app_properties() + + def onStageCompleted(self, stageCompleted): # noqa: N802,N803 + # type: (Any) -> None + from py4j.protocol import Py4JJavaError # type: ignore + + stage_info = stageCompleted.stageInfo() + message = "" + level = "" + data = {"attemptId": stage_info.attemptId(), "name": stage_info.name()} + + # Have to Try Except because stageInfo.failureReason() is typed with Scala Option + try: + data["reason"] = stage_info.failureReason().get() + message = "Stage {} Failed".format(stage_info.stageId()) + level = "warning" + except Py4JJavaError: + message = "Stage {} Completed".format(stage_info.stageId()) + level = "info" + + self.hub.add_breadcrumb(level=level, message=message, data=data) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_worker.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_worker.py new file mode 100644 index 0000000000..bae4413d11 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_worker.py @@ -0,0 +1,120 @@ +from __future__ import absolute_import + +import sys + +from sentry_sdk import configure_scope +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + exc_info_from_error, + single_exception_from_error_tuple, + walk_exception_chain, + event_hint_with_exc_info, +) + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Optional + + from sentry_sdk._types import ExcInfo, Event, Hint + + +class SparkWorkerIntegration(Integration): + identifier = "spark_worker" + + @staticmethod + def setup_once(): + # type: () -> None + import pyspark.daemon as original_daemon + + original_daemon.worker_main = _sentry_worker_main + + +def _capture_exception(exc_info, hub): + # type: (ExcInfo, Hub) -> None + client = hub.client + + client_options = client.options # type: ignore + + mechanism = {"type": "spark", "handled": False} + + exc_info = exc_info_from_error(exc_info) + + exc_type, exc_value, tb = exc_info + rv = [] + + # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors + for exc_type, exc_value, tb in walk_exception_chain(exc_info): + if exc_type not in (SystemExit, EOFError, ConnectionResetError): + rv.append( + single_exception_from_error_tuple( + exc_type, exc_value, tb, client_options, mechanism + ) + ) + + if rv: + rv.reverse() + hint = event_hint_with_exc_info(exc_info) + event = {"level": "error", "exception": {"values": rv}} + + _tag_task_context() + + hub.capture_event(event, hint=hint) + + +def _tag_task_context(): + # type: () -> None + from pyspark.taskcontext import TaskContext + + with configure_scope() as scope: + + @scope.add_event_processor + def process_event(event, hint): + # type: (Event, Hint) -> Optional[Event] + with capture_internal_exceptions(): + integration = Hub.current.get_integration(SparkWorkerIntegration) + task_context = TaskContext.get() + + if integration is None or task_context is None: + return event + + event.setdefault("tags", {}).setdefault( + "stageId", task_context.stageId() + ) + event["tags"].setdefault("partitionId", task_context.partitionId()) + event["tags"].setdefault("attemptNumber", task_context.attemptNumber()) + event["tags"].setdefault("taskAttemptId", task_context.taskAttemptId()) + + if task_context._localProperties: + if "sentry_app_name" in task_context._localProperties: + event["tags"].setdefault( + "app_name", task_context._localProperties["sentry_app_name"] + ) + event["tags"].setdefault( + "application_id", + task_context._localProperties["sentry_application_id"], + ) + + if "callSite.short" in task_context._localProperties: + event.setdefault("extra", {}).setdefault( + "callSite", task_context._localProperties["callSite.short"] + ) + + return event + + +def _sentry_worker_main(*args, **kwargs): + # type: (*Optional[Any], **Optional[Any]) -> None + import pyspark.worker as original_worker + + try: + original_worker.main(*args, **kwargs) + except SystemExit: + if Hub.current.get_integration(SparkWorkerIntegration) is not None: + hub = Hub.current + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info, hub) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/sqlalchemy.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/sqlalchemy.py new file mode 100644 index 0000000000..f24d2f20bf --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/sqlalchemy.py @@ -0,0 +1,86 @@ +from __future__ import absolute_import + +from sentry_sdk._types import MYPY +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.tracing import record_sql_queries + +try: + from sqlalchemy.engine import Engine # type: ignore + from sqlalchemy.event import listen # type: ignore + from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore +except ImportError: + raise DidNotEnable("SQLAlchemy not installed.") + +if MYPY: + from typing import Any + from typing import ContextManager + from typing import Optional + + from sentry_sdk.tracing import Span + + +class SqlalchemyIntegration(Integration): + identifier = "sqlalchemy" + + @staticmethod + def setup_once(): + # type: () -> None + + try: + version = tuple(map(int, SQLALCHEMY_VERSION.split("b")[0].split("."))) + except (TypeError, ValueError): + raise DidNotEnable( + "Unparseable SQLAlchemy version: {}".format(SQLALCHEMY_VERSION) + ) + + if version < (1, 2): + raise DidNotEnable("SQLAlchemy 1.2 or newer required.") + + listen(Engine, "before_cursor_execute", _before_cursor_execute) + listen(Engine, "after_cursor_execute", _after_cursor_execute) + listen(Engine, "handle_error", _handle_error) + + +def _before_cursor_execute( + conn, cursor, statement, parameters, context, executemany, *args +): + # type: (Any, Any, Any, Any, Any, bool, *Any) -> None + hub = Hub.current + if hub.get_integration(SqlalchemyIntegration) is None: + return + + ctx_mgr = record_sql_queries( + hub, + cursor, + statement, + parameters, + paramstyle=context and context.dialect and context.dialect.paramstyle or None, + executemany=executemany, + ) + conn._sentry_sql_span_manager = ctx_mgr + + span = ctx_mgr.__enter__() + + if span is not None: + conn._sentry_sql_span = span + + +def _after_cursor_execute(conn, cursor, statement, *args): + # type: (Any, Any, Any, *Any) -> None + ctx_mgr = getattr( + conn, "_sentry_sql_span_manager", None + ) # type: ContextManager[Any] + + if ctx_mgr is not None: + conn._sentry_sql_span_manager = None + ctx_mgr.__exit__(None, None, None) + + +def _handle_error(context, *args): + # type: (Any, *Any) -> None + conn = context.connection + span = getattr(conn, "_sentry_sql_span", None) # type: Optional[Span] + + if span is not None: + span.set_status("internal_error") diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/stdlib.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/stdlib.py new file mode 100644 index 0000000000..56cece70ac --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/stdlib.py @@ -0,0 +1,230 @@ +import os +import subprocess +import sys +import platform + +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.tracing import EnvironHeaders +from sentry_sdk.utils import capture_internal_exceptions, safe_repr + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Callable + from typing import Dict + from typing import Optional + from typing import List + + from sentry_sdk._types import Event, Hint + + +try: + from httplib import HTTPConnection # type: ignore +except ImportError: + from http.client import HTTPConnection + + +_RUNTIME_CONTEXT = { + "name": platform.python_implementation(), + "version": "%s.%s.%s" % (sys.version_info[:3]), + "build": sys.version, +} + + +class StdlibIntegration(Integration): + identifier = "stdlib" + + @staticmethod + def setup_once(): + # type: () -> None + _install_httplib() + _install_subprocess() + + @add_global_event_processor + def add_python_runtime_context(event, hint): + # type: (Event, Hint) -> Optional[Event] + if Hub.current.get_integration(StdlibIntegration) is not None: + contexts = event.setdefault("contexts", {}) + if isinstance(contexts, dict) and "runtime" not in contexts: + contexts["runtime"] = _RUNTIME_CONTEXT + + return event + + +def _install_httplib(): + # type: () -> None + real_putrequest = HTTPConnection.putrequest + real_getresponse = HTTPConnection.getresponse + + def putrequest(self, method, url, *args, **kwargs): + # type: (HTTPConnection, str, str, *Any, **Any) -> Any + hub = Hub.current + if hub.get_integration(StdlibIntegration) is None: + return real_putrequest(self, method, url, *args, **kwargs) + + host = self.host + port = self.port + default_port = self.default_port + + real_url = url + if not real_url.startswith(("http://", "https://")): + real_url = "%s://%s%s%s" % ( + default_port == 443 and "https" or "http", + host, + port != default_port and ":%s" % port or "", + url, + ) + + span = hub.start_span(op="http", description="%s %s" % (method, real_url)) + + span.set_data("method", method) + span.set_data("url", real_url) + + rv = real_putrequest(self, method, url, *args, **kwargs) + + for key, value in hub.iter_trace_propagation_headers(): + self.putheader(key, value) + + self._sentrysdk_span = span + + return rv + + def getresponse(self, *args, **kwargs): + # type: (HTTPConnection, *Any, **Any) -> Any + span = getattr(self, "_sentrysdk_span", None) + + if span is None: + return real_getresponse(self, *args, **kwargs) + + rv = real_getresponse(self, *args, **kwargs) + + span.set_data("status_code", rv.status) + span.set_http_status(int(rv.status)) + span.set_data("reason", rv.reason) + span.finish() + + return rv + + HTTPConnection.putrequest = putrequest + HTTPConnection.getresponse = getresponse + + +def _init_argument(args, kwargs, name, position, setdefault_callback=None): + # type: (List[Any], Dict[Any, Any], str, int, Optional[Callable[[Any], Any]]) -> Any + """ + given (*args, **kwargs) of a function call, retrieve (and optionally set a + default for) an argument by either name or position. + + This is useful for wrapping functions with complex type signatures and + extracting a few arguments without needing to redefine that function's + entire type signature. + """ + + if name in kwargs: + rv = kwargs[name] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + kwargs[name] = rv + elif position < len(args): + rv = args[position] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + args[position] = rv + else: + rv = setdefault_callback and setdefault_callback(None) + if rv is not None: + kwargs[name] = rv + + return rv + + +def _install_subprocess(): + # type: () -> None + old_popen_init = subprocess.Popen.__init__ + + def sentry_patched_popen_init(self, *a, **kw): + # type: (subprocess.Popen[Any], *Any, **Any) -> None + + hub = Hub.current + if hub.get_integration(StdlibIntegration) is None: + return old_popen_init(self, *a, **kw) # type: ignore + + # Convert from tuple to list to be able to set values. + a = list(a) + + args = _init_argument(a, kw, "args", 0) or [] + cwd = _init_argument(a, kw, "cwd", 9) + + # if args is not a list or tuple (and e.g. some iterator instead), + # let's not use it at all. There are too many things that can go wrong + # when trying to collect an iterator into a list and setting that list + # into `a` again. + # + # Also invocations where `args` is not a sequence are not actually + # legal. They just happen to work under CPython. + description = None + + if isinstance(args, (list, tuple)) and len(args) < 100: + with capture_internal_exceptions(): + description = " ".join(map(str, args)) + + if description is None: + description = safe_repr(args) + + env = None + + for k, v in hub.iter_trace_propagation_headers(): + if env is None: + env = _init_argument(a, kw, "env", 10, lambda x: dict(x or os.environ)) + env["SUBPROCESS_" + k.upper().replace("-", "_")] = v + + with hub.start_span(op="subprocess", description=description) as span: + if cwd: + span.set_data("subprocess.cwd", cwd) + + rv = old_popen_init(self, *a, **kw) # type: ignore + + span.set_tag("subprocess.pid", self.pid) + return rv + + subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore + + old_popen_wait = subprocess.Popen.wait + + def sentry_patched_popen_wait(self, *a, **kw): + # type: (subprocess.Popen[Any], *Any, **Any) -> Any + hub = Hub.current + + if hub.get_integration(StdlibIntegration) is None: + return old_popen_wait(self, *a, **kw) + + with hub.start_span(op="subprocess.wait") as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_wait(self, *a, **kw) + + subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore + + old_popen_communicate = subprocess.Popen.communicate + + def sentry_patched_popen_communicate(self, *a, **kw): + # type: (subprocess.Popen[Any], *Any, **Any) -> Any + hub = Hub.current + + if hub.get_integration(StdlibIntegration) is None: + return old_popen_communicate(self, *a, **kw) + + with hub.start_span(op="subprocess.communicate") as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_communicate(self, *a, **kw) + + subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore + + +def get_subprocess_traceparent_headers(): + # type: () -> EnvironHeaders + return EnvironHeaders(os.environ, prefix="SUBPROCESS_") diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/threading.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/threading.py new file mode 100644 index 0000000000..b750257e2a --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/threading.py @@ -0,0 +1,90 @@ +from __future__ import absolute_import + +import sys +from threading import Thread, current_thread + +from sentry_sdk import Hub +from sentry_sdk._compat import reraise +from sentry_sdk._types import MYPY +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import event_from_exception, capture_internal_exceptions + +if MYPY: + from typing import Any + from typing import TypeVar + from typing import Callable + from typing import Optional + + from sentry_sdk._types import ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + + +class ThreadingIntegration(Integration): + identifier = "threading" + + def __init__(self, propagate_hub=False): + # type: (bool) -> None + self.propagate_hub = propagate_hub + + @staticmethod + def setup_once(): + # type: () -> None + old_start = Thread.start + + def sentry_start(self, *a, **kw): + # type: (Thread, *Any, **Any) -> Any + hub = Hub.current + integration = hub.get_integration(ThreadingIntegration) + if integration is not None: + if not integration.propagate_hub: + hub_ = None + else: + hub_ = Hub(hub) + # Patching instance methods in `start()` creates a reference cycle if + # done in a naive way. See + # https://github.com/getsentry/sentry-python/pull/434 + # + # In threading module, using current_thread API will access current thread instance + # without holding it to avoid a reference cycle in an easier way. + with capture_internal_exceptions(): + new_run = _wrap_run(hub_, getattr(self.run, "__func__", self.run)) + self.run = new_run # type: ignore + + return old_start(self, *a, **kw) # type: ignore + + Thread.start = sentry_start # type: ignore + + +def _wrap_run(parent_hub, old_run_func): + # type: (Optional[Hub], F) -> F + def run(*a, **kw): + # type: (*Any, **Any) -> Any + hub = parent_hub or Hub.current + with hub: + try: + self = current_thread() + return old_run_func(self, *a, **kw) + except Exception: + reraise(*_capture_exception()) + + return run # type: ignore + + +def _capture_exception(): + # type: () -> ExcInfo + hub = Hub.current + exc_info = sys.exc_info() + + if hub.get_integration(ThreadingIntegration) is not None: + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "threading", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + return exc_info diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/tornado.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/tornado.py new file mode 100644 index 0000000000..d3ae065690 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/tornado.py @@ -0,0 +1,203 @@ +import weakref +from inspect import iscoroutinefunction + +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.utils import ( + HAS_REAL_CONTEXTVARS, + event_from_exception, + capture_internal_exceptions, + transaction_from_function, +) +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations._wsgi_common import ( + RequestExtractor, + _filter_headers, + _is_json_content_type, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk._compat import iteritems + +try: + from tornado import version_info as TORNADO_VERSION # type: ignore + from tornado.web import RequestHandler, HTTPError + from tornado.gen import coroutine +except ImportError: + raise DidNotEnable("Tornado not installed") + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Any + from typing import Optional + from typing import Dict + from typing import Callable + + from sentry_sdk._types import EventProcessor + + +class TornadoIntegration(Integration): + identifier = "tornado" + + @staticmethod + def setup_once(): + # type: () -> None + if TORNADO_VERSION < (5, 0): + raise DidNotEnable("Tornado 5+ required") + + if not HAS_REAL_CONTEXTVARS: + # Tornado is async. We better have contextvars or we're going to leak + # state between requests. + raise DidNotEnable( + "The tornado integration for Sentry requires Python 3.6+ or the aiocontextvars package" + ) + + ignore_logger("tornado.access") + + old_execute = RequestHandler._execute # type: ignore + + awaitable = iscoroutinefunction(old_execute) + + if awaitable: + # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) + # In that case our method should be a coroutine function too + async def sentry_execute_request_handler(self, *args, **kwargs): + # type: (Any, *Any, **Any) -> Any + hub = Hub.current + integration = hub.get_integration(TornadoIntegration) + if integration is None: + return await old_execute(self, *args, **kwargs) + + weak_handler = weakref.ref(self) + + with Hub(hub) as hub: + with hub.configure_scope() as scope: + scope.clear_breadcrumbs() + processor = _make_event_processor(weak_handler) # type: ignore + scope.add_event_processor(processor) + return await old_execute(self, *args, **kwargs) + + else: + + @coroutine # type: ignore + def sentry_execute_request_handler(self, *args, **kwargs): + # type: (RequestHandler, *Any, **Any) -> Any + hub = Hub.current + integration = hub.get_integration(TornadoIntegration) + if integration is None: + return old_execute(self, *args, **kwargs) + + weak_handler = weakref.ref(self) + + with Hub(hub) as hub: + with hub.configure_scope() as scope: + scope.clear_breadcrumbs() + processor = _make_event_processor(weak_handler) # type: ignore + scope.add_event_processor(processor) + result = yield from old_execute(self, *args, **kwargs) + return result + + RequestHandler._execute = sentry_execute_request_handler # type: ignore + + old_log_exception = RequestHandler.log_exception + + def sentry_log_exception(self, ty, value, tb, *args, **kwargs): + # type: (Any, type, BaseException, Any, *Any, **Any) -> Optional[Any] + _capture_exception(ty, value, tb) + return old_log_exception(self, ty, value, tb, *args, **kwargs) # type: ignore + + RequestHandler.log_exception = sentry_log_exception # type: ignore + + +def _capture_exception(ty, value, tb): + # type: (type, BaseException, Any) -> None + hub = Hub.current + if hub.get_integration(TornadoIntegration) is None: + return + if isinstance(value, HTTPError): + return + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + event, hint = event_from_exception( + (ty, value, tb), + client_options=client.options, + mechanism={"type": "tornado", "handled": False}, + ) + + hub.capture_event(event, hint=hint) + + +def _make_event_processor(weak_handler): + # type: (Callable[[], RequestHandler]) -> EventProcessor + def tornado_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + handler = weak_handler() + if handler is None: + return event + + request = handler.request + + with capture_internal_exceptions(): + method = getattr(handler, handler.request.method.lower()) + event["transaction"] = transaction_from_function(method) + + with capture_internal_exceptions(): + extractor = TornadoRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + + request_info["url"] = "%s://%s%s" % ( + request.protocol, + request.host, + request.path, + ) + + request_info["query_string"] = request.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_ip} + request_info["headers"] = _filter_headers(dict(request.headers)) + + with capture_internal_exceptions(): + if handler.current_user and _should_send_default_pii(): + event.setdefault("user", {}).setdefault("is_authenticated", True) + + return event + + return tornado_processor + + +class TornadoRequestExtractor(RequestExtractor): + def content_length(self): + # type: () -> int + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self): + # type: () -> Dict[str, str] + return {k: v.value for k, v in iteritems(self.request.cookies)} + + def raw_data(self): + # type: () -> bytes + return self.request.body + + def form(self): + # type: () -> Dict[str, Any] + return { + k: [v.decode("latin1", "replace") for v in vs] + for k, vs in iteritems(self.request.body_arguments) + } + + def is_json(self): + # type: () -> bool + return _is_json_content_type(self.request.headers.get("content-type")) + + def files(self): + # type: () -> Dict[str, Any] + return {k: v[0] for k, v in iteritems(self.request.files) if v} + + def size_of_file(self, file): + # type: (Any) -> int + return len(file.body or ()) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/trytond.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/trytond.py new file mode 100644 index 0000000000..062a756993 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/trytond.py @@ -0,0 +1,55 @@ +import sentry_sdk.hub +import sentry_sdk.utils +import sentry_sdk.integrations +import sentry_sdk.integrations.wsgi +from sentry_sdk._types import MYPY + +from trytond.exceptions import TrytonException # type: ignore +from trytond.wsgi import app # type: ignore + +if MYPY: + from typing import Any + + +# TODO: trytond-worker, trytond-cron and trytond-admin intergations + + +class TrytondWSGIIntegration(sentry_sdk.integrations.Integration): + identifier = "trytond_wsgi" + + def __init__(self): # type: () -> None + pass + + @staticmethod + def setup_once(): # type: () -> None + + app.wsgi_app = sentry_sdk.integrations.wsgi.SentryWsgiMiddleware(app.wsgi_app) + + def error_handler(e): # type: (Exception) -> None + hub = sentry_sdk.hub.Hub.current + + if hub.get_integration(TrytondWSGIIntegration) is None: + return + elif isinstance(e, TrytonException): + return + else: + # If an integration is there, a client has to be there. + client = hub.client # type: Any + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=client.options, + mechanism={"type": "trytond", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + # Expected error handlers signature was changed + # when the error_handler decorator was introduced + # in Tryton-5.4 + if hasattr(app, "error_handler"): + + @app.error_handler + def _(app, request, e): # type: ignore + error_handler(e) + + else: + app.error_handlers.append(error_handler) diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/wsgi.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/wsgi.py new file mode 100644 index 0000000000..22982d8bb1 --- /dev/null +++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/wsgi.py @@ -0,0 +1,309 @@ +import functools +import sys + +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exceptions, + event_from_exception, +) +from sentry_sdk._compat import PY2, reraise, iteritems +from sentry_sdk.tracing import Span +from sentry_sdk.sessions import auto_session_tracking +from sentry_sdk.integrations._wsgi_common import _filter_headers + +from sentry_sdk._types import MYPY + +if MYPY: + from typing import Callable + from typing import Dict + from typing import Iterator + from typing import Any + from typing import Tuple + from typing import Optional + from typing import TypeVar + from typing import Protocol + + from sentry_sdk.utils import ExcInfo + from sentry_sdk._types import EventProcessor + + WsgiResponseIter = TypeVar("WsgiResponseIter") + WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") + WsgiExcInfo = TypeVar("WsgiExcInfo") + + class StartResponse(Protocol): + def __call__(self, status, response_headers, exc_info=None): + # type: (str, WsgiResponseHeaders, Optional[WsgiExcInfo]) -> WsgiResponseIter + pass + + +_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") + + +if PY2: + + def wsgi_decoding_dance(s, charset="utf-8", errors="replace"): + # type: (str, str, str) -> str + return s.decode(charset, errors) + + +else: + + def wsgi_decoding_dance(s, charset="utf-8", errors="replace"): + # type: (str, str, str) -> str + return s.encode("latin1").decode(charset, errors) + + +def get_host(environ): + # type: (Dict[str, str]) -> str + """Return the host for the given WSGI environment. Yanked from Werkzeug.""" + if environ.get("HTTP_HOST"): + rv = environ["HTTP_HOST"] + if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"): + rv = rv[:-3] + elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"): + rv = rv[:-4] + elif environ.get("SERVER_NAME"): + rv = environ["SERVER_NAME"] + if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( + ("https", "443"), + ("http", "80"), + ): + rv += ":" + environ["SERVER_PORT"] + else: + # In spite of the WSGI spec, SERVER_NAME might not be present. + rv = "unknown" + + return rv + + +def get_request_url(environ): + # type: (Dict[str, str]) -> str + """Return the absolute URL without query string for the given WSGI + environment.""" + return "%s://%s/%s" % ( + environ.get("wsgi.url_scheme"), + get_host(environ), + wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"), + ) + + +class SentryWsgiMiddleware(object): + __slots__ = ("app",) + + def __init__(self, app): + # type: (Callable[[Dict[str, str], Callable[..., Any]], Any]) -> None + self.app = app + + def __call__(self, environ, start_response): + # type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse + if _wsgi_middleware_applied.get(False): + return self.app(environ, start_response) + + _wsgi_middleware_applied.set(True) + try: + hub = Hub(Hub.current) + with auto_session_tracking(hub): + with hub: + with capture_internal_exceptions(): + with hub.configure_scope() as scope: + scope.clear_breadcrumbs() + scope._name = "wsgi" + scope.add_event_processor( + _make_wsgi_event_processor(environ) + ) + + span = Span.continue_from_environ(environ) + span.op = "http.server" + span.transaction = "generic WSGI request" + + with hub.start_span(span) as span: + try: + rv = self.app( + environ, + functools.partial( + _sentry_start_response, start_response, span + ), + ) + except BaseException: + reraise(*_capture_exception(hub)) + finally: + _wsgi_middleware_applied.set(False) + + return _ScopedResponse(hub, rv) + + +def _sentry_start_response( + old_start_response, # type: StartResponse + span, # type: Span + status, # type: str + response_headers, # type: WsgiResponseHeaders + exc_info=None, # type: Optional[WsgiExcInfo] +): + # type: (...) -> WsgiResponseIter + with capture_internal_exceptions(): + status_int = int(status.split(" ", 1)[0]) + span.set_http_status(status_int) + + if exc_info is None: + # The Django Rest Framework WSGI test client, and likely other + # (incorrect) implementations, cannot deal with the exc_info argument + # if one is present. Avoid providing a third argument if not necessary. + return old_start_response(status, response_headers) + else: + return old_start_response(status, response_headers, exc_info) + + +def _get_environ(environ): + # type: (Dict[str, str]) -> Iterator[Tuple[str, str]] + """ + Returns our whitelisted environment variables. + """ + keys = ["SERVER_NAME", "SERVER_PORT"] + if _should_send_default_pii(): + # make debugging of proxy setup easier. Proxy headers are + # in headers. + keys += ["REMOTE_ADDR"] + + for key in keys: + if key in environ: + yield key, environ[key] + + +# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` +# +# We need this function because Django does not give us a "pure" http header +# dict. So we might as well use it for all WSGI integrations. +def _get_headers(environ): + # type: (Dict[str, str]) -> Iterator[Tuple[str, str]] + """ + Returns only proper HTTP headers. + + """ + for key, value in iteritems(environ): + key = str(key) + if key.startswith("HTTP_") and key not in ( + "HTTP_CONTENT_TYPE", + "HTTP_CONTENT_LENGTH", + ): + yield key[5:].replace("_", "-").title(), value + elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): + yield key.replace("_", "-").title(), value + + +def get_client_ip(environ): + # type: (Dict[str, str]) -> Optional[Any] + """ + Infer the user IP address from various headers. This cannot be used in + security sensitive situations since the value may be forged from a client, + but it's good enough for the event payload. + """ + try: + return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() + except (KeyError, IndexError): + pass + + try: + return environ["HTTP_X_REAL_IP"] + except KeyError: + pass + + return environ.get("REMOTE_ADDR") + + +def _capture_exception(hub): + # type: (Hub) -> ExcInfo + exc_info = sys.exc_info() + + # Check client here as it might have been unset while streaming response + if hub.client is not None: + e = exc_info[1] + + # SystemExit(0) is the only uncaught exception that is expected behavior + should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) + if not should_skip_capture: + event, hint = event_from_exception( + exc_info, + client_options=hub.client.options, + mechanism={"type": "wsgi", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + return exc_info + + +class _ScopedResponse(object): + __slots__ = ("_response", "_hub") + + def __init__(self, hub, response): + # type: (Hub, Iterator[bytes]) -> None + self._hub = hub + self._response = response + + def __iter__(self): + # type: () -> Iterator[bytes] + iterator = iter(self._response) + + while True: + with self._hub: + try: + chunk = next(iterator) + except StopIteration: + break + except BaseException: + reraise(*_capture_exception(self._hub)) + + yield chunk + + def close(self): + # type: () -> None + with self._hub: + try: + self._response.close() # type: ignore + except AttributeError: + pass + except BaseException: + reraise(*_capture_exception(self._hub)) + + +def _make_wsgi_event_processor(environ): + # type: (Dict[str, str]) -> EventProcessor + # It's a bit unfortunate that we have to extract and parse the request data + # from the environ so eagerly, but there are a few good reasons for this. + # + # We might be in a situation where the scope/hub never gets torn down + # properly. In that case we will have an unnecessary strong reference to + # all objects in the environ (some of which may take a lot of memory) when + # we're really just interested in a few of them. + # + # Keeping the environment around for longer than the request lifecycle is + # also not necessarily something uWSGI can deal with: + # https://github.com/unbit/uwsgi/issues/1950 + + client_ip = get_client_ip(environ) + request_url = get_request_url(environ) + query_string = environ.get("QUERY_STRING") + method = environ.get("REQUEST_METHOD") + env = dict(_get_environ(environ)) + headers = _filter_headers(dict(_get_headers(environ))) + + def event_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + with capture_internal_exceptions(): + # if the code below fails halfway through we at least have some data + request_info = event.setdefault("request", {}) + + if _should_send_default_pii(): + user_info = event.setdefault("user", {}) + if client_ip: + user_info.setdefault("ip_address", client_ip) + + request_info["url"] = request_url + request_info["query_string"] = query_string + request_info["method"] = method + request_info["env"] = env + request_info["headers"] = headers + + return event + + return event_processor |