Skip to content

Commit

Permalink
Configure HTTP methods to capture in WSGI middleware and frameworks (#…
Browse files Browse the repository at this point in the history
…3531)

- Do not capture transactions for OPTIONS and HEAD HTTP methods by default.
- Make it possible with an `http_methods_to_capture` config option for Django, Flask, Starlette, and FastAPI to specify what HTTP methods to capture.
  • Loading branch information
antonpirker authored Oct 1, 2024
1 parent a3ab1ea commit 1c64ff7
Show file tree
Hide file tree
Showing 12 changed files with 477 additions and 72 deletions.
21 changes: 21 additions & 0 deletions sentry_sdk/integrations/_wsgi_common.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from contextlib import contextmanager
import json
from copy import deepcopy

Expand All @@ -15,6 +16,7 @@
if TYPE_CHECKING:
from typing import Any
from typing import Dict
from typing import Iterator
from typing import Mapping
from typing import MutableMapping
from typing import Optional
Expand All @@ -37,6 +39,25 @@
x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_")
)

DEFAULT_HTTP_METHODS_TO_CAPTURE = (
"CONNECT",
"DELETE",
"GET",
# "HEAD", # do not capture HEAD requests by default
# "OPTIONS", # do not capture OPTIONS requests by default
"PATCH",
"POST",
"PUT",
"TRACE",
)


# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support
@contextmanager
def nullcontext():
# type: () -> Iterator[None]
yield


def request_body_within_bounds(client, content_length):
# type: (Optional[sentry_sdk.client.BaseClient], int) -> bool
Expand Down
100 changes: 57 additions & 43 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
_get_request_data,
_get_url,
)
from sentry_sdk.integrations._wsgi_common import (
DEFAULT_HTTP_METHODS_TO_CAPTURE,
nullcontext,
)
from sentry_sdk.sessions import track_session
from sentry_sdk.tracing import (
SOURCE_FOR_STYLE,
Expand Down Expand Up @@ -89,17 +93,19 @@ class SentryAsgiMiddleware:
"transaction_style",
"mechanism_type",
"span_origin",
"http_methods_to_capture",
)

