From e5ab47564d4741274f0141440283d2b65159f352 Mon Sep 17 00:00:00 2001 From: Giovanni Barillari Date: Sun, 13 Oct 2024 18:00:27 +0200 Subject: [PATCH] Support `emmett-core` extensions --- .github/workflows/publish.yml | 28 ++-- .gitignore | 2 +- Makefile | 15 ++ README.md | 2 - emmett_sentry/__init__.py | 2 +- emmett_sentry/__version__.py | 2 +- emmett_sentry/_imports.py | 18 +++ emmett_sentry/ext.py | 84 ++++++----- emmett_sentry/helpers.py | 276 ++++++++++++++++------------------ emmett_sentry/instrument.py | 192 ++++++++++++----------- pyproject.toml | 80 +++++++--- 11 files changed, 392 insertions(+), 309 deletions(-) create mode 100644 Makefile create mode 100644 emmett_sentry/_imports.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 47e2788..78d329a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,20 +7,24 @@ on: jobs: publish: runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/emmett-sentry + permissions: + id-token: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.10' - - name: Install and configure Poetry - uses: gi0baro/setup-poetry-bin@v1.3 - with: - virtualenvs-in-project: true - - name: Publish + python-version: 3.12 + - name: Install uv + uses: astral-sh/setup-uv@v3 + - name: Build distributions run: | - poetry build - poetry publish - env: - POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} + uv build + - name: Publish package to pypi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true diff --git a/.gitignore b/.gitignore index bd78081..059e457 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ __pycache__ *.egg-info/* build/* dist/* -poetry.lock +uv.lock diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f8dd1fe --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.DEFAULT_GOAL := all +pysources = emmett_sentry + +.PHONY: format +format: + ruff check --fix $(pysources) + ruff format $(pysources) + +.PHONY: lint +lint: + ruff check $(pysources) + ruff format --check $(pysources) + +.PHONY: all +all: format lint diff --git a/README.md b/README.md index fe78b3b..3aff84f 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ Emmett-Sentry is an [Emmett framework](https://emmett.sh) extension integrating [Sentry](https://sentry.io) monitoring platform. -[![pip version](https://img.shields.io/pypi/v/emmett-sentry.svg?style=flat)](https://pypi.python.org/pypi/emmett-sentry) - ## Installation You can install Emmett-Sentry using pip: diff --git a/emmett_sentry/__init__.py b/emmett_sentry/__init__.py index fd03ea7..90e7654 100644 --- a/emmett_sentry/__init__.py +++ b/emmett_sentry/__init__.py @@ -1 +1 @@ -from .ext import Sentry # noqa +from .ext import Sentry diff --git a/emmett_sentry/__version__.py b/emmett_sentry/__version__.py index 43c4ab0..49e0fc1 100644 --- a/emmett_sentry/__version__.py +++ b/emmett_sentry/__version__.py @@ -1 +1 @@ -__version__ = "0.6.1" +__version__ = "0.7.0" diff --git a/emmett_sentry/_imports.py b/emmett_sentry/_imports.py new file mode 100644 index 0000000..6bdb768 --- /dev/null +++ b/emmett_sentry/_imports.py @@ -0,0 +1,18 @@ +try: + from emmett import current + from emmett.extensions import Extension, Signals, listen_signal + + _is_emmett = True +except ImportError: + _is_emmett = False + from emmett55 import current + from emmett_core.datastructures import sdict + from emmett_core.extensions import Extension + + Signals = sdict() + + def listen_signal(*args, **kwargs): + def wrapper(f): + return f + + return wrapper diff --git a/emmett_sentry/ext.py b/emmett_sentry/ext.py index 19341b7..3b451d4 100644 --- a/emmett_sentry/ext.py +++ b/emmett_sentry/ext.py @@ -1,35 +1,34 @@ import sys - from typing import Any, Awaitable, Callable, Dict, Optional, TypeVar import sentry_sdk -from emmett.extensions import Extension, Signals, listen_signal -from sentry_sdk.hub import Hub - +from ._imports import Extension, Signals, _is_emmett, listen_signal from .helpers import _capture_exception, _capture_message, patch_routers -T = TypeVar('T') + +T = TypeVar("T") class Sentry(Extension): - default_config = dict( - dsn="", - environment="development", - release=None, - auto_load=True, - enable_tracing=False, - sample_rate=1.0, - tracing_sample_rate=None, - tracing_exclude_routes=[], - trace_websockets=False, - trace_orm=True, - trace_templates=True, - trace_sessions=True, - trace_cache=True, - trace_pipes=False, - integrations=[] - ) + default_config = { + "dsn": "", + "environment": "development", + "release": None, + "auto_load": True, + "enable_tracing": False, + "sample_rate": 1.0, + "tracing_sample_rate": None, + "tracing_exclude_routes": [], + "trace_websockets": False, + "trace_orm": True, + "trace_templates": True, + "trace_sessions": True, + "trace_cache": True, + "trace_pipes": False, + "integrations": [], + "sdk_opts": {}, + } _initialized = False _errmsg = "You need to configure Sentry extension before using its methods" @@ -39,15 +38,17 @@ def on_load(self): if not self.config.dsn: return self._tracing_excluded_routes = set(self.config.tracing_exclude_routes) - sentry_sdk.init( - dsn=self.config.dsn, - environment=self.config.environment, - release=self.config.release, - sample_rate=self.config.sample_rate, - traces_sample_rate=self.config.tracing_sample_rate, - before_send=self._before_send, - integrations=self.config.integrations - ) + sdk_config = { + "dsn": self.config.dsn, + "environment": self.config.environment, + "release": self.config.release, + "sample_rate": self.config.sample_rate, + "traces_sample_rate": self.config.tracing_sample_rate, + "before_send": self._before_send, + "integrations": self.config.integrations, + } + sdk_config = {**sdk_config, **self.config.sdk_opts} + sentry_sdk.init(**sdk_config) if self.config.auto_load: patch_routers(self) self._instrument() @@ -55,23 +56,28 @@ def on_load(self): def _instrument(self): if self.config.enable_tracing: - if self.config.trace_templates: + if self.config.trace_templates and _is_emmett: from .instrument import instrument_templater + instrument_templater(self.app) if self.config.trace_sessions: from .instrument import instrument_sessions + instrument_sessions() if self.config.trace_cache: from .instrument import instrument_cache + instrument_cache() if self.config.trace_pipes: from .instrument import instrument_pipes + instrument_pipes() @listen_signal(Signals.after_database) def _signal_db(self, database): - if self.config.enable_tracing and self.config.trace_orm: + if self.config.enable_tracing and self.config.trace_orm and _is_emmett: from .instrument import instrument_orm + instrument_orm(database) def _before_send(self, event, hint): @@ -98,14 +104,14 @@ def before_send(self, f: T) -> T: def capture_exception(exception, **contexts): - with Hub(Hub.current) as hub: + with sentry_sdk.get_current_scope() as scope: for key, val in contexts.items(): - hub.scope.set_context(key, val) - _capture_exception(hub, exception) + scope.set_context(key, val) + _capture_exception(exception) def capture_message(message, level, **contexts): - with Hub(Hub.current) as hub: + with sentry_sdk.get_current_scope() as scope: for key, val in contexts.items(): - hub.scope.set_context(key, val) - _capture_message(hub, message, level=level) + scope.set_context(key, val) + _capture_message(message, level=level) diff --git a/emmett_sentry/helpers.py b/emmett_sentry/helpers.py index 05f0766..3b79358 100644 --- a/emmett_sentry/helpers.py +++ b/emmett_sentry/helpers.py @@ -1,82 +1,135 @@ +import sys import urllib import weakref - +from contextlib import contextmanager, nullcontext from functools import wraps -from emmett import current -from emmett.asgi.wrappers import Request as ASGIRequest, Websocket as ASGIWebsocket -from emmett.http import HTTPResponse -from emmett.rsgi.wrappers import Request as RSGIRequest, Websocket as RSGIWebsocket -from sentry_sdk.hub import Hub, _should_send_default_pii +import sentry_sdk +from emmett_core.http.response import HTTPResponse +from sentry_sdk.api import continue_trace from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.tracing import Transaction, TRANSACTION_SOURCE_ROUTE +from sentry_sdk.sessions import track_session +from sentry_sdk.tracing import SOURCE_FOR_STYLE from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from ._imports import current -def _capture_exception(hub, exception): - with capture_internal_exceptions(): - event, hint = event_from_exception( - exception, - client_options=hub.client.options, - mechanism={"type": "emmett", "handled": False}, - ) - hub.capture_event(event, hint=hint) +_SPAN_ORIGIN = "auto.http.emmett" +_SPAN_ORIGIN_DB = "auto.db.emmett" -def _capture_message(hub, message, level = None): - with capture_internal_exceptions(): - hub.capture_message(message, level=level) +def _capture_exception(exception, handled=False): + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "emmett", "handled": handled}, + ) + sentry_sdk.capture_event(event, hint=hint) -def _configure_transaction(scope, wrapper): - scope.clear_breadcrumbs() - scope.set_transaction_name(wrapper.name, source=TRANSACTION_SOURCE_ROUTE) +def _capture_message(message, level=None): + sentry_sdk.capture_message(message, level=level) -def _continue_transaction(scope, wrapper, wrapper_type): - scope.clear_breadcrumbs() - proto = ( - "rsgi" if hasattr(wrapper._scope, "rsgi_version") else - "asgi" + +@contextmanager +def _http_scope_wrapper(req, proto, with_sess=True, with_txn=False): + _sentry_scope_gen = sentry_sdk.isolation_scope() + _sentry_scope = _sentry_scope_gen.__enter__() + _sentry_session = track_session(_sentry_scope, session_mode="request") if with_sess else nullcontext() + _sentry_session.__enter__() + _configure_scope(_sentry_scope, proto, _process_http, req) + _ctx = ( + sentry_sdk.start_transaction(_configure_transaction(proto, req, "http"), custom_sampling_context=None) + if with_txn + else nullcontext() + ) + _txn = _ctx.__enter__() + try: + yield _sentry_scope + except HTTPResponse as http: + _txn.set_http_status(http.status_code) + _ctx.__exit__(None, None, None) + _sentry_session.__exit__(None, None, None) + _sentry_scope_gen.__exit__(None, None, None) + raise http + except Exception: + _txn.set_http_status(500) + exc = sys.exc_info() + _ctx.__exit__(*exc) + _sentry_session.__exit__(*exc) + _sentry_scope_gen.__exit__(*exc) + raise + _txn.set_http_status(current.response.status) + _ctx.__exit__(None, None, None) + _sentry_session.__exit__(None, None, None) + _sentry_scope_gen.__exit__(None, None, None) + + +@contextmanager +def _ws_scope_wrapper(ws, proto, with_sess=True, with_txn=False): + _sentry_scope_gen = sentry_sdk.isolation_scope() + _sentry_scope = _sentry_scope_gen.__enter__() + _sentry_session = track_session(_sentry_scope, session_mode="request") if with_sess else nullcontext() + _sentry_session.__enter__() + _configure_scope(_sentry_scope, proto, _process_ws, ws) + _ctx = ( + sentry_sdk.start_transaction(_configure_transaction(proto, ws, "websocket"), custom_sampling_context=None) + if with_txn + else nullcontext() ) - txn = Transaction.continue_from_headers( - wrapper.headers, - op=f"{wrapper_type}.server", - name=wrapper.name, - source=TRANSACTION_SOURCE_ROUTE + _ctx.__enter__() + try: + yield _sentry_scope + except Exception: + exc = sys.exc_info() + _ctx.__exit__(*exc) + _sentry_session.__exit__(*exc) + _sentry_scope_gen.__exit__(*exc) + raise + _ctx.__exit__() + _sentry_session.__exit__() + _sentry_scope_gen.__exit__() + + +def _configure_scope(scope, proto, proc, wrapper): + scope.clear_breadcrumbs() + scope._name = proto + scope.add_event_processor(proc(proto, weakref.ref(wrapper))) + + +def _configure_transaction(proto, wrapper, sgi_proto): + txn_name, txn_source = wrapper.name, SOURCE_FOR_STYLE["endpoint"] + txn = continue_trace( + wrapper.headers, op=f"{sgi_proto}.server", name=txn_name, source=txn_source, origin=_SPAN_ORIGIN ) - txn.set_tag(f"{proto}.type", wrapper_type) + txn.set_tag(f"{proto}.type", sgi_proto) return txn def _process_common_asgi(data, wrapper): - data["query_string"] = urllib.parse.unquote( - wrapper._scope["query_string"].decode("latin-1") - ) + data["query_string"] = urllib.parse.unquote(wrapper._scope["query_string"].decode("latin-1")) + def _process_common_rsgi(data, wrapper): data["query_string"] = urllib.parse.unquote(wrapper._scope.query_string) -def _process_common(data, wrapper): - data["url"] = "%s://%s%s" % ( - wrapper.scheme, - wrapper.host, - wrapper.path - ) +def _process_common(data, proto, wrapper): + data["url"] = "%s://%s%s" % (wrapper.scheme, wrapper.host, wrapper.path) data["env"] = {} data["headers"] = _filter_headers(dict(wrapper.headers.items())) - if _should_send_default_pii(): + if sentry_sdk.get_client().should_send_default_pii(): data["env"]["REMOTE_ADDR"] = wrapper.client - if isinstance(wrapper, (ASGIRequest, ASGIWebsocket)): - _process_common_asgi(data, wrapper) - elif isinstance(wrapper, (RSGIRequest, RSGIWebsocket)): + if proto == "rsgi": _process_common_rsgi(data, wrapper) + elif proto == "asgi": + _process_common_asgi(data, wrapper) -def _process_http(weak_wrapper): +def _process_http(proto, weak_wrapper): def processor(event, hint): wrapper = weak_wrapper() if wrapper is None: @@ -84,7 +137,7 @@ def processor(event, hint): with capture_internal_exceptions(): data = event.setdefault("request", {}) - _process_common(data, wrapper) + _process_common(data, proto, wrapper) data["method"] = wrapper.method data["content_length"] = wrapper.content_length @@ -93,7 +146,7 @@ def processor(event, hint): return processor -def _process_ws(weak_wrapper): +def _process_ws(proto, weak_wrapper): def processor(event, hint): wrapper = weak_wrapper() if wrapper is None: @@ -101,142 +154,67 @@ def processor(event, hint): with capture_internal_exceptions(): data = event.setdefault("request", {}) - _process_common(data, wrapper) + _process_common(data, proto, wrapper) return event return processor -def _build_http_dispatcher_wrapper_err(ext, dispatch_method): +def _build_http_dispatcher_wrapper(ext, dispatch_method, use_txn=False): @wraps(dispatch_method) async def wrap(*args, **kwargs): - hub = Hub.current - weak_request = weakref.ref(current.request) - - with Hub(hub) as hub: - with hub.configure_scope() as scope: - _configure_transaction(scope, current.request) - scope.add_event_processor(_process_http(weak_request)) - for key, builder in ext._scopes.items(): - scope.set_context(key, await builder()) - try: - return await dispatch_method(*args, **kwargs) - except HTTPResponse: - raise - except Exception as exc: - scope.set_context( - "body_params", - await current.request.body_params - ) - _capture_exception(hub, exc) - raise - - return wrap + proto = "rsgi" if hasattr(current.request._scope, "rsgi_version") else "asgi" + with _http_scope_wrapper(current.request, proto, with_sess=use_txn, with_txn=use_txn) as sentry_scope: + for key, builder in ext._scopes.items(): + sentry_scope.set_context(key, await builder()) + try: + return await dispatch_method(*args, **kwargs) + except HTTPResponse: + raise + except Exception as exc: + sentry_scope.set_context("body_params", await current.request.body_params) + _capture_exception(exc) + raise -def _build_http_dispatcher_wrapper_txn(ext, dispatch_method): - @wraps(dispatch_method) - async def wrap(*args, **kwargs): - hub = Hub.current - weak_request = weakref.ref(current.request) - - with Hub(hub) as hub: - with hub.configure_scope() as scope: - txn = _continue_transaction(scope, current.request, "http") - scope.add_event_processor(_process_http(weak_request)) - for key, builder in ext._scopes.items(): - scope.set_context(key, await builder()) - with hub.start_transaction(txn): - try: - return await dispatch_method(*args, **kwargs) - except HTTPResponse: - raise - except Exception as exc: - scope.set_context( - "body_params", - await current.request.body_params - ) - _capture_exception(hub, exc) - raise return wrap -def _build_ws_dispatcher_wrapper_err(ext, dispatch_method): +def _build_ws_dispatcher_wrapper(ext, dispatch_method, use_txn=False): @wraps(dispatch_method) async def wrap(*args, **kwargs): - hub = Hub.current - weak_websocket = weakref.ref(current.websocket) + proto = "rsgi" if hasattr(current.websocket._scope, "rsgi_version") else "asgi" - with hub.configure_scope() as scope: - _configure_transaction(scope, current.websocket) - scope.add_event_processor(_process_ws(weak_websocket)) + with _http_scope_wrapper(current.websocket, proto, with_sess=use_txn, with_txn=use_txn) as sentry_scope: for key, builder in ext._scopes.items(): - scope.set_context(key, await builder()) + sentry_scope.set_context(key, await builder()) try: return await dispatch_method(*args, **kwargs) except Exception as exc: - _capture_exception(hub, exc) + _capture_exception(exc) raise - return wrap - -def _build_ws_dispatcher_wrapper_txn(ext, dispatch_method): - @wraps(dispatch_method) - async def wrap(*args, **kwargs): - hub = Hub.current - weak_websocket = weakref.ref(current.websocket) - - with Hub(hub) as hub: - with hub.configure_scope() as scope: - txn = _continue_transaction(scope, current.request, "websocket") - scope.add_event_processor(_process_ws(weak_websocket)) - for key, builder in ext._scopes.items(): - scope.set_context(key, await builder()) - - with hub.start_transaction(txn): - try: - return await dispatch_method(*args, **kwargs) - except Exception as exc: - _capture_exception(hub, exc) - raise return wrap def _build_routing_rec_http(ext, rec_cls): - def _routing_rec_http(router, name, match, dispatch): - wrapper = ( - _build_http_dispatcher_wrapper_txn if ( - ext.config.enable_tracing and - name not in ext._tracing_excluded_routes - ) else _build_http_dispatcher_wrapper_err - ) - - return rec_cls( - name=name, - match=match, - dispatch=wrapper(ext, dispatch) - ) + def _routing_rec_http(router, name, dispatch): + use_txn = ext.config.enable_tracing and name not in ext._tracing_excluded_routes + return rec_cls(name=name, dispatch=_build_http_dispatcher_wrapper(ext, dispatch, use_txn=use_txn)) return _routing_rec_http def _build_routing_rec_ws(ext, rec_cls): - def _routing_rec_ws(router, name, match, dispatch, flow_recv, flow_send): - wrapper = ( - _build_ws_dispatcher_wrapper_txn if ( - ext.config.enable_tracing and - ext.config.trace_websockets and - name not in ext._tracing_excluded_routes - ) else _build_ws_dispatcher_wrapper_err - ) + def _routing_rec_ws(router, name, dispatch, flow_recv, flow_send): + use_txn = ext.config.enable_tracing and ext.config.trace_websockets and name not in ext._tracing_excluded_routes return rec_cls( name=name, - match=match, - dispatch=wrapper(ext, dispatch), + dispatch=_build_ws_dispatcher_wrapper(ext, dispatch, use_txn=use_txn), flow_recv=flow_recv, - flow_send=flow_send + flow_send=flow_send, ) return _routing_rec_ws diff --git a/emmett_sentry/instrument.py b/emmett_sentry/instrument.py index 270e6dd..db1867d 100644 --- a/emmett_sentry/instrument.py +++ b/emmett_sentry/instrument.py @@ -1,68 +1,75 @@ +import asyncio from functools import wraps -from emmett.orm.adapters import SQLAdapter +import sentry_sdk from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.hub import Hub from sentry_sdk.tracing_utils import record_sql_queries +from ._imports import _is_emmett +from .helpers import _SPAN_ORIGIN, _SPAN_ORIGIN_DB + def _orm_tracer(adapter): - original_method = adapter.execute + _original_method = adapter.execute - @wraps(original_method) + @wraps(_original_method) def wrapped(*args, **kwargs): - hub = Hub.current with record_sql_queries( - hub, - adapter.cursor, - args[0], - [], - None, - False, + cursor=adapter.cursor, + query=args[0], + params_list=[], + paramstyle=None, + executemany=False, + span_origin=_SPAN_ORIGIN_DB, ) as span: span.set_data(SPANDATA.DB_SYSTEM, adapter.dbengine) - return original_method(*args, **kwargs) + return _original_method(*args, **kwargs) + return wrapped +def _templater_ctx_filter(ctx): + return {key: ctx[key] for key in set(ctx.keys()) - {"__builtins__", "__writer__"}} + + def _templater_tracer(render): @wraps(render) def wrapped(*args, **kwargs): - hub = Hub.current - with hub.start_span( - op=OP.TEMPLATE_RENDER, description=args[0] + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=args[0], + origin=_SPAN_ORIGIN, ) as span: - span.set_tag("renoir.template_name", args[0]) + span.set_data("context", _templater_ctx_filter(kwargs.get("context") or args[1])) return render(*args, **kwargs) + return wrapped def _session_tracer(pipe, method): - original_method = getattr(pipe, method) - span_name = { - '_load_session': 'load', - '_pack_session': 'save' - }[method] + _original_method = getattr(pipe, method) + span_name = {"_load_session": "load", "_pack_session": "save"}[method] - @wraps(original_method) + @wraps(_original_method) def wrapped(*args, **kwargs): - hub = Hub.current - with hub.start_span(op=f"session.{span_name}"): - return original_method(*args, **kwargs) + with sentry_sdk.start_span(op=f"session.{span_name}", origin=_SPAN_ORIGIN): + return _original_method(*args, **kwargs) + return wrapped -def _cache_tracer(handler, method): - original_method = getattr(handler, method) +def _cache_tracer(handler, method, op): + _original_method = getattr(handler, method) - @wraps(original_method) + @wraps(_original_method) def wrapped(*args, **kwargs): - hub = Hub.current - with hub.start_span( - op=f"cache.{method}", description=args[1] + with sentry_sdk.start_span( + op=op, + origin=_SPAN_ORIGIN, ) as span: - span.set_tag("emmett.cache_key", args[1]) - return original_method(*args, **kwargs) + span.set_data(SPANDATA.CACHE_KEY, args[1]) + return _original_method(*args, **kwargs) + return wrapped @@ -71,10 +78,14 @@ def _pipe_edge_tracer(original, mode): @wraps(original) async def wrapped(*args, **kwargs): - hub = Hub.current - with hub.start_span(op=f"pipe.{mode}", description=name) as span: + with sentry_sdk.start_span( + op=f"middleware.emmett.{mode}", + name=name, + origin=_SPAN_ORIGIN, + ) as span: span.set_tag("emmett.pipe", name) return await original(*args, **kwargs) + return wrapped @@ -83,10 +94,30 @@ def _pipe_flow_tracer(original, pipe): @wraps(original) async def wrapped(*args, **kwargs): - hub = Hub.current - with hub.start_span(op="pipe", description=name) as span: + with sentry_sdk.start_span( + op="middleware.emmett", + name=name, + origin=_SPAN_ORIGIN, + ) as span: span.set_tag("emmett.pipe", name) return await original(*args, **kwargs) + + return wrapped + + +def _flow_target_tracer(pipeline, original): + if not asyncio.iscoroutinefunction(original): + original = pipeline._awaitable_wrap(original) + + @wraps(original) + async def wrapped(*args, **kwargs): + with sentry_sdk.start_span( + op=OP.FUNCTION, + name=original.__qualname__, + origin=_SPAN_ORIGIN, + ): + return await original(*args, **kwargs) + return wrapped @@ -95,6 +126,7 @@ def _pipeline_edges_instrument(original_method, mode): def wrapped(*args, **kwargs): flow = original_method(*args, **kwargs) return [_pipe_edge_tracer(item, mode) for item in flow] + return wrapped @@ -102,6 +134,7 @@ def _pipe_flow_wrapper(original, pipe): @wraps(original) def wrapped(pipe_method, *args, **kwargs): return original(_pipe_flow_tracer(pipe_method, pipe), *args, **kwargs) + return wrapped @@ -110,10 +143,21 @@ def _pipeline_flow_instrument(original_method): def wrapped(*args, **kwargs): rv = original_method(*args, **kwargs) return _pipe_flow_wrapper(rv, args[1]) + + return wrapped + + +def _pipeline_target_instrument(original_method): + @wraps(original_method) + def wrapped(pipeline, f): + return original_method(pipeline, _flow_target_tracer(pipeline, f)) + return wrapped def instrument_orm(db): + from emmett.orm.adapters import SQLAdapter + if not isinstance(db._adapter, SQLAdapter): return db._adapter.execute = _orm_tracer(db._adapter) @@ -124,57 +168,35 @@ def instrument_templater(app): def instrument_sessions(): - from emmett.sessions import CookieSessionPipe, FileSessionPipe, RedisSessionPipe - setattr( - CookieSessionPipe, - '_load_session', - _session_tracer(CookieSessionPipe, '_load_session') - ) - setattr( - CookieSessionPipe, - '_pack_session', - _session_tracer(CookieSessionPipe, '_pack_session') - ) - setattr( - FileSessionPipe, - '_load_session', - _session_tracer(FileSessionPipe, '_load_session') - ) - setattr( - FileSessionPipe, - '_pack_session', - _session_tracer(FileSessionPipe, '_pack_session') - ) - setattr( - RedisSessionPipe, - '_load_session', - _session_tracer(RedisSessionPipe, '_load_session') - ) - setattr( - RedisSessionPipe, - '_pack_session', - _session_tracer(RedisSessionPipe, '_pack_session') - ) + from emmett_core.sessions import CookieSessionPipe, FileSessionPipe, RedisSessionPipe + + CookieSessionPipe._load_session = _session_tracer(CookieSessionPipe, "_load_session") + CookieSessionPipe._pack_session = _session_tracer(CookieSessionPipe, "_pack_session") + FileSessionPipe._load_session = _session_tracer(FileSessionPipe, "_load_session") + FileSessionPipe._pack_session = _session_tracer(FileSessionPipe, "_pack_session") + RedisSessionPipe._load_session = _session_tracer(RedisSessionPipe, "_load_session") + RedisSessionPipe._pack_session = _session_tracer(RedisSessionPipe, "_pack_session") def instrument_cache(): - from emmett.cache import RamCache, DiskCache, RedisCache - setattr(RamCache, 'get', _cache_tracer(RamCache, 'get')) - setattr(RamCache, 'set', _cache_tracer(RamCache, 'set')) - setattr(DiskCache, 'get', _cache_tracer(DiskCache, 'get')) - setattr(DiskCache, 'set', _cache_tracer(DiskCache, 'set')) - setattr(RedisCache, 'get', _cache_tracer(RedisCache, 'get')) - setattr(RedisCache, 'set', _cache_tracer(RedisCache, 'set')) + from emmett_core.cache.handlers import RamCache, RedisCache + + RamCache.get = _cache_tracer(RamCache, "get", "get") + RamCache.set = _cache_tracer(RamCache, "set", "put") + RedisCache.get = _cache_tracer(RedisCache, "get", "get") + RedisCache.set = _cache_tracer(RedisCache, "set", "put") + + if _is_emmett: + from emmett.cache import DiskCache + + DiskCache.get = _cache_tracer(DiskCache, "get", "get") + DiskCache.set = _cache_tracer(DiskCache, "set", "put") def instrument_pipes(): - from emmett.pipeline import RequestPipeline - RequestPipeline._flow_open = _pipeline_edges_instrument( - RequestPipeline._flow_open, 'open' - ) - RequestPipeline._flow_close = _pipeline_edges_instrument( - RequestPipeline._flow_close, 'close' - ) - RequestPipeline._get_proper_wrapper = _pipeline_flow_instrument( - RequestPipeline._get_proper_wrapper - ) + from emmett_core.pipeline import RequestPipeline + + RequestPipeline._flow_open = _pipeline_edges_instrument(RequestPipeline._flow_open, "open") + RequestPipeline._flow_close = _pipeline_edges_instrument(RequestPipeline._flow_close, "close") + RequestPipeline._get_proper_wrapper = _pipeline_flow_instrument(RequestPipeline._get_proper_wrapper) + RequestPipeline.__call__ = _pipeline_target_instrument(RequestPipeline.__call__) diff --git a/pyproject.toml b/pyproject.toml index 776874e..b148691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,18 @@ -[project] -name = "emmett-sentry" +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] +[project] name = "emmett-sentry" -version = "0.6.1" +version = "0.7.0" description = "Sentry extension for Emmett framework" -authors = ["Giovanni Barillari "] +readme = "README.md" license = "BSD-3-Clause" +requires-python = ">=3.8" -readme = "README.md" -homepage = "https://github.com/emmett-framework/sentry" -repository = "https://github.com/emmett-framework/sentry" +authors = [ + { name = "Giovanni Barillari", email = "g@baro.dev" } +] keywords = ["sentry", "logging", "emmett"] classifiers = [ @@ -25,24 +27,64 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules" ] +dependencies = [ + "emmett-core~=1.0", + "sentry-sdk~=2.16", +] + +[project.urls] +Homepage = 'https://github.com/emmett-framework/sentry' +Funding = 'https://github.com/sponsors/gi0baro' +Source = 'https://github.com/emmett-framework/sentry' +Issues = 'https://github.com/emmett-framework/sentry/issues' + +[tool.hatch.build.targets.sdist] include = [ - "LICENSE" + '/README.md', + '/LICENSE', + '/emmett_sentry', ] -[tool.poetry.dependencies] -python = "^3.8" -emmett = "^2.5.0" -sentry-sdk = "^1.31.0" +[tool.ruff] +line-length = 120 + +[tool.ruff.format] +quote-style = 'double' + +[tool.ruff.lint] +extend-select = [ + # E and F are enabled by default + 'B', # flake8-bugbear + 'C4', # flake8-comprehensions + 'C90', # mccabe + 'I', # isort + 'N', # pep8-naming + 'Q', # flake8-quotes + 'RUF100', # ruff (unused noqa) + 'S', # flake8-bandit + 'W', # pycodestyle +] +extend-ignore = [ + 'S101', # assert is fine +] +mccabe = { max-complexity = 44 } -[tool.poetry.dev-dependencies] +[tool.ruff.lint.isort] +combine-as-imports = true +lines-after-imports = 2 +known-first-party = ['emmett_sentry'] -[tool.poetry.urls] -"Issue Tracker" = "https://github.com/emmett-framework/sentry/issues" +[tool.ruff.lint.per-file-ignores] +'emmett_sentry/__init__.py' = ['F401'] +'emmett_sentry/_imports.py' = ['F401'] -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.uv] +dev-dependencies = [ + "ruff~=0.5.0", +]