| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- import sys
- import weakref
- from inspect import isawaitable
- from urllib.parse import urlsplit
- import sentry_sdk
- from sentry_sdk import continue_trace
- from sentry_sdk.consts import OP
- from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
- from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers
- from sentry_sdk.integrations.logging import ignore_logger
- from sentry_sdk.tracing import TransactionSource
- from sentry_sdk.utils import (
- capture_internal_exceptions,
- ensure_integration_enabled,
- event_from_exception,
- HAS_REAL_CONTEXTVARS,
- CONTEXTVARS_ERROR_MESSAGE,
- parse_version,
- reraise,
- )
- from typing import TYPE_CHECKING
- if TYPE_CHECKING:
- from collections.abc import Container
- from typing import Any
- from typing import Callable
- from typing import Optional
- from typing import Union
- from typing import Dict
- from sanic.request import Request, RequestParameters
- from sanic.response import BaseHTTPResponse
- from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint
- from sanic.router import Route
- 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")
- old_error_handler_lookup = ErrorHandler.lookup
- old_handle_request = Sanic.handle_request
- old_router_get = Router.get
- try:
- # This method was introduced in Sanic v21.9
- old_startup = Sanic._startup
- except AttributeError:
- pass
- class SanicIntegration(Integration):
- identifier = "sanic"
- origin = f"auto.http.{identifier}"
- version = None
- def __init__(
- self, unsampled_statuses: "Optional[Container[int]]" = frozenset({404})
- ) -> None:
- """
- The unsampled_statuses parameter can be used to specify for which HTTP statuses the
- transactions should not be sent to Sentry. By default, transactions are sent for all
- HTTP statuses, except 404. Set unsampled_statuses to None to send transactions for all
- HTTP statuses, including 404.
- """
- self._unsampled_statuses = unsampled_statuses or set()
- @staticmethod
- def setup_once() -> None:
- SanicIntegration.version = parse_version(SANIC_VERSION)
- _check_minimum_version(SanicIntegration, SanicIntegration.version)
- 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 the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE
- )
- 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")
- if SanicIntegration.version is not None and SanicIntegration.version < (21, 9):
- _setup_legacy_sanic()
- return
- _setup_sanic()
- class SanicRequestExtractor(RequestExtractor):
- def content_length(self) -> int:
- if self.request.body is None:
- return 0
- return len(self.request.body)
- def cookies(self) -> "Dict[str, str]":
- return dict(self.request.cookies)
- def raw_data(self) -> bytes:
- return self.request.body
- def form(self) -> "RequestParameters":
- return self.request.form
- def is_json(self) -> bool:
- raise NotImplementedError()
- def json(self) -> "Optional[Any]":
- return self.request.json
- def files(self) -> "RequestParameters":
- return self.request.files
- def size_of_file(self, file: "Any") -> int:
- return len(file.body or ())
- def _setup_sanic() -> None:
- Sanic._startup = _startup
- ErrorHandler.lookup = _sentry_error_handler_lookup
- def _setup_legacy_sanic() -> None:
- Sanic.handle_request = _legacy_handle_request
- Router.get = _legacy_router_get
- ErrorHandler.lookup = _sentry_error_handler_lookup
- async def _startup(self: "Sanic") -> None:
- # This happens about as early in the lifecycle as possible, just after the
- # Request object is created. The body has not yet been consumed.
- self.signal("http.lifecycle.request")(_context_enter)
- # This happens after the handler is complete. In v21.9 this signal is not
- # dispatched when there is an exception. Therefore we need to close out
- # and call _context_exit from the custom exception handler as well.
- # See https://github.com/sanic-org/sanic/issues/2297
- self.signal("http.lifecycle.response")(_context_exit)
- # This happens inside of request handling immediately after the route
- # has been identified by the router.
- self.signal("http.routing.after")(_set_transaction)
- # The above signals need to be declared before this can be called.
- await old_startup(self)
- async def _context_enter(request: "Request") -> None:
- request.ctx._sentry_do_integration = (
- sentry_sdk.get_client().get_integration(SanicIntegration) is not None
- )
- if not request.ctx._sentry_do_integration:
- return
- weak_request = weakref.ref(request)
- request.ctx._sentry_scope = sentry_sdk.isolation_scope()
- scope = request.ctx._sentry_scope.__enter__()
- scope.clear_breadcrumbs()
- scope.add_event_processor(_make_request_processor(weak_request))
- transaction = continue_trace(
- dict(request.headers),
- op=OP.HTTP_SERVER,
- # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction
- name=request.path,
- source=TransactionSource.URL,
- origin=SanicIntegration.origin,
- )
- request.ctx._sentry_transaction = sentry_sdk.start_transaction(
- transaction
- ).__enter__()
- async def _context_exit(
- request: "Request", response: "Optional[BaseHTTPResponse]" = None
- ) -> None:
- with capture_internal_exceptions():
- if not request.ctx._sentry_do_integration:
- return
- integration = sentry_sdk.get_client().get_integration(SanicIntegration)
- response_status = None if response is None else response.status
- # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception
- # happens while trying to end the transaction, we still attempt to exit the hub.
- with capture_internal_exceptions():
- request.ctx._sentry_transaction.set_http_status(response_status)
- request.ctx._sentry_transaction.sampled &= (
- isinstance(integration, SanicIntegration)
- and response_status not in integration._unsampled_statuses
- )
- request.ctx._sentry_transaction.__exit__(None, None, None)
- request.ctx._sentry_scope.__exit__(None, None, None)
- async def _set_transaction(request: "Request", route: "Route", **_: "Any") -> None:
- if request.ctx._sentry_do_integration:
- with capture_internal_exceptions():
- scope = sentry_sdk.get_current_scope()
- route_name = route.name.replace(request.app.name, "").strip(".")
- scope.set_transaction_name(route_name, source=TransactionSource.COMPONENT)
- def _sentry_error_handler_lookup(
- self: "Any", exception: Exception, *args: "Any", **kwargs: "Any"
- ) -> "Optional[object]":
- _capture_exception(exception)
- old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs)
- if old_error_handler is None:
- return None
- if sentry_sdk.get_client().get_integration(SanicIntegration) is None:
- return old_error_handler
- async def sentry_wrapped_error_handler(
- request: "Request", exception: 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)
- finally:
- # As mentioned in previous comment in _startup, this can be removed
- # after https://github.com/sanic-org/sanic/issues/2297 is resolved
- if SanicIntegration.version and SanicIntegration.version == (21, 9):
- await _context_exit(request)
- return sentry_wrapped_error_handler
- async def _legacy_handle_request(
- self: "Any", request: "Request", *args: "Any", **kwargs: "Any"
- ) -> "Any":
- if sentry_sdk.get_client().get_integration(SanicIntegration) is None:
- return await old_handle_request(self, request, *args, **kwargs)
- weak_request = weakref.ref(request)
- with sentry_sdk.isolation_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
- def _legacy_router_get(self: "Any", *args: "Union[Any, Request]") -> "Any":
- rv = old_router_get(self, *args)
- if sentry_sdk.get_client().get_integration(SanicIntegration) is not None:
- with capture_internal_exceptions():
- scope = sentry_sdk.get_isolation_scope()
- if SanicIntegration.version and SanicIntegration.version >= (21, 3):
- # Sanic versions above and including 21.3 append the app name to the
- # route name, and so we need to remove it from Route name so the
- # transaction name is consistent across all versions
- sanic_app_name = self.ctx.app.name
- sanic_route = rv[0].name
- if sanic_route.startswith("%s." % sanic_app_name):
- # We add a 1 to the len of the sanic_app_name because there is a dot
- # that joins app name and the route name
- # Format: app_name.route_name
- sanic_route = sanic_route[len(sanic_app_name) + 1 :]
- scope.set_transaction_name(
- sanic_route, source=TransactionSource.COMPONENT
- )
- else:
- scope.set_transaction_name(
- rv[0].__name__, source=TransactionSource.COMPONENT
- )
- return rv
- @ensure_integration_enabled(SanicIntegration)
- def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None:
- with capture_internal_exceptions():
- event, hint = event_from_exception(
- exception,
- client_options=sentry_sdk.get_client().options,
- mechanism={"type": "sanic", "handled": False},
- )
- if hint and hasattr(hint["exc_info"][0], "quiet") and hint["exc_info"][0].quiet:
- return
- sentry_sdk.capture_event(event, hint=hint)
- def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor":
- def sanic_processor(event: "Event", hint: "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 = 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
|