Skip to content

Commit

Permalink
✨ feat: add starlite integration
Browse files Browse the repository at this point in the history
  • Loading branch information
gazorby committed Dec 3, 2022
1 parent 46697dd commit c395d52
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 0 deletions.
28 changes: 28 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,31 @@ def _get_default_options():


VERSION = "1.11.1"


class OP:
DB = "db"
DB_REDIS = "db.redis"
EVENT_DJANGO = "event.django"
FUNCTION = "function"
FUNCTION_AWS = "function.aws"
FUNCTION_GCP = "function.gcp"
HTTP_CLIENT = "http.client"
HTTP_CLIENT_STREAM = "http.client.stream"
HTTP_SERVER = "http.server"
MIDDLEWARE_DJANGO = "middleware.django"
MIDDLEWARE_STARLETTE = "middleware.starlette"
MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive"
MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send"
MIDDLEWARE_STARLITE = "middleware.starlite"
MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive"
MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send"
QUEUE_SUBMIT_CELERY = "queue.submit.celery"
QUEUE_TASK_CELERY = "queue.task.celery"
QUEUE_TASK_RQ = "queue.task.rq"
SUBPROCESS = "subprocess"
SUBPROCESS_WAIT = "subprocess.wait"
SUBPROCESS_COMMUNICATE = "subprocess.communicate"
TEMPLATE_RENDER = "template.render"
VIEW_RENDER = "view.render"
WEBSOCKET_SERVER = "websocket.server"
260 changes: 260 additions & 0 deletions sentry_sdk/integrations/starlite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
from typing import TYPE_CHECKING, Any, Awaitable, Dict, List, Optional, Union

from pydantic import BaseModel

from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE
from sentry_sdk.utils import event_from_exception, transaction_from_function
from sentry_sdk.consts import OP

try:
from starlite import Request, Starlite, State
from starlite.handlers.base import BaseRouteHandler
from starlite.middleware import DefineMiddleware
from starlite.plugins.base import get_plugin_for_value
from starlite.routes.http import HTTPRoute
from starlite.utils import ConnectionDataExtractor, is_async_callable

if TYPE_CHECKING:
from starlite.types import (
ASGIApp,
HTTPReceiveMessage,
HTTPScope,
Message,
Middleware,
Receive,
Scope,
Send,
WebSocketReceiveMessage,
)

from sentry_sdk._types import Event
except ImportError:
raise DidNotEnable("Starlette is not installed")


_DEFAULT_TRANSACTION_NAME = "generic Starlite request"


class SentryStarliteASGIMiddleware(SentryAsgiMiddleware):
def __init__(self, app: "ASGIApp"):
super().__init__(
app=app,
unsafe_context_data=False,
transaction_style="endpoint",
mechanism_type="asgi",
)


class StarliteIntegration(Integration):
identifier = "starlite"

@staticmethod
def setup_once():
patch_app_init()
patch_middlewares()
patch_http_route_handle()


def patch_app_init() -> None:
"""
Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the
`SentryStarliteASGIMiddleware` as the outmost middleware in the stack.
See:
- https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception
- https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/
"""
old__init__ = Starlite.__init__ # type: ignore

def injection_wrapper(self, *args, **kwargs):

after_exception = kwargs.pop("after_exception", [])
kwargs.update(
after_exception=[
exception_handler,
*(
after_exception
if isinstance(after_exception, list)
else [after_exception]
),
]
)

SentryStarliteASGIMiddleware.__call__ = SentryStarliteASGIMiddleware._run_asgi3
middleware = kwargs.pop("middleware", [])
kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware]
old__init__(self, *args, **kwargs)

Starlite.__init__ = injection_wrapper


def patch_middlewares() -> None:
old__resolve_middleware_stack = BaseRouteHandler.resolve_middleware

def resolve_middleware_wrapper(self: Any) -> List["Middleware"]:
return list(
map(
lambda middleware: enable_span_for_middleware(middleware),
old__resolve_middleware_stack(self),
)
)

BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper


def enable_span_for_middleware(middleware: "Middleware") -> "Middleware":
if not hasattr(middleware, "__call__"):
return middleware

if isinstance(middleware, DefineMiddleware):
old_call = middleware.middleware.__call__
else:
old_call = middleware.__call__

