관리-도구
편집 파일: __init__.py
# -*- coding: utf-8 -*- from __future__ import absolute_import import sys import weakref from django import VERSION as DJANGO_VERSION # type: ignore from django.db.models.query import QuerySet # type: ignore from django.core import signals # type: ignore if False: from typing import Any from typing import Dict from typing import Tuple from typing import Union from sentry_sdk.integrations.wsgi import _ScopedResponse from typing import Callable from django.core.handlers.wsgi import WSGIRequest # type: ignore from django.http.response import HttpResponse # type: ignore from django.http.request import QueryDict # type: ignore from django.utils.datastructures import MultiValueDict # type: ignore from typing import List try: from django.urls import resolve # type: ignore except ImportError: from django.core.urlresolvers import resolve # type: ignore from sentry_sdk import Hub from sentry_sdk.hub import _should_send_default_pii from sentry_sdk.scope import add_global_event_processor from sentry_sdk.utils import ( add_global_repr_processor, capture_internal_exceptions, event_from_exception, safe_repr, format_and_strip, transaction_from_function, walk_exception_chain, ) from sentry_sdk.integrations import Integration 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.django.transactions import LEGACY_RESOLVER from sentry_sdk.integrations.django.templates import get_template_frame_from_exception if DJANGO_VERSION < (1, 10): def is_authenticated(request_user): # type: (Any) -> bool return request_user.is_authenticated() else: def is_authenticated(request_user): # type: (Any) -> bool return request_user.is_authenticated class DjangoIntegration(Integration): identifier = "django" transaction_style = None def __init__(self, transaction_style="url"): # type: (str) -> None TRANSACTION_STYLE_VALUES = ("function_name", "url") 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 @staticmethod def setup_once(): # type: () -> None install_sql_hook() # Patch in our custom middleware. # logs an error for every 500 ignore_logger("django.server") ignore_logger("django.request") from django.core.handlers.wsgi import WSGIHandler old_app = WSGIHandler.__call__ def sentry_patched_wsgi_handler(self, environ, start_response): # type: (Any, Dict[str, str], Callable) -> _ScopedResponse if Hub.current.get_integration(DjangoIntegration) is None: return old_app(self, environ, start_response) return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))( environ, start_response ) WSGIHandler.__call__ = sentry_patched_wsgi_handler # patch get_response, because at that point we have the Django request # object from django.core.handlers.base import BaseHandler # type: ignore old_get_response = BaseHandler.get_response def sentry_patched_get_response(self, request): # type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException] hub = Hub.current integration = hub.get_integration(DjangoIntegration) if integration is not None: with hub.configure_scope() as scope: scope.add_event_processor( _make_event_processor(weakref.ref(request), integration) ) return old_get_response(self, request) BaseHandler.get_response = sentry_patched_get_response signals.got_request_exception.connect(_got_request_exception) @add_global_event_processor def process_django_templates(event, hint): # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] exc_info = hint.get("exc_info", None) if exc_info is None: return event exception = event.get("exception", None) if exception is None: return event values = exception.get("values", None) if values is None: return event for exception, (_, exc_value, _) in zip( values, walk_exception_chain(exc_info) ): frame = get_template_frame_from_exception(exc_value) if frame is not None: frames = exception.get("stacktrace", {}).get("frames", []) for i in reversed(range(len(frames))): f = frames[i] if ( f.get("function") in ("parse", "render") and f.get("module") == "django.template.base" ): i += 1 break else: i = len(frames) frames.insert(i, frame) return event @add_global_repr_processor def _django_queryset_repr(value, hint): if not isinstance(value, QuerySet) or value._result_cache: return NotImplemented # Do not call Hub.get_integration here. It is intentional that # running under a new hub does not suddenly start executing # querysets. This might be surprising to the user but it's likely # less annoying. return u"<%s from %s at 0x%x>" % ( value.__class__.__name__, value.__module__, id(value), ) def _make_event_processor(weak_request, integration): # type: (Callable[[], WSGIRequest], DjangoIntegration) -> Callable def event_processor(event, hint): # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] # if the request is gone we are fine not logging the data from # it. This might happen if the processor is pushed away to # another thread. request = weak_request() if request is None: return event try: if integration.transaction_style == "function_name": event["transaction"] = transaction_from_function( resolve(request.path).func ) elif integration.transaction_style == "url": event["transaction"] = LEGACY_RESOLVER.resolve(request.path) except Exception: pass with capture_internal_exceptions(): DjangoRequestExtractor(request).extract_into_event(event) if _should_send_default_pii(): with capture_internal_exceptions(): _set_user_info(request, event) return event return event_processor def _got_request_exception(request=None, **kwargs): # type: (WSGIRequest, **Any) -> None hub = Hub.current integration = hub.get_integration(DjangoIntegration) if integration is not None: event, hint = event_from_exception( sys.exc_info(), client_options=hub.client.options, mechanism={"type": "django", "handled": False}, ) hub.capture_event(event, hint=hint) class DjangoRequestExtractor(RequestExtractor): def env(self): # type: () -> Dict[str, str] return self.request.META def cookies(self): # type: () -> Dict[str, str] return self.request.COOKIES def raw_data(self): # type: () -> bytes return self.request.body def form(self): # type: () -> QueryDict return self.request.POST def files(self): # type: () -> MultiValueDict return self.request.FILES def size_of_file(self, file): return file.size def parsed_body(self): try: return self.request.data except AttributeError: return RequestExtractor.parsed_body(self) def _set_user_info(request, event): # type: (WSGIRequest, Dict[str, Any]) -> None user_info = event.setdefault("user", {}) user = getattr(request, "user", None) if user is None or not is_authenticated(user): return try: user_info["id"] = str(user.pk) except Exception: pass try: user_info["email"] = user.email except Exception: pass try: user_info["username"] = user.get_username() except Exception: pass class _FormatConverter(object): def __init__(self, param_mapping): # type: (Dict[str, int]) -> None self.param_mapping = param_mapping self.params = [] # type: List[Any] def __getitem__(self, val): # type: (str) -> str self.params.append(self.param_mapping.get(val)) return "%s" def format_sql(sql, params): # type: (Any, Any) -> Tuple[str, List[str]] rv = [] if isinstance(params, dict): # convert sql with named parameters to sql with unnamed parameters conv = _FormatConverter(params) if params: sql = sql % conv params = conv.params else: params = () for param in params or (): if param is None: rv.append("NULL") param = safe_repr(param) rv.append(param) return sql, rv def record_sql(sql, params, cursor=None): # type: (Any, Any, Any) -> None hub = Hub.current if hub.get_integration(DjangoIntegration) is None: return real_sql = None real_params = None try: # Prefer our own SQL formatting logic because it's the only one that # has proper value trimming. real_sql, real_params = format_sql(sql, params) if real_sql: real_sql = format_and_strip(real_sql, real_params) except Exception: pass if not real_sql and cursor and hasattr(cursor, "mogrify"): # If formatting failed and we're using psycopg2, it could be that we're # looking at a query that uses Composed objects. Use psycopg2's mogrify # function to format the query. We lose per-parameter trimming but gain # accuracy in formatting. # # This is intentionally the second choice because we assume Composed # queries are not widely used, while per-parameter trimming is # generally highly desirable. try: if cursor and hasattr(cursor, "mogrify"): real_sql = cursor.mogrify(sql, params) if isinstance(real_sql, bytes): real_sql = real_sql.decode(cursor.connection.encoding) except Exception: pass if real_sql: with capture_internal_exceptions(): hub.add_breadcrumb(message=real_sql, category="query") def install_sql_hook(): # type: () -> None """If installed this causes Django's queries to be captured.""" try: from django.db.backends.utils import CursorWrapper # type: ignore except ImportError: from django.db.backends.util import CursorWrapper # type: ignore try: real_execute = CursorWrapper.execute real_executemany = CursorWrapper.executemany except AttributeError: # This won't work on Django versions < 1.6 return def record_many_sql(sql, param_list, cursor): for params in param_list: record_sql(sql, params, cursor) def execute(self, sql, params=None): try: return real_execute(self, sql, params) finally: record_sql(sql, params, self.cursor) def executemany(self, sql, param_list): try: return real_executemany(self, sql, param_list) finally: record_many_sql(sql, param_list, self.cursor) CursorWrapper.execute = execute CursorWrapper.executemany = executemany ignore_logger("django.db.backends")