Skip to content

Commit

Permalink
feat(transactions): Transaction Source (#1490)
Browse files Browse the repository at this point in the history
Added transaction source (plus tests) to the following Integrations: 
Flask, ASGI, Bottle, Django, Celery, Falcon, Pyramid, Quart, Sanic, Tornado, AIOHTTP, Chalice, GCP, AWS Lambda,
  • Loading branch information
antonpirker authored Jul 15, 2022
1 parent b076a78 commit d4bc0f8
Show file tree
Hide file tree
Showing 31 changed files with 613 additions and 166 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer

- repo: https://github.com/psf/black
rev: stable
rev: 22.6.0
hooks:
- id: black

- repo: https://gitlab.com/pycqa/flake8
rev: 4.0.1
rev: 3.9.2
hooks:
- id: flake8

Expand Down
7 changes: 5 additions & 2 deletions sentry_sdk/integrations/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
_filter_headers,
request_body_within_bounds,
)
from sentry_sdk.tracing import Transaction
from sentry_sdk.tracing import SOURCE_FOR_STYLE, Transaction
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
Expand Down Expand Up @@ -148,7 +148,10 @@ async def sentry_urldispatcher_resolve(self, request):

if name is not None:
with Hub.current.configure_scope() as scope:
scope.transaction = name
scope.set_transaction_name(
name,
source=SOURCE_FOR_STYLE[integration.transaction_style],
)

return rv