def __init__(
self,
app,
unsafe_context_data=False,
transaction_style="endpoint",
mechanism_type="asgi",
span_origin="manual",
app, # type: Any
unsafe_context_data=False, # type: bool
transaction_style="endpoint", # type: str
mechanism_type="asgi", # type: str
span_origin="manual", # type: str
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
):
# type: (Any, bool, str, str, str) -> None
# type: (...) -> None
"""
Instrument an ASGI application with Sentry. Provides HTTP/websocket
data to sent events and basic handling for exceptions bubbling up
Expand Down Expand Up @@ -134,6 +140,7 @@ def __init__(
self.mechanism_type = mechanism_type
self.span_origin = span_origin
self.app = app
self.http_methods_to_capture = http_methods_to_capture

if _looks_like_asgi3(app):
self.__call__ = self._run_asgi3 # type: Callable[..., Any]
Expand Down Expand Up @@ -185,52 +192,59 @@ async def _run_app(self, scope, receive, send, asgi_version):
scope,
)

if ty in ("http", "websocket"):
transaction = continue_trace(
_get_headers(scope),
op="{}.server".format(ty),
name=transaction_name,
source=transaction_source,
origin=self.span_origin,
)
logger.debug(
"[ASGI] Created transaction (continuing trace): %s",
transaction,
)
else:
transaction = Transaction(
op=OP.HTTP_SERVER,
name=transaction_name,
source=transaction_source,
origin=self.span_origin,
)
method = scope.get("method", "").upper()
transaction = None
if method in self.http_methods_to_capture:
if ty in ("http", "websocket"):
transaction = continue_trace(
_get_headers(scope),
op="{}.server".format(ty),
name=transaction_name,
source=transaction_source,
origin=self.span_origin,
)
logger.debug(
"[ASGI] Created transaction (continuing trace): %s",
transaction,
)
else:
transaction = Transaction(
op=OP.HTTP_SERVER,
name=transaction_name,
source=transaction_source,
origin=self.span_origin,
)
logger.debug(
"[ASGI] Created transaction (new): %s", transaction
)

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

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

with sentry_sdk.start_transaction(
transaction,
custom_sampling_context={"asgi_scope": scope},
with (
sentry_sdk.start_transaction(
transaction,
custom_sampling_context={"asgi_scope": scope},
)
if transaction is not None
else nullcontext()
):
logger.debug("[ASGI] Started transaction: %s", transaction)
try:

async def _sentry_wrapped_send(event):
# type: (Dict[str, Any]) -> Any
is_http_response = (
event.get("type") == "http.response.start"
and transaction is not None
and "status" in event
)
if is_http_response:
transaction.set_http_status(event["status"])
if transaction is not None:
is_http_response = (
event.get("type") == "http.response.start"
and "status" in event
)
if is_http_response:
transaction.set_http_status(event["status"])

return await send(event)

Expand Down
27 changes: 20 additions & 7 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
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
from sentry_sdk.integrations._wsgi_common import (
DEFAULT_HTTP_METHODS_TO_CAPTURE,
RequestExtractor,
)

try:
from django import VERSION as DJANGO_VERSION
Expand Down Expand Up @@ -125,13 +128,14 @@ class DjangoIntegration(Integration):

def __init__(
self,
transaction_style="url",
middleware_spans=True,
signals_spans=True,
cache_spans=False,
signals_denylist=None,
transaction_style="url", # type: str
middleware_spans=True, # type: bool
signals_spans=True, # type: bool
cache_spans=False, # type: bool
signals_denylist=None, # type: Optional[list[signals.Signal]]
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
):
# type: (str, bool, bool, bool, Optional[list[signals.Signal]]) -> None
# type: (...) -> None
if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
Expand All @@ -145,6 +149,8 @@ def __init__(

self.cache_spans = cache_spans

self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))

@staticmethod
def setup_once():
# type: () -> None
Expand Down Expand Up @@ -172,10 +178,17 @@ def sentry_patched_wsgi_handler(self, environ, start_response):

use_x_forwarded_for = settings.USE_X_FORWARDED_HOST

integration = sentry_sdk.get_client().get_integration(DjangoIntegration)

middleware = SentryWsgiMiddleware(
bound_old_app,
use_x_forwarded_for,
span_origin=DjangoIntegration.origin,
http_methods_to_capture=(
integration.http_methods_to_capture
if integration
else DEFAULT_HTTP_METHODS_TO_CAPTURE
),
)
return middleware(environ, start_response)

Expand Down
21 changes: 18 additions & 3 deletions sentry_sdk/integrations/flask.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import sentry_sdk
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations._wsgi_common import (
DEFAULT_HTTP_METHODS_TO_CAPTURE,
RequestExtractor,
)
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.tracing import SOURCE_FOR_STYLE
Expand Down Expand Up @@ -52,14 +55,19 @@ class FlaskIntegration(Integration):

transaction_style = ""

def __init__(self, transaction_style="endpoint"):
# type: (str) -> None
def __init__(
self,
transaction_style="endpoint", # type: str
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.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))

@staticmethod
def setup_once():
Expand All @@ -83,9 +91,16 @@ def sentry_patched_wsgi_app(self, environ, start_response):
if sentry_sdk.get_client().get_integration(FlaskIntegration) is None:
return old_app(self, environ, start_response)

integration = sentry_sdk.get_client().get_integration(FlaskIntegration)

middleware = SentryWsgiMiddleware(
lambda *a, **kw: old_app(self, *a, **kw),
span_origin=FlaskIntegration.origin,
http_methods_to_capture=(
integration.http_methods_to_capture
if integration
else DEFAULT_HTTP_METHODS_TO_CAPTURE
),
)
return middleware(environ, start_response)

Expand Down
8 changes: 8 additions & 0 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
_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,
Expand Down Expand Up @@ -85,6 +86,7 @@ def __init__(
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:
Expand All @@ -94,6 +96,7 @@ def __init__(
)
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 = (
Expand Down Expand Up @@ -390,6 +393,11 @@ async def _sentry_patched_asgi_app(self, scope, receive, send):
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
),
)

middleware.__call__ = middleware._run_asgi3
Expand Down
Loading

0 comments on commit 1c64ff7

Please sign in to comment.