Skip to content

Commit

Permalink
Cleanup ASGI integration (#2335)
Browse files Browse the repository at this point in the history
This does not change behaviour/functionality. Some smaller refactoring to make it easier to work on ASGI (and probably Starlette) integration
  • Loading branch information
antonpirker authored and sentrivana committed Sep 18, 2023
1 parent 1f4fa1e commit d91b241
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 128 deletions.
104 changes: 104 additions & 0 deletions sentry_sdk/integrations/_asgi_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import urllib

from sentry_sdk.hub import _should_send_default_pii
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk._types import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any
from typing import Dict
from typing import Optional
from typing_extensions import Literal


def _get_headers(asgi_scope):
# type: (Any) -> Dict[str, str]
"""
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 asgi_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_url(asgi_scope, default_scheme, host):
# type: (Dict[str, Any], Literal["ws", "http"], Optional[str]) -> str
"""
Extract URL from the ASGI scope, without also including the querystring.
"""
scheme = asgi_scope.get("scheme", default_scheme)

server = asgi_scope.get("server", None)
path = asgi_scope.get("root_path", "") + asgi_scope.get("path", "")

if host:
return "%s://%s%s" % (scheme, host, 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(asgi_scope):
# type: (Any) -> Any
"""
Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
"""
qs = asgi_scope.get("query_string")
if not qs:
return None
return urllib.parse.unquote(qs.decode("latin-1"))


def _get_ip(asgi_scope):
# type: (Any) -> str
"""
Extract IP Address from the ASGI scope based on request headers with fallback to scope client.
"""
headers = _get_headers(asgi_scope)
try:
return headers["x-forwarded-for"].split(",")[0].strip()
except (KeyError, IndexError):
pass

try:
return headers["x-real-ip"]
except KeyError:
pass

return asgi_scope.get("client")[0]


def _get_request_data(asgi_scope):
# type: (Any) -> Dict[str, Any]
"""
Returns data related to the HTTP request from the ASGI scope.
"""
request_data = {} # type: Dict[str, Any]
ty = asgi_scope["type"]
if ty in ("http", "websocket"):
request_data["method"] = asgi_scope.get("method")

request_data["headers"] = headers = _filter_headers(_get_headers(asgi_scope))
request_data["query_string"] = _get_query(asgi_scope)

request_data["url"] = _get_url(
asgi_scope, "http" if ty == "http" else "ws", headers.get("host")
)

client = asgi_scope.get("client")
if client and _should_send_default_pii():
request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)}

return request_data
124 changes: 34 additions & 90 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@

import asyncio
import inspect
import urllib
from copy import deepcopy

from sentry_sdk._functools import partial
from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk.api import continue_trace
from sentry_sdk.consts import OP
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.hub import Hub

from sentry_sdk.integrations._asgi_common import (
_get_headers,
_get_request_data,
)
from sentry_sdk.integrations.modules import _get_installed_modules
from sentry_sdk.sessions import auto_session_tracking
from sentry_sdk.tracing import (
Expand All @@ -37,8 +40,6 @@
from typing import Optional
from typing import Callable

from typing_extensions import Literal

from sentry_sdk._types import Event, Hint


Expand Down Expand Up @@ -169,19 +170,32 @@ async def _run_app(self, scope, receive, send, asgi_version):

if ty in ("http", "websocket"):
transaction = continue_trace(
self._get_headers(scope),
_get_headers(scope),
op="{}.server".format(ty),
)
logger.debug(
"[ASGI] Created transaction (continuing trace): %s",
transaction,
)
else:
transaction = Transaction(op=OP.HTTP_SERVER)
logger.debug(
"[ASGI] Created transaction (new): %s", transaction
)

transaction.name = _DEFAULT_TRANSACTION_NAME
transaction.source = TRANSACTION_SOURCE_ROUTE
transaction.set_tag("asgi.type", ty)
logger.debug(
"[ASGI] Set transaction name and source on transaction: '%s' / '%s'",
transaction.name,
transaction.source,
)

with hub.start_transaction(
transaction, custom_sampling_context={"asgi_scope": scope}
):
logger.debug("[ASGI] Started transaction: %s", transaction)
try:

async def _sentry_wrapped_send(event):
Expand Down Expand Up @@ -214,31 +228,15 @@ async def _sentry_wrapped_send(event):

def event_processor(self, event, hint, asgi_scope):
# type: (Event, Hint, Any) -> Optional[Event]
request_info = event.get("request", {})

ty = asgi_scope["type"]
if ty in ("http", "websocket"):
request_info["method"] = asgi_scope.get("method")
request_info["headers"] = headers = _filter_headers(
self._get_headers(asgi_scope)
)
request_info["query_string"] = self._get_query(asgi_scope)

request_info["url"] = self._get_url(
asgi_scope, "http" if ty == "http" else "ws", headers.get("host")
)

client = asgi_scope.get("client")
if client and _should_send_default_pii():
request_info["env"] = {"REMOTE_ADDR": self._get_ip(asgi_scope)}
request_data = event.get("request", {})
request_data.update(_get_request_data(asgi_scope))
event["request"] = deepcopy(request_data)

self._set_transaction_name_and_source(event, self.transaction_style, asgi_scope)

event["request"] = deepcopy(request_info)

return event

# Helper functions for extracting request data.
# Helper functions.
#
# Note: Those functions are not public API. If you want to mutate request
# data to your liking it's recommended to use the `before_send` callback
Expand Down Expand Up @@ -275,71 +273,17 @@ def _set_transaction_name_and_source(self, event, transaction_style, asgi_scope)
if not name:
event["transaction"] = _DEFAULT_TRANSACTION_NAME
event["transaction_info"] = {"source": TRANSACTION_SOURCE_ROUTE}
logger.debug(
"[ASGI] Set default transaction name and source on event: '%s' / '%s'",
event["transaction"],
event["transaction_info"]["source"],
)
return

event["transaction"] = name
event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]}

def _get_url(self, scope, default_scheme, host):
# type: (Dict[str, Any], Literal["ws", "http"], Optional[str]) -> str
"""
Extract URL from the ASGI scope, without also including the querystring.
"""
scheme = scope.get("scheme", default_scheme)

server = scope.get("server", None)
path = scope.get("root_path", "") + scope.get("path", "")

if host:
return "%s://%s%s" % (scheme, host, 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.
"""
qs = scope.get("query_string")
if not qs:
return None
return urllib.parse.unquote(qs.decode("latin-1"))

def _get_ip(self, scope):
# type: (Any) -> str
"""
Extract IP Address from the ASGI scope based on request headers with fallback to scope client.
"""
headers = self._get_headers(scope)
try:
return headers["x-forwarded-for"].split(",")[0].strip()
except (KeyError, IndexError):
pass

try:
return headers["x-real-ip"]
except KeyError:
pass

return scope.get("client")[0]

def _get_headers(self, scope):
# type: (Any) -> Dict[str, str]
"""
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
logger.debug(
"[ASGI] Set transaction name and source on event: '%s' / '%s'",
event["transaction"],
event["transaction_info"]["source"],
)
5 changes: 4 additions & 1 deletion sentry_sdk/integrations/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE
from sentry_sdk.utils import transaction_from_function
from sentry_sdk.utils import transaction_from_function, logger

if TYPE_CHECKING:
from typing import Any, Callable, Dict
Expand Down Expand Up @@ -60,6 +60,9 @@ def _set_transaction_name_and_source(scope, transaction_style, request):
source = SOURCE_FOR_STYLE[transaction_style]

scope.set_transaction_name(name, source=source)
logger.debug(
"[FastAPI] Set transaction name and source on scope: %s / %s", name, source
)


def patch_get_request_handler():
Expand Down
4 changes: 4 additions & 0 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
AnnotatedValue,
capture_internal_exceptions,
event_from_exception,
logger,
parse_version,
transaction_from_function,
)
Expand Down Expand Up @@ -648,3 +649,6 @@ def _set_transaction_name_and_source(scope, transaction_style, request):
source = SOURCE_FOR_STYLE[transaction_style]

scope.set_transaction_name(name, source=source)
logger.debug(
"[Starlette] Set transaction name and source on scope: %s / %s", name, source
)
Loading

0 comments on commit d91b241

Please sign in to comment.