diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py | 357 |
1 files changed, 357 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py new file mode 100644 index 00000000..ad3202bf --- /dev/null +++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/aiohttp.py @@ -0,0 +1,357 @@ +import sys +import weakref +from functools import wraps + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANSTATUS, SPANDATA +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + _check_minimum_version, + Integration, + DidNotEnable, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.sessions import track_session +from sentry_sdk.integrations._wsgi_common import ( + _filter_headers, + request_body_within_bounds, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SOURCE_FOR_STYLE, + TransactionSource, +) +from sentry_sdk.tracing_utils import should_propagate_trace +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + parse_url, + parse_version, + reraise, + transaction_from_function, + HAS_REAL_CONTEXTVARS, + CONTEXTVARS_ERROR_MESSAGE, + SENSITIVE_DATA_SUBSTITUTE, + AnnotatedValue, +) + +try: + import asyncio + + from aiohttp import __version__ as AIOHTTP_VERSION + from aiohttp import ClientSession, TraceConfig + from aiohttp.web import Application, HTTPException, UrlDispatcher +except ImportError: + raise DidNotEnable("AIOHTTP not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from aiohttp.web_request import Request + from aiohttp.web_urldispatcher import UrlMappingMatchInfo + from aiohttp import TraceRequestStartParams, TraceRequestEndParams + + from collections.abc import Set + from types import SimpleNamespace + from typing import Any + from typing import Optional + from typing import Tuple + from typing import Union + + from sentry_sdk.utils import ExcInfo + from sentry_sdk._types import Event, EventProcessor + + +TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern") + + +class AioHttpIntegration(Integration): + identifier = "aiohttp" + origin = f"auto.http.{identifier}" + + def __init__( + self, + transaction_style="handler_name", # type: str + *, + failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Set[int] + ): + # 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._failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once(): + # type: () -> None + + version = parse_version(AIOHTTP_VERSION) + _check_minimum_version(AioHttpIntegration, version) + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The aiohttp integration for Sentry requires Python 3.7+ " + " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + + ignore_logger("aiohttp.server") + + old_handle = Application._handle + + async def sentry_app_handle(self, request, *args, **kwargs): + # type: (Any, Request, *Any, **Any) -> Any + integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) + if integration is None: + return await old_handle(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + + with sentry_sdk.isolation_scope() as scope: + with track_session(scope, session_mode="request"): + # Scope data will not leak between requests because aiohttp + # create a task to wrap each request. + scope.generate_propagation_context() + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + headers = dict(request.headers) + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # If this transaction name makes it to the UI, AIOHTTP's + # URL resolver did not find a route or died trying. + name="generic AIOHTTP request", + source=TransactionSource.ROUTE, + origin=AioHttpIntegration.origin, + ) + with sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"aiohttp_request": request}, + ): + try: + response = await old_handle(self, request) + except HTTPException as e: + transaction.set_http_status(e.status_code) + + if ( + e.status_code + in integration._failed_request_status_codes + ): + _capture_exception() + + raise + except (asyncio.CancelledError, ConnectionResetError): + transaction.set_status(SPANSTATUS.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()) + + try: + # A valid response handler will return a valid response with a status. But, if the handler + # returns an invalid response (e.g. None), the line below will raise an AttributeError. + # Even though this is likely invalid, we need to handle this case to ensure we don't break + # the application. + response_status = response.status + except AttributeError: + pass + else: + transaction.set_http_status(response_status) + + return response + + Application._handle = sentry_app_handle + + old_urldispatcher_resolve = UrlDispatcher.resolve + + @wraps(old_urldispatcher_resolve) + async def sentry_urldispatcher_resolve(self, request): + # type: (UrlDispatcher, Request) -> UrlMappingMatchInfo + rv = await old_urldispatcher_resolve(self, request) + + integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) + if integration is None: + return rv + + name = None + + try: + if integration.transaction_style == "handler_name": + name = transaction_from_function(rv.handler) + elif integration.transaction_style == "method_and_path_pattern": + route_info = rv.get_info() + pattern = route_info.get("path") or route_info.get("formatter") + name = "{} {}".format(request.method, pattern) + except Exception: + pass + + if name is not None: + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=SOURCE_FOR_STYLE[integration.transaction_style], + ) + + return rv + + UrlDispatcher.resolve = sentry_urldispatcher_resolve + + old_client_session_init = ClientSession.__init__ + + @ensure_integration_enabled(AioHttpIntegration, old_client_session_init) + def init(*args, **kwargs): + # type: (Any, Any) -> None + client_trace_configs = list(kwargs.get("trace_configs") or ()) + trace_config = create_trace_config() + client_trace_configs.append(trace_config) + + kwargs["trace_configs"] = client_trace_configs + return old_client_session_init(*args, **kwargs) + + ClientSession.__init__ = init + + +def create_trace_config(): + # type: () -> TraceConfig + + async def on_request_start(session, trace_config_ctx, params): + # type: (ClientSession, SimpleNamespace, TraceRequestStartParams) -> None + if sentry_sdk.get_client().get_integration(AioHttpIntegration) is None: + return + + method = params.method.upper() + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(params.url), sanitize=False) + + span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), + origin=AioHttpIntegration.origin, + ) + span.set_data(SPANDATA.HTTP_METHOD, method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + client = sentry_sdk.get_client() + + if should_propagate_trace(client, str(params.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=params.url + ) + ) + if key == BAGGAGE_HEADER_NAME and params.headers.get( + BAGGAGE_HEADER_NAME + ): + # do not overwrite any existing baggage, just append to it + params.headers[key] += "," + value + else: + params.headers[key] = value + + trace_config_ctx.span = span + + async def on_request_end(session, trace_config_ctx, params): + # type: (ClientSession, SimpleNamespace, TraceRequestEndParams) -> None + if trace_config_ctx.span is None: + return + + span = trace_config_ctx.span + span.set_http_status(int(params.response.status)) + span.set_data("reason", params.response.reason) + span.finish() + + trace_config = TraceConfig() + + trace_config.on_request_start.append(on_request_start) + trace_config.on_request_end.append(on_request_end) + + return trace_config + + +def _make_request_processor(weak_request): + # type: (weakref.ReferenceType[Request]) -> EventProcessor + def aiohttp_processor( + event, # type: Event + hint, # type: dict[str, Tuple[type, BaseException, Any]] + ): + # type: (...) -> Event + 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} + 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(request) + + return event + + return aiohttp_processor + + +def _capture_exception(): + # type: () -> ExcInfo + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "aiohttp", "handled": False}, + ) + sentry_sdk.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(request): + # type: (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(sentry_sdk.get_client(), len(bytes_body)): + return AnnotatedValue.removed_because_over_size_limit() + + 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 |