| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737 |
- import asyncio
- import functools
- import warnings
- from collections.abc import Set
- from copy import deepcopy
- from json import JSONDecodeError
- import sentry_sdk
- from sentry_sdk.consts import OP
- from sentry_sdk.integrations import (
- DidNotEnable,
- Integration,
- _DEFAULT_FAILED_REQUEST_STATUS_CODES,
- )
- from sentry_sdk.integrations._wsgi_common import (
- DEFAULT_HTTP_METHODS_TO_CAPTURE,
- HttpCodeRangeContainer,
- _is_json_content_type,
- request_body_within_bounds,
- )
- from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
- from sentry_sdk.scope import should_send_default_pii
- from sentry_sdk.tracing import (
- SOURCE_FOR_STYLE,
- TransactionSource,
- )
- from sentry_sdk.utils import (
- AnnotatedValue,
- capture_internal_exceptions,
- ensure_integration_enabled,
- event_from_exception,
- parse_version,
- transaction_from_function,
- )
- from typing import TYPE_CHECKING
- if TYPE_CHECKING:
- from typing import Any, Awaitable, Callable, Container, Dict, Optional, Tuple, Union
- from sentry_sdk._types import Event, HttpStatusCodeRange
- try:
- import starlette # type: ignore
- from starlette import __version__ as STARLETTE_VERSION
- from starlette.applications import Starlette # type: ignore
- from starlette.datastructures import UploadFile # type: ignore
- from starlette.middleware import Middleware # type: ignore
- from starlette.middleware.authentication import ( # type: ignore
- AuthenticationMiddleware,
- )
- from starlette.requests import Request # type: ignore
- from starlette.routing import Match # type: ignore
- from starlette.types import ASGIApp, Receive, Scope as StarletteScope, Send # type: ignore
- except ImportError:
- raise DidNotEnable("Starlette is not installed")
- try:
- # Starlette 0.20
- from starlette.middleware.exceptions import ExceptionMiddleware # type: ignore
- except ImportError:
- # Startlette 0.19.1
- from starlette.exceptions import ExceptionMiddleware # type: ignore
- try:
- # Optional dependency of Starlette to parse form data.
- try:
- # python-multipart 0.0.13 and later
- import python_multipart as multipart # type: ignore
- except ImportError:
- # python-multipart 0.0.12 and earlier
- import multipart # type: ignore
- except ImportError:
- multipart = None
- _DEFAULT_TRANSACTION_NAME = "generic Starlette request"
- TRANSACTION_STYLE_VALUES = ("endpoint", "url")
- class StarletteIntegration(Integration):
- identifier = "starlette"
- origin = f"auto.http.{identifier}"
- transaction_style = ""
- def __init__(
- self,
- transaction_style="url", # type: str
- failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Union[Set[int], list[HttpStatusCodeRange], None]
- middleware_spans=True, # type: bool
- http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
- ):
- # type: (...) -> 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
- self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
- if isinstance(failed_request_status_codes, Set):
- self.failed_request_status_codes = failed_request_status_codes # type: Container[int]
- else:
- warnings.warn(
- "Passing a list or None for failed_request_status_codes is deprecated. "
- "Please pass a set of int instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- if failed_request_status_codes is None:
- self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES
- else:
- self.failed_request_status_codes = HttpCodeRangeContainer(
- failed_request_status_codes
- )
- @staticmethod
- def setup_once():
- # type: () -> None
- version = parse_version(STARLETTE_VERSION)
- if version is None:
- raise DidNotEnable(
- "Unparsable Starlette version: {}".format(STARLETTE_VERSION)
- )
- patch_middlewares()
- patch_asgi_app()
- patch_request_response()
- if version >= (0, 24):
- patch_templates()
- def _enable_span_for_middleware(middleware_class):
- # type: (Any) -> type
- old_call = middleware_class.__call__
- async def _create_span_call(app, scope, receive, send, **kwargs):
- # type: (Any, Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]], Any) -> None
- integration = sentry_sdk.get_client().get_integration(StarletteIntegration)
- if integration is None or not integration.middleware_spans:
- return await old_call(app, scope, receive, send, **kwargs)
- middleware_name = app.__class__.__name__
- # Update transaction name with middleware name
- name, source = _get_transaction_from_middleware(app, scope, integration)
- if name is not None:
- sentry_sdk.get_current_scope().set_transaction_name(
- name,
- source=source,
- )
- with sentry_sdk.start_span(
- op=OP.MIDDLEWARE_STARLETTE,
- name=middleware_name,
- origin=StarletteIntegration.origin,
- ) as middleware_span:
- middleware_span.set_tag("starlette.middleware_name", middleware_name)
- # Creating spans for the "receive" callback
- async def _sentry_receive(*args, **kwargs):
- # type: (*Any, **Any) -> Any
- with sentry_sdk.start_span(
- op=OP.MIDDLEWARE_STARLETTE_RECEIVE,
- name=getattr(receive, "__qualname__", str(receive)),
- origin=StarletteIntegration.origin,
- ) as span:
- span.set_tag("starlette.middleware_name", middleware_name)
- return await receive(*args, **kwargs)
- receive_name = getattr(receive, "__name__", str(receive))
- receive_patched = receive_name == "_sentry_receive"
- new_receive = _sentry_receive if not receive_patched else receive
- # Creating spans for the "send" callback
- async def _sentry_send(*args, **kwargs):
- # type: (*Any, **Any) -> Any
- with sentry_sdk.start_span(
- op=OP.MIDDLEWARE_STARLETTE_SEND,
- name=getattr(send, "__qualname__", str(send)),
- origin=StarletteIntegration.origin,
- ) as span:
- span.set_tag("starlette.middleware_name", middleware_name)
- return await send(*args, **kwargs)
- send_name = getattr(send, "__name__", str(send))
- send_patched = send_name == "_sentry_send"
- new_send = _sentry_send if not send_patched else send
- return await old_call(app, scope, new_receive, new_send, **kwargs)
- not_yet_patched = old_call.__name__ not in [
- "_create_span_call",
- "_sentry_authenticationmiddleware_call",
- "_sentry_exceptionmiddleware_call",
- ]
- if not_yet_patched:
- middleware_class.__call__ = _create_span_call
- return middleware_class
- @ensure_integration_enabled(StarletteIntegration)
- def _capture_exception(exception, handled=False):
- # type: (BaseException, **Any) -> None
- event, hint = event_from_exception(
- exception,
- client_options=sentry_sdk.get_client().options,
- mechanism={"type": StarletteIntegration.identifier, "handled": handled},
- )
- sentry_sdk.capture_event(event, hint=hint)
- def patch_exception_middleware(middleware_class):
- # type: (Any) -> None
- """
- Capture all exceptions in Starlette app and
- also extract user information.
- """
- old_middleware_init = middleware_class.__init__
- not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init)
- if not_yet_patched:
- def _sentry_middleware_init(self, *args, **kwargs):
- # type: (Any, Any, Any) -> None
- old_middleware_init(self, *args, **kwargs)
- # Patch existing exception handlers
- old_handlers = self._exception_handlers.copy()
- async def _sentry_patched_exception_handler(self, *args, **kwargs):
- # type: (Any, Any, Any) -> None
- integration = sentry_sdk.get_client().get_integration(
- StarletteIntegration
- )
- exp = args[0]
- if integration is not None:
- is_http_server_error = (
- hasattr(exp, "status_code")
- and isinstance(exp.status_code, int)
- and exp.status_code in integration.failed_request_status_codes
- )
- if is_http_server_error:
- _capture_exception(exp, handled=True)
- # Find a matching handler
- old_handler = None
- for cls in type(exp).__mro__:
- if cls in old_handlers:
- old_handler = old_handlers[cls]
- break
- if old_handler is None:
- return
- if _is_async_callable(old_handler):
- return await old_handler(self, *args, **kwargs)
- else:
- return old_handler(self, *args, **kwargs)
- for key in self._exception_handlers.keys():
- self._exception_handlers[key] = _sentry_patched_exception_handler
- middleware_class.__init__ = _sentry_middleware_init
- old_call = middleware_class.__call__
- async def _sentry_exceptionmiddleware_call(self, scope, receive, send):
- # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None
- # Also add the user (that was eventually set by be Authentication middle
- # that was called before this middleware). This is done because the authentication
- # middleware sets the user in the scope and then (in the same function)
- # calls this exception middelware. In case there is no exception (or no handler
- # for the type of exception occuring) then the exception bubbles up and setting the
- # user information into the sentry scope is done in auth middleware and the
- # ASGI middleware will then send everything to Sentry and this is fine.
- # But if there is an exception happening that the exception middleware here
- # has a handler for, it will send the exception directly to Sentry, so we need
- # the user information right now.
- # This is why we do it here.
- _add_user_to_sentry_scope(scope)
- await old_call(self, scope, receive, send)
- middleware_class.__call__ = _sentry_exceptionmiddleware_call
- @ensure_integration_enabled(StarletteIntegration)
- def _add_user_to_sentry_scope(scope):
- # type: (Dict[str, Any]) -> None
- """
- Extracts user information from the ASGI scope and
- adds it to Sentry's scope.
- """
- if "user" not in scope:
- return
- if not should_send_default_pii():
- return
- user_info = {} # type: Dict[str, Any]
- starlette_user = scope["user"]
- username = getattr(starlette_user, "username", None)
- if username:
- user_info.setdefault("username", starlette_user.username)
- user_id = getattr(starlette_user, "id", None)
- if user_id:
- user_info.setdefault("id", starlette_user.id)
- email = getattr(starlette_user, "email", None)
- if email:
- user_info.setdefault("email", starlette_user.email)
- sentry_scope = sentry_sdk.get_isolation_scope()
- sentry_scope.user = user_info
- def patch_authentication_middleware(middleware_class):
- # type: (Any) -> None
- """
- Add user information to Sentry scope.
- """
- old_call = middleware_class.__call__
- not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call)
- if not_yet_patched:
- async def _sentry_authenticationmiddleware_call(self, scope, receive, send):
- # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None
- await old_call(self, scope, receive, send)
- _add_user_to_sentry_scope(scope)
- middleware_class.__call__ = _sentry_authenticationmiddleware_call
- def patch_middlewares():
- # type: () -> None
- """
- Patches Starlettes `Middleware` class to record
- spans for every middleware invoked.
- """
- old_middleware_init = Middleware.__init__
- not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init)
- if not_yet_patched:
- def _sentry_middleware_init(self, cls, *args, **kwargs):
- # type: (Any, Any, Any, Any) -> None
- if cls == SentryAsgiMiddleware:
- return old_middleware_init(self, cls, *args, **kwargs)
- span_enabled_cls = _enable_span_for_middleware(cls)
- old_middleware_init(self, span_enabled_cls, *args, **kwargs)
- if cls == AuthenticationMiddleware:
- patch_authentication_middleware(cls)
- if cls == ExceptionMiddleware:
- patch_exception_middleware(cls)
- Middleware.__init__ = _sentry_middleware_init
- def patch_asgi_app():
- # type: () -> None
- """
- Instrument Starlette ASGI app using the SentryAsgiMiddleware.
- """
- old_app = Starlette.__call__
- async def _sentry_patched_asgi_app(self, scope, receive, send):
- # type: (Starlette, StarletteScope, Receive, Send) -> None
- integration = sentry_sdk.get_client().get_integration(StarletteIntegration)
- if integration is None:
- return await old_app(self, scope, receive, send)
- middleware = SentryAsgiMiddleware(
- lambda *a, **kw: old_app(self, *a, **kw),
- mechanism_type=StarletteIntegration.identifier,
- transaction_style=integration.transaction_style,
- span_origin=StarletteIntegration.origin,
- http_methods_to_capture=(
- integration.http_methods_to_capture
- if integration
- else DEFAULT_HTTP_METHODS_TO_CAPTURE
- ),
- asgi_version=3,
- )
- return await middleware(scope, receive, send)
- Starlette.__call__ = _sentry_patched_asgi_app
- # This was vendored in from Starlette to support Starlette 0.19.1 because
- # this function was only introduced in 0.20.x
- def _is_async_callable(obj):
- # type: (Any) -> bool
- while isinstance(obj, functools.partial):
- obj = obj.func
- return asyncio.iscoroutinefunction(obj) or (
- callable(obj) and asyncio.iscoroutinefunction(obj.__call__)
- )
- def patch_request_response():
- # type: () -> None
- old_request_response = starlette.routing.request_response
- def _sentry_request_response(func):
- # type: (Callable[[Any], Any]) -> ASGIApp
- old_func = func
- is_coroutine = _is_async_callable(old_func)
- if is_coroutine:
- async def _sentry_async_func(*args, **kwargs):
- # type: (*Any, **Any) -> Any
- integration = sentry_sdk.get_client().get_integration(
- StarletteIntegration
- )
- if integration is None:
- return await old_func(*args, **kwargs)
- request = args[0]
- _set_transaction_name_and_source(
- sentry_sdk.get_current_scope(),
- integration.transaction_style,
- request,
- )
- sentry_scope = sentry_sdk.get_isolation_scope()
- extractor = StarletteRequestExtractor(request)
- info = await extractor.extract_request_info()
- def _make_request_event_processor(req, integration):
- # type: (Any, Any) -> Callable[[Event, dict[str, Any]], Event]
- def event_processor(event, hint):
- # type: (Event, Dict[str, Any]) -> Event
- # Add info from request to event
- request_info = event.get("request", {})
- if info:
- if "cookies" in info:
- request_info["cookies"] = info["cookies"]
- if "data" in info:
- request_info["data"] = info["data"]
- event["request"] = deepcopy(request_info)
- return event
- return event_processor
- sentry_scope._name = StarletteIntegration.identifier
- sentry_scope.add_event_processor(
- _make_request_event_processor(request, integration)
- )
- return await old_func(*args, **kwargs)
- func = _sentry_async_func
- else:
- @functools.wraps(old_func)
- def _sentry_sync_func(*args, **kwargs):
- # type: (*Any, **Any) -> Any
- integration = sentry_sdk.get_client().get_integration(
- StarletteIntegration
- )
- if integration is None:
- return old_func(*args, **kwargs)
- current_scope = sentry_sdk.get_current_scope()
- if current_scope.transaction is not None:
- current_scope.transaction.update_active_thread()
- sentry_scope = sentry_sdk.get_isolation_scope()
- if sentry_scope.profile is not None:
- sentry_scope.profile.update_active_thread_id()
- request = args[0]
- _set_transaction_name_and_source(
- sentry_scope, integration.transaction_style, request
- )
- extractor = StarletteRequestExtractor(request)
- cookies = extractor.extract_cookies_from_request()
- def _make_request_event_processor(req, integration):
- # type: (Any, Any) -> Callable[[Event, dict[str, Any]], Event]
- def event_processor(event, hint):
- # type: (Event, dict[str, Any]) -> Event
- # Extract information from request
- request_info = event.get("request", {})
- if cookies:
- request_info["cookies"] = cookies
- event["request"] = deepcopy(request_info)
- return event
- return event_processor
- sentry_scope._name = StarletteIntegration.identifier
- sentry_scope.add_event_processor(
- _make_request_event_processor(request, integration)
- )
- return old_func(*args, **kwargs)
- func = _sentry_sync_func
- return old_request_response(func)
- starlette.routing.request_response = _sentry_request_response
- def patch_templates():
- # type: () -> None
- # If markupsafe is not installed, then Jinja2 is not installed
- # (markupsafe is a dependency of Jinja2)
- # In this case we do not need to patch the Jinja2Templates class
- try:
- from markupsafe import Markup
- except ImportError:
- return # Nothing to do
- from starlette.templating import Jinja2Templates # type: ignore
- old_jinja2templates_init = Jinja2Templates.__init__
- not_yet_patched = "_sentry_jinja2templates_init" not in str(
- old_jinja2templates_init
- )
- if not_yet_patched:
- def _sentry_jinja2templates_init(self, *args, **kwargs):
- # type: (Jinja2Templates, *Any, **Any) -> None
- def add_sentry_trace_meta(request):
- # type: (Request) -> Dict[str, Any]
- trace_meta = Markup(
- sentry_sdk.get_current_scope().trace_propagation_meta()
- )
- return {
- "sentry_trace_meta": trace_meta,
- }
- kwargs.setdefault("context_processors", [])
- if add_sentry_trace_meta not in kwargs["context_processors"]:
- kwargs["context_processors"].append(add_sentry_trace_meta)
- return old_jinja2templates_init(self, *args, **kwargs)
- Jinja2Templates.__init__ = _sentry_jinja2templates_init
- class StarletteRequestExtractor:
- """
- Extracts useful information from the Starlette request
- (like form data or cookies) and adds it to the Sentry event.
- """
- request = None # type: Request
- def __init__(self, request):
- # type: (StarletteRequestExtractor, Request) -> None
- self.request = request
- def extract_cookies_from_request(self):
- # type: (StarletteRequestExtractor) -> Optional[Dict[str, Any]]
- cookies = None # type: Optional[Dict[str, Any]]
- if should_send_default_pii():
- cookies = self.cookies()
- return cookies
- async def extract_request_info(self):
- # type: (StarletteRequestExtractor) -> Optional[Dict[str, Any]]
- client = sentry_sdk.get_client()
- request_info = {} # type: Dict[str, Any]
- with capture_internal_exceptions():
- # Add cookies
- if should_send_default_pii():
- request_info["cookies"] = self.cookies()
- # If there is no body, just return the cookies
- content_length = await self.content_length()
- if not content_length:
- return request_info
- # Add annotation if body is too big
- if content_length and not request_body_within_bounds(
- client, content_length
- ):
- request_info["data"] = AnnotatedValue.removed_because_over_size_limit()
- return request_info
- # Add JSON body, if it is a JSON request
- json = await self.json()
- if json:
- request_info["data"] = json
- return request_info
- # Add form as key/value pairs, if request has form data
- form = await self.form()
- if form:
- form_data = {}
- for key, val in form.items():
- is_file = isinstance(val, UploadFile)
- form_data[key] = (
- val
- if not is_file
- else AnnotatedValue.removed_because_raw_data()
- )
- request_info["data"] = form_data
- return request_info
- # Raw data, do not add body just an annotation
- request_info["data"] = AnnotatedValue.removed_because_raw_data()
- return request_info
- async def content_length(self):
- # type: (StarletteRequestExtractor) -> Optional[int]
- if "content-length" in self.request.headers:
- return int(self.request.headers["content-length"])
- return None
- def cookies(self):
- # type: (StarletteRequestExtractor) -> Dict[str, Any]
- return self.request.cookies
- async def form(self):
- # type: (StarletteRequestExtractor) -> Any
- if multipart is None:
- return None
- # Parse the body first to get it cached, as Starlette does not cache form() as it
- # does with body() and json() https://github.com/encode/starlette/discussions/1933
- # Calling `.form()` without calling `.body()` first will
- # potentially break the users project.
- await self.request.body()
- return await self.request.form()
- def is_json(self):
- # type: (StarletteRequestExtractor) -> bool
- return _is_json_content_type(self.request.headers.get("content-type"))
- async def json(self):
- # type: (StarletteRequestExtractor) -> Optional[Dict[str, Any]]
- if not self.is_json():
- return None
- try:
- return await self.request.json()
- except JSONDecodeError:
- return None
- def _transaction_name_from_router(scope):
- # type: (StarletteScope) -> Optional[str]
- router = scope.get("router")
- if not router:
- return None
- for route in router.routes:
- match = route.matches(scope)
- if match[0] == Match.FULL:
- try:
- return route.path
- except AttributeError:
- # routes added via app.host() won't have a path attribute
- return scope.get("path")
- return None
- def _set_transaction_name_and_source(scope, transaction_style, request):
- # type: (sentry_sdk.Scope, str, Any) -> None
- name = None
- source = SOURCE_FOR_STYLE[transaction_style]
- if transaction_style == "endpoint":
- endpoint = request.scope.get("endpoint")
- if endpoint:
- name = transaction_from_function(endpoint) or None
- elif transaction_style == "url":
- name = _transaction_name_from_router(request.scope)
- if name is None:
- name = _DEFAULT_TRANSACTION_NAME
- source = TransactionSource.ROUTE
- scope.set_transaction_name(name, source=source)
- def _get_transaction_from_middleware(app, asgi_scope, integration):
- # type: (Any, Dict[str, Any], StarletteIntegration) -> Tuple[Optional[str], Optional[str]]
- name = None
- source = None
- if integration.transaction_style == "endpoint":
- name = transaction_from_function(app.__class__)
- source = TransactionSource.COMPONENT
- elif integration.transaction_style == "url":
- name = _transaction_name_from_router(asgi_scope)
- source = TransactionSource.ROUTE
- return name, source
|