Expand Down
64 changes: 45 additions & 19 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.sessions import auto_session_tracking
from sentry_sdk.tracing import (
SOURCE_FOR_STYLE,
TRANSACTION_SOURCE_ROUTE,
TRANSACTION_SOURCE_UNKNOWN,
)
from sentry_sdk.utils import (
ContextVar,
event_from_exception,
Expand Down Expand Up @@ -147,6 +152,7 @@ async def _run_app(self, scope, callback):
transaction = Transaction(op="asgi.server")

transaction.name = _DEFAULT_TRANSACTION_NAME
transaction.source = TRANSACTION_SOURCE_ROUTE
transaction.set_tag("asgi.type", ty)

with hub.start_transaction(
Expand Down Expand Up @@ -183,25 +189,7 @@ def event_processor(self, event, hint, asgi_scope):
if client and _should_send_default_pii():
request_info["env"] = {"REMOTE_ADDR": self._get_ip(asgi_scope)}

if (
event.get("transaction", _DEFAULT_TRANSACTION_NAME)
== _DEFAULT_TRANSACTION_NAME
):
if self.transaction_style == "endpoint":
endpoint = asgi_scope.get("endpoint")
# Webframeworks like Starlette mutate the ASGI env once routing is
# done, which is sometime after the request has started. If we have
# an endpoint, overwrite our generic transaction name.
if endpoint:
event["transaction"] = transaction_from_function(endpoint)
elif self.transaction_style == "url":
# FastAPI includes the route object in the scope to let Sentry extract the
# path from it for the transaction name
route = asgi_scope.get("route")
if route:
path = getattr(route, "path", None)
if path is not None:
event["transaction"] = path
self._set_transaction_name_and_source(event, self.transaction_style, asgi_scope)

event["request"] = request_info

Expand All @@ -213,6 +201,44 @@ def event_processor(self, event, hint, asgi_scope):
# data to your liking it's recommended to use the `before_send` callback
# for that.

def _set_transaction_name_and_source(self, event, transaction_style, asgi_scope):
# type: (Event, str, Any) -> None

transaction_name_already_set = (
event.get("transaction", _DEFAULT_TRANSACTION_NAME)
!= _DEFAULT_TRANSACTION_NAME
)
if transaction_name_already_set:
return

name = ""

if transaction_style == "endpoint":
endpoint = asgi_scope.get("endpoint")
# Webframeworks like Starlette mutate the ASGI env once routing is
# done, which is sometime after the request has started. If we have
# an endpoint, overwrite our generic transaction name.
if endpoint:
name = transaction_from_function(endpoint) or ""

elif transaction_style == "url":
# FastAPI includes the route object in the scope to let Sentry extract the
# path from it for the transaction name
route = asgi_scope.get("route")
if route:
path = getattr(route, "path", None)
if path is not None:
name = path

if not name:
# If no transaction name can be found set an unknown source.
# This can happen when ASGI frameworks that are not yet supported well are used.
event["transaction_info"] = {"source": TRANSACTION_SOURCE_UNKNOWN}
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
"""
Expand Down
7 changes: 5 additions & 2 deletions sentry_sdk/integrations/aws_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys

from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.tracing import Transaction
from sentry_sdk.tracing import TRANSACTION_SOURCE_COMPONENT, Transaction
from sentry_sdk._compat import reraise
from sentry_sdk.utils import (
AnnotatedValue,
Expand Down Expand Up @@ -139,7 +139,10 @@ def sentry_handler(aws_event, aws_context, *args, **kwargs):
if headers is None:
headers = {}
transaction = Transaction.continue_from_headers(
headers, op="serverless.function", name=aws_context.function_name
headers,
op="serverless.function",
name=aws_context.function_name,
source=TRANSACTION_SOURCE_COMPONENT,
)
with hub.start_transaction(
transaction,
Expand Down
39 changes: 25 additions & 14 deletions sentry_sdk/integrations/bottle.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import absolute_import

from sentry_sdk.hub import Hub
from sentry_sdk.tracing import SOURCE_FOR_STYLE
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
Expand All @@ -20,7 +21,7 @@
from typing import Optional
from bottle import FileUpload, FormsDict, LocalRequest # type: ignore

from sentry_sdk._types import EventProcessor
from sentry_sdk._types import EventProcessor, Event

try:
from bottle import (
Expand All @@ -40,7 +41,7 @@
class BottleIntegration(Integration):
identifier = "bottle"

transaction_style = None
transaction_style = ""

def __init__(self, transaction_style="endpoint"):
# type: (str) -> None
Expand Down Expand Up @@ -176,24 +177,34 @@ def size_of_file(self, file):
return file.content_length


def _set_transaction_name_and_source(event, transaction_style, request):
# type: (Event, str, Any) -> None
name = ""

if transaction_style == "url":
name = request.route.rule or ""

elif transaction_style == "endpoint":
name = (
request.route.name
or transaction_from_function(request.route.callback)
or ""
)

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


def _make_request_event_processor(app, request, integration):
# type: (Bottle, LocalRequest, BottleIntegration) -> EventProcessor
def inner(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]

try:
if integration.transaction_style == "endpoint":
event["transaction"] = request.route.name or transaction_from_function(
request.route.callback
)
elif integration.transaction_style == "url":
event["transaction"] = request.route.rule
except Exception:
pass
def event_processor(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
_set_transaction_name_and_source(event, integration.transaction_style, request)

with capture_internal_exceptions():
BottleRequestExtractor(request).extract_into_event(event)

return event

return inner
return event_processor
8 changes: 6 additions & 2 deletions sentry_sdk/integrations/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import sys

from sentry_sdk.hub import Hub
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from sentry_sdk.tracing import TRANSACTION_SOURCE_TASK
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
)
from sentry_sdk.tracing import Transaction
from sentry_sdk._compat import reraise
from sentry_sdk.integrations import Integration, DidNotEnable
Expand Down Expand Up @@ -154,8 +158,8 @@ def _inner(*args, **kwargs):
args[3].get("headers") or {},
op="celery.task",
name="unknown celery task",
source=TRANSACTION_SOURCE_TASK,
)

transaction.name = task.name
transaction.set_status("ok")

Expand Down
7 changes: 6 additions & 1 deletion sentry_sdk/integrations/chalice.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.integrations.aws_lambda import _make_request_event_processor
from sentry_sdk.tracing import TRANSACTION_SOURCE_COMPONENT
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
Expand Down Expand Up @@ -65,7 +66,11 @@ def wrapped_view_function(**function_args):
with hub.push_scope() as scope:
with capture_internal_exceptions():
configured_time = app.lambda_context.get_remaining_time_in_millis()
scope.transaction = app.lambda_context.function_name
scope.set_transaction_name(
app.lambda_context.function_name,
source=TRANSACTION_SOURCE_COMPONENT,
)

scope.add_event_processor(
_make_request_event_processor(
app.current_request.to_dict(),
Expand Down
56 changes: 34 additions & 22 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.serializer import add_global_repr_processor
from sentry_sdk.tracing import SOURCE_FOR_STYLE
from sentry_sdk.tracing_utils import record_sql_queries
from sentry_sdk.utils import (
HAS_REAL_CONTEXTVARS,
Expand Down Expand Up @@ -82,7 +83,7 @@ def is_authenticated(request_user):
class DjangoIntegration(Integration):
identifier = "django"

transaction_style = None
transaction_style = ""
middleware_spans = None

def __init__(self, transaction_style="url", middleware_spans=True):
Expand Down Expand Up @@ -319,6 +320,32 @@ def _patch_django_asgi_handler():
patch_django_asgi_handler_impl(ASGIHandler)


def _set_transaction_name_and_source(scope, transaction_style, request):
# type: (Scope, str, WSGIRequest) -> None
try:
transaction_name = ""
if transaction_style == "function_name":
fn = resolve(request.path).func
transaction_name = (
transaction_from_function(getattr(fn, "view_class", fn)) or ""
)

elif transaction_style == "url":
if hasattr(request, "urlconf"):
transaction_name = LEGACY_RESOLVER.resolve(
request.path_info, urlconf=request.urlconf
)
else:
transaction_name = LEGACY_RESOLVER.resolve(request.path_info)

scope.set_transaction_name(
transaction_name,
source=SOURCE_FOR_STYLE[transaction_style],
)
except Exception:
pass


def _before_get_response(request):
# type: (WSGIRequest) -> None
hub = Hub.current
Expand All @@ -330,24 +357,15 @@ def _before_get_response(request):

with hub.configure_scope() as scope:
# Rely on WSGI middleware to start a trace
try:
if integration.transaction_style == "function_name":
fn = resolve(request.path).func
scope.transaction = transaction_from_function(
getattr(fn, "view_class", fn)
)
elif integration.transaction_style == "url":
scope.transaction = LEGACY_RESOLVER.resolve(request.path_info)
except Exception:
pass
_set_transaction_name_and_source(scope, integration.transaction_style, request)

scope.add_event_processor(
_make_event_processor(weakref.ref(request), integration)
)


def _attempt_resolve_again(request, scope):
# type: (WSGIRequest, Scope) -> None
def _attempt_resolve_again(request, scope, transaction_style):
# type: (WSGIRequest, Scope, str) -> None
"""
Some django middlewares overwrite request.urlconf
so we need to respect that contract,
Expand All @@ -356,13 +374,7 @@ def _attempt_resolve_again(request, scope):
if not hasattr(request, "urlconf"):
return

try:
scope.transaction = LEGACY_RESOLVER.resolve(
request.path_info,
urlconf=request.urlconf,
)
except Exception:
pass
_set_transaction_name_and_source(scope, transaction_style, request)


def _after_get_response(request):
Expand All @@ -373,7 +385,7 @@ def _after_get_response(request):
return

with hub.configure_scope() as scope:
_attempt_resolve_again(request, scope)
_attempt_resolve_again(request, scope, integration.transaction_style)


def _patch_get_response():
Expand Down Expand Up @@ -438,7 +450,7 @@ def _got_request_exception(request=None, **kwargs):

if request is not None and integration.transaction_style == "url":
with hub.configure_scope() as scope:
_attempt_resolve_again(request, scope)
_attempt_resolve_again(request, scope, integration.transaction_style)

# If an integration is there, a client has to be there.
client = hub.client # type: Any
Expand Down
Loading

0 comments on commit d4bc0f8

Please sign in to comment.