async def _create_span_call(self, scope: "Scope", receive: "Receive", send: "Send"):
hub = Hub.current
integration = hub.get_integration(StarliteIntegration)
if integration is not None:
middleware_name = self.__class__.__name__
with hub.start_span(
op=OP.MIDDLEWARE_STARLITE, description=middleware_name
) as middleware_span:
middleware_span.set_tag("starlite.middleware_name", middleware_name)

# Creating spans for the "receive" callback
async def _sentry_receive(
*args, **kwargs
) -> Awaitable[Union["HTTPReceiveMessage", "WebSocketReceiveMessage"]]:
hub = Hub.current
with hub.start_span(
op=OP.MIDDLEWARE_STARLITE_RECEIVE,
description=getattr(receive, "__qualname__", str(receive)),
) as span:
span.set_tag("starlite.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(message: "Message") -> Awaitable[None]:
hub = Hub.current
with hub.start_span(
op=OP.MIDDLEWARE_STARLITE_SEND,
description=getattr(send, "__qualname__", str(send)),
) as span:
span.set_tag("starlite.middleware_name", middleware_name)
return await send(message)

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(self, scope, new_receive, new_send)
else:
return await old_call(self, scope, receive, send)

not_yet_patched = old_call.__name__ not in ["_create_span_call"]

if not_yet_patched:
if isinstance(middleware, DefineMiddleware):
middleware.middleware.__call__ = _create_span_call
else:
middleware.__call__ = _create_span_call

return middleware


def patch_http_route_handle() -> None:
old_handle = HTTPRoute.handle

async def handle_wrapper(
self, scope: "HTTPScope", receive: "Receive", send: "Send"
) -> None:
hub = Hub.current
integration: StarliteIntegration = hub.get_integration(StarliteIntegration)
if integration is None:
return await old_handle(self, scope, receive, send)

with hub.configure_scope() as sentry_scope:
request: "Request[Any, Any]" = scope["app"].request_class(
scope=scope, receive=receive, send=send
)
extracted_request_data = ConnectionDataExtractor(
parse_body=True, parse_query=True
)(request)
body = extracted_request_data.pop("body")

request_data = await body

def event_processor(event: "Event", _: Dict[str, Any]) -> "Event":
route_handler = scope.get("route_handler")

request_info = event.get("request", {})
request_info["content_length"] = len(scope.get("_body", b"")) # type: ignore
if _should_send_default_pii():
request_info["cookies"] = extracted_request_data["cookies"]
if request_data is not None:
request_info["data"] = request_data

tx_name = route_handler.name or transaction_from_function(
route_handler.fn
)
tx_info = {"source": SOURCE_FOR_STYLE["endpoint"]}

if not tx_name:
tx_name = _DEFAULT_TRANSACTION_NAME
tx_info = {"source": TRANSACTION_SOURCE_ROUTE}

event.update(
request=request_info, transaction=tx_name, transaction_info=tx_info
)
return event

sentry_scope._name = StarliteIntegration.identifier
sentry_scope.add_event_processor(event_processor)

await old_handle(self, scope, receive, send)

HTTPRoute.handle = handle_wrapper


def retrieve_user_from_scope(scope: "Scope") -> Optional[Dict[str, Any]]:
scope_user = scope.get("user", {})
if not scope_user or not _should_send_default_pii():
return None

if isinstance(scope_user, dict):
return scope_user
if isinstance(scope_user, BaseModel):
return scope_user.dict()
if hasattr(scope_user, "asdict"): # dataclasses
return scope_user.asdict()

plugin = get_plugin_for_value(scope_user)
if plugin and not is_async_callable(plugin.to_dict):
return plugin.to_dict(scope_user)

return None


def exception_handler(exc: Exception, scope: "Scope", _: "State"):
hub = Hub.current
if hub.get_integration(StarliteIntegration) is None:
return

user_info = retrieve_user_from_scope(scope)
if user_info and isinstance(user_info, dict):
with hub.configure_scope() as sentry_scope:
sentry_scope.set_user(user_info)

event, hint = event_from_exception(
exc,
client_options=hub.client.options if hub.client else None,
mechanism={"type": StarliteIntegration.identifier, "handled": True},
)

hub.capture_event(event, hint=hint)
Empty file.

0 comments on commit c395d52

Please sign in to comment.