Django项目配合sentry实现浅析
发布日期:2021-07-25 13:04:50 浏览次数:22 分类:技术文章

本文共 50075 字,大约阅读时间需要 166 分钟。

Django项目日志配合sentry概述

本文环境python3.5.2,Django版本1.10.2

Django项目中日志配合sentry的实现

sentry是一个错误跟踪网站,可以收集获取运行中报错的相关信息。本文的实例代码参考上篇博文Django项目日志概述中的示例代码,其中部分示例代码如下;

在settings.py文件中添加如下配置LOGGING = {    ...    'handlers': {        ...        'sentry': {            'level': 'ERROR',            'filters': ['require_debug_false'],            'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler'        },    },    'loggers': {        ...        'django.request': {            'handlers': ['file', 'sentry'],            'level': 'INFO',            'propagate': False,        },        ...    }}

然后在APPS中添加’raven.contrib.django.raven_compat’, 还需要相关项目等的配置,具体的申请配置流程可上sentry官网详细查阅,通过上述大致配置流程就可以获取并跟踪Django服务运行过程中出现的报错的相关信息,此时Django项目中的请求报错信息就会发送到sentry网站上,我们可以通过sentry网站的相关账号就可以去查看服务报错的信息,本文我们就简单概述一下sentry错误跟踪的实现过程。

sentry错误跟踪的实现概述

首先,先通过pip install安装好sentry,然后将raven.contrib.django.raven_compat添加到app配置中,首先先查看一下app初始化的时候,sentry做了哪些工作;

from __future__ import absolute_importfrom raven.contrib.django import *  # NOQA

此时就直接导入了raven.contrib.django所有的模块,此时首先关注一下raven.contrib.django.models.py中的初始化过程;

if (    'raven.contrib.django' in settings.INSTALLED_APPS or    'raven.contrib.django.raven_compat' in settings.INSTALLED_APPS):                                                                          # 判断是否在app中配置了sentry    register_serializers()                                                  # 注册序列化    install_middleware()                                                    # 注册中间件    if not getattr(settings, 'DISABLE_SENTRY_INSTRUMENTATION', False):      # 获取是否配置了不使用sentry配置        handler = SentryDjangoHandler()                                     # 如果可用使用则初始化handler        handler.install()                                                   # 调用handler的初始化方法

这代代码在导入模块的时候在加载app.models.py中会被加载执行,首先先注册一下序列化的操作,然后再加载一下raven中的中间件,

def install_middleware():    """    Force installation of SentryMiddlware if it's not explicitly present.    This ensures things like request context and transaction names are made    available.    """    name = 'raven.contrib.django.middleware.SentryMiddleware'                       # 中间价配置名称    all_names = (name, 'raven.contrib.django.middleware.SentryLogMiddleware')       # 另一个配置名称  气质两个中间件对应的操作一样就是类名不一样    with settings_lock:                                                             # 获取锁        # default settings.MIDDLEWARE is None           middleware_attr = 'MIDDLEWARE' if getattr(settings,                                                  'MIDDLEWARE',                                                  None) is not None \            else 'MIDDLEWARE_CLASSES'                                               # 根据Django版本不同获取中间件的配置名称        # make sure to get an empty tuple when attr is None        middleware = getattr(settings, middleware_attr, ()) or ()                   # 获取Django项目中配置的中间件的值        if set(all_names).isdisjoint(set(middleware)):                              # 判断是否已经存在中间件是否已经手工配置过            setattr(settings,                    middleware_attr,                    type(middleware)((name,)) + middleware)                         # 如果没有配置则将raven的中间件配置放在列表的第一个

此时就将项目中的中间件进行了配置,继续查看raven.contrib.django.middleware.SentryMiddleware的内容;

if MiddlewareMixin is object:    _SentryMiddlewareBase = threading.local                                                             # 是否成功导入Django项目中的MiddlewareMixinelse:    _SentryMiddlewareBase = type('_SentryMiddlewareBase', (MiddlewareMixin, threading.local), {})       # 导入成功则生成一个继承自MiddlewareMixin和threading.local类的类class SentryMiddleware(_SentryMiddlewareBase):    resolver = RouteResolver()                                                          # 解析路径相关信息    # backwards compat    @property    def thread(self):        return self    def _get_transaction_from_request(self, request):        # TODO(dcramer): it'd be nice to pull out parameters        # and make this a normalized path        return self.resolver.resolve(request.path)                                      # 解析路径相关信息    def process_request(self, request):                                                 # 在处理之前        self._txid = None        self.thread.request = request                                                   # 通过线程数据单独保存request    def process_view(self, request, func, args, kwargs):        from raven.contrib.django.models import client                                  # 在处理view的时候就导入client        try:            self._txid = client.transaction.push(                self._get_transaction_from_request(request)            )                                                                           # 获取相关的路径信息并压入列表中        except Exception as exc:            client.error_logger.exception(repr(exc))        else:            # we utilize request_finished as the exception gets reported            # *after* process_response is executed, and thus clearing the            # transaction there would leave it empty            # XXX(dcramer): weakref's cause a threading issue in certain            # versions of Django (e.g. 1.6). While they'd be ideal, we're under            # the assumption that Django will always call our function except            # in the situation of a process or thread dying.            request_finished.connect(self.request_finished, weak=False)                 # 处理完成后的回调函数        return None    def request_finished(self, **kwargs):        from raven.contrib.django.models import client        if getattr(self, '_txid', None):            client.transaction.pop(self._txid)                                          # 如果不为空则删除_txid            self._txid = None                                                           # 置空        request_finished.disconnect(self.request_finished)                              # 移除回调信息SentryLogMiddleware = SentryMiddleware

至此中间件的初始化过程就完成了,项目的初始化过程也已经完成。其中日志处理的SentryHandler的初始化过程与上一遍博文的handler的初始化过程类似,再次就不做详细解释。

概述Django项目中500报错时sentry的处理过程

在正常的请求处理过程中,Django项目中的处理是通过WSGIHandler来处理请求的,在request正常初始化完成就会调用handler的 self.get_response(request)方法来处理请求,此时就会在路由解析完成后,就会执行到_get_response中的如下代码;

# Apply view middlewarefor middleware_method in self._view_middleware:    response = middleware_method(request, callback, callback_args, callback_kwargs)         # 执行中间件的process_view方法    if response:        break

此时就会执行到SentryMiddleware的process_view方法,

def process_view(self, request, func, args, kwargs):    from raven.contrib.django.models import client    try:        self._txid = client.transaction.push(            self._get_transaction_from_request(request)                     # 调用client并初始化一个全局的client        )    except Exception as exc:        client.error_logger.exception(repr(exc))    else:        # we utilize request_finished as the exception gets reported        # *after* process_response is executed, and thus clearing the        # transaction there would leave it empty        # XXX(dcramer): weakref's cause a threading issue in certain        # versions of Django (e.g. 1.6). While they'd be ideal, we're under        # the assumption that Django will always call our function except        # in the situation of a process or thread dying.        request_finished.connect(self.request_finished, weak=False)    return None

此时就会初始化一个全局的client实例,来处理handle,此时查看client的初始化过程;

class ProxyClient(object):    """    A proxy which represents the currently client at all times.    """    # introspection support:    __members__ = property(lambda x: x.__dir__())    # Need to pretend to be the wrapped class, for the sake of objects that care    # about this (especially in equality tests)    __class__ = property(lambda x: get_client().__class__)    __dict__ = property(lambda o: get_client().__dict__)    __repr__ = lambda x: repr(get_client())    __getattr__ = lambda x, o: getattr(get_client(), o)    __setattr__ = lambda x, o, v: setattr(get_client(), o, v)    __delattr__ = lambda x, o: delattr(get_client(), o)    __lt__ = lambda x, o: get_client() < o    __le__ = lambda x, o: get_client() <= o    __eq__ = lambda x, o: get_client() == o    __ne__ = lambda x, o: get_client() != o    __gt__ = lambda x, o: get_client() > o    __ge__ = lambda x, o: get_client() >= o    if PY2:        __cmp__ = lambda x, o: cmp(get_client(), o)  # NOQA    __hash__ = lambda x: hash(get_client())    # attributes are currently not callable    # __call__ = lambda x, *a, **kw: get_client()(*a, **kw)    __nonzero__ = lambda x: bool(get_client())    __len__ = lambda x: len(get_client())    __getitem__ = lambda x, i: get_client()[i]    __iter__ = lambda x: iter(get_client())    __contains__ = lambda x, i: i in get_client()    __getslice__ = lambda x, i, j: get_client()[i:j]    __add__ = lambda x, o: get_client() + o    __sub__ = lambda x, o: get_client() - o    __mul__ = lambda x, o: get_client() * o    __floordiv__ = lambda x, o: get_client() // o    __mod__ = lambda x, o: get_client() % o    __divmod__ = lambda x, o: get_client().__divmod__(o)    __pow__ = lambda x, o: get_client() ** o    __lshift__ = lambda x, o: get_client() << o    __rshift__ = lambda x, o: get_client() >> o    __and__ = lambda x, o: get_client() & o    __xor__ = lambda x, o: get_client() ^ o    __or__ = lambda x, o: get_client() | o    __div__ = lambda x, o: get_client().__div__(o)    __truediv__ = lambda x, o: get_client().__truediv__(o)    __neg__ = lambda x: -(get_client())    __pos__ = lambda x: +(get_client())    __abs__ = lambda x: abs(get_client())    __invert__ = lambda x: ~(get_client())    __complex__ = lambda x: complex(get_client())    __int__ = lambda x: int(get_client())    if PY2:        __long__ = lambda x: long(get_client())  # NOQA    __float__ = lambda x: float(get_client())    __str__ = lambda x: binary_type(get_client())    __unicode__ = lambda x: text_type(get_client())    __oct__ = lambda x: oct(get_client())    __hex__ = lambda x: hex(get_client())    __index__ = lambda x: get_client().__index__()    __coerce__ = lambda x, o: x.__coerce__(x, o)    __enter__ = lambda x: x.__enter__()    __exit__ = lambda x, *a, **kw: x.__exit__(*a, **kw)client = ProxyClient()                                              # 代理类的实例

该代理类重写了方法,并且主要的返回就是通过get_client()来调用client实例的对应方法,此时继续查看get_client()函数;

def get_client(client=None, reset=False):    global _client                                                              # 全局的_client    tmp_client = client is not None                                             # 判断传入的client是否为空    if not tmp_client:                                                          # 如果为空则获取配置文件中的SENTRY_CLIENT对应的Client类        client = getattr(settings, 'SENTRY_CLIENT', 'raven.contrib.django.DjangoClient')    if _client[0] != client or reset:                                           # 判断两个client是否相同或者是否需要重置        options = convert_options(            settings,            defaults={                'include_paths': get_installed_apps(),            },        )                                                                       # 获取配置传入参数        try:            Client = import_string(client)                                      # 导入该类        except ImportError:            logger.exception('Failed to import client: %s', client)            if not _client[1]:                # If there is no previous client, set the default one.                client = 'raven.contrib.django.DjangoClient'                _client = (client, get_client(client))        else:            instance = Client(**options)                                        # 初始化该类            if not tmp_client:                                                  _client = (client, instance)                                    # 保存到_client中            return instance                                                     # 返回实例    return _client[1]                                                           # 直接返回实例

此时默认的处理client就是raven.contrib.django.DjangoClient,

class DjangoClient(Client):    logger = logging.getLogger('sentry.errors.client.django')    def __init__(self, *args, **kwargs):        install_sql_hook = kwargs.pop('install_sql_hook', True)         # 获取是否注册hook配置        Client.__init__(self, *args, **kwargs)                          # 调用父类的初始化方法        if install_sql_hook:                                            # 如果注册            self.install_sql_hook()                                     # 默认注册    def install_sql_hook(self):        install_sql_hook()                                              # 调用sql hook方法

此时就调用了父类的初始化方法,大致看下Client的初始化方法;

class Client(object):    logger = logging.getLogger('raven')    protocol_version = '6'    _registry = TransportRegistry(transports=default_transports)    def __init__(self, dsn=None, raise_send_errors=False, transport=None,                 install_sys_hook=True, install_logging_hook=True,                 hook_libraries=None, enable_breadcrumbs=True, **options):        global Raven        o = options                                                                     # 传入配置参数        self.raise_send_errors = raise_send_errors        # configure loggers first        cls = self.__class__                                                            # 获取本类        self.state = ClientState()                                                      # 获取状态        self.logger = logging.getLogger(            '%s.%s' % (cls.__module__, cls.__name__))                                   # 获取日志        self.error_logger = logging.getLogger('sentry.errors')         self.uncaught_logger = logging.getLogger('sentry.errors.uncaught')         self._transport_cache = {}        self.set_dsn(dsn, transport)                                                    # 设置发送客户端        ...        if Raven is None:            Raven = self        # We want to remember the creating thread id here because this        # comes in useful for the context special handling        self.main_thread_id = get_thread_ident()                                        self.enable_breadcrumbs = enable_breadcrumbs        from raven.context import Context        self._context = Context(self)        if install_sys_hook:            self.install_sys_hook()                                                 # 注册sys hook        if install_logging_hook:            self.install_logging_hook()                                             # 注册日志hook        self.hook_libraries(hook_libraries)                                         # 注册libraries hook

其中比较主要的方法就是set_dsn方法,方法install_logging_hook和hook_libraries方法,其中set_dsn方法如下;

def set_dsn(self, dsn=None, transport=None):    if not dsn and os.environ.get('SENTRY_DSN'):                                # 获取项目的url配置信息        msg = "Configuring Raven from environment variable 'SENTRY_DSN'"        self.logger.debug(msg)        dsn = os.environ['SENTRY_DSN']                                          # 获取dsn    if dsn not in self._transport_cache:                                        # 如果dsn不在缓存的信息中        if not dsn:                                                             # 没有传入dsn            result = RemoteConfig(transport=transport)                          # 初始化配置        else:            result = RemoteConfig.from_string(                dsn,                transport=transport,                transport_registry=self._registry,            )                                                                   # 从字符串初始化        self._transport_cache[dsn] = result                                     # 防止到缓存中        self.remote = result                                                    # 设置    else:        self.remote = self._transport_cache[dsn]                                # 直接赋值    self.logger.debug("Configuring Raven for host: {0}".format(self.remote))class RemoteConfig(object):    def __init__(self, base_url=None, project=None, public_key=None,                 secret_key=None, transport=None, options=None):        if base_url:            base_url = base_url.rstrip('/')            store_endpoint = '%s/api/%s/store/' % (base_url, project)        else:            store_endpoint = None        self.base_url = base_url                                                # 配置传入的项目url        self.project = project                                                  # 获取项目信息        self.public_key = public_key                                            # 获取配置的公钥信息        self.secret_key = secret_key                                            # 获取配置的私钥信息        self.options = options or {}        self.store_endpoint = store_endpoint        self._transport_cls = transport or DEFAULT_TRANSPORT                    # 初始化使用默认的传输方式

主要就是完成了对传输方式的配置和对远端信息的配置,分解项目的公钥与私钥。install_logging_hook方法的执行过程如下;

def install_logging_hook(self):    from raven.breadcrumbs import install_logging_hook    install_logging_hook()                                      # 调用加载方法@once                                                           # 执行一次装饰器def install_logging_hook():    """Installs the logging hook if it was not installed yet.  Otherwise    does nothing.    """    _patch_logger()                                             # 调用该方法def _patch_logger():    cls = logging.Logger                                        # 获取标准库中的Logger类    methods = {        'debug': logging.DEBUG,        'info': logging.INFO,        'warning': logging.WARNING,        'warn': logging.WARN,        'error': logging.ERROR,        'exception': logging.ERROR,        'critical': logging.CRITICAL,        'fatal': logging.FATAL    }                                                           # 获取所有的方法    for method_name, level in iteritems(methods):               # 遍历该列表所有值        new_func = _wrap_logging_method(            getattr(cls, method_name), level)                   # 包装到_wrap_logging_method方法中        setattr(logging.Logger, method_name, new_func)          # 重新赋值    logging.Logger.log = _wrap_logging_method(        logging.Logger.log)                                     # 重新包装log方法def _wrap_logging_method(meth, level=None):    if not isinstance(meth, FunctionType):                      # 判断是否函数类型        func = meth.im_func    else:        func = meth                                             # 直接赋值函数    # We were patched for raven before    if getattr(func, '__patched_for_raven__', False):           # 如果获取到该属性为真 则直接返回        return    if level is None:        args = ('level', 'msg')                                         fwd = 'level, msg'    else:        args = ('msg',)        fwd = '%d, msg' % level    code = get_code(func)    # This requires a bit of explanation why we're doing this.  Due to how    # logging itself works we need to pretend that the method actually was    # created within the logging module.  There are a few ways to detect    # this and we fake all of them: we use the same function globals (the    # one from the logging module), we create it entirely there which    # means that also the filename is set correctly.  This fools the    # detection code in logging and it makes logging itself skip past our    # code when determining the code location.    #    # Because we point the globals to the logging module we now need to    # refer to our own functions (original and the crumb recording    # function) through a closure instead of the global scope.    #    # We also add a lot of newlines in front of the code so that the    # code location lines up again in case someone runs inspect.getsource    # on the function.    ns = {}    eval(compile('''%(offset)sif 1:    def factory(original, record_crumb):        def %(name)s(self, %(args)s, *args, **kwargs):            record_crumb(self, %(fwd)s, *args, **kwargs)            return original(self, %(args)s, *args, **kwargs)        return %(name)s    \n''' % {        'offset': '\n' * (code.co_firstlineno - 3),        'name': func.__name__,        'args': ', '.join(args),        'fwd': fwd,        'level': level,    }, logging._srcfile, 'exec'), logging.__dict__, ns)                     # 获取函数属性    new_func = ns['factory'](meth, _record_log_breadcrumb)                  # 传入该函数并在执行meth函数之前执行_record_log_breadcrumb函数    new_func.__doc__ = func.__doc__                                         # 获取函数的文档说明    assert code.co_firstlineno == get_code(func).co_firstlineno    # In theory this should already be set correctly, but in some cases    # it is not.  So override it.    new_func.__module__ == func.__module__                                  # 设置新函数的相关属性    new_func.__name__ == func.__name__    new_func.__patched_for_raven__ = True                                   # 设置为真    return new_func                                                         # 返回函数

该函数执行完成后,就在每次调用log的打印函数之前都执行_record_log_breadcrumb函数;执行完成该函数后,执行了hook_libraries函数,给相关库里面的信息添加一些属性;

def hook_libraries(self, libraries):    from raven.breadcrumbs import hook_libraries    hook_libraries(libraries)hooked_libraries = {}def libraryhook(name):    def decorator(f):        f = once(f)                                             # 调用一次        hooked_libraries[name] = f                              # 配置到hooked_libraries字典中        return f                                                # 返回该函数    return decorator                                            # 返回该装饰器@libraryhook('requests') def _hook_requests():    try:        from requests.sessions import Session                   # 获取requests中的Session    except ImportError:        return    real_send = Session.send                                    # 获取本身的属性值    def send(self, request, *args, **kwargs):        def _record_request(response):            record(type='http', category='requests', data={                'url': request.url,                'method': request.method,                'status_code': response and response.status_code or None,                'reason': response and response.reason or None,            })        try:            resp = real_send(self, request, *args, **kwargs)    # 使用库原本的发送函数        except Exception:            _record_request(None)                               # 如果报错则记录            raise        else:            _record_request(resp)                               # 如果执行成功则记录成功的返回信息        return resp                                             # 返回结果    Session.send = send                                         # 重置Session的send方法    ignore_logger('requests.packages.urllib3.connectionpool',                  allow_level=logging.WARNING)@libraryhook('httplib')def _install_httplib():    try:        from httplib import HTTPConnection                              # 导入HTTPConnection    except ImportError:         from http.client import HTTPConnection                          # 导入失败则导入client中的HTTPConnection    real_putrequest = HTTPConnection.putrequest                         # 获取处理请求函数    real_getresponse = HTTPConnection.getresponse                       # 获取返回结果函数    def putrequest(self, method, url, *args, **kwargs):        self._raven_status_dict = status = {}                               # 设置对应的字典属性值        host = self.host                                                    # 获取域名与端口        port = self.port        default_port = self.default_port        def processor(data):            real_url = url            if not real_url.startswith(('http://', 'https://')):                real_url = '%s://%s%s%s' % (                    default_port == 443 and 'https' or 'http',                    host,                    port != default_port and ':%s' % port or '',                    url,                )            data['data'] = {                'url': real_url,                'method': method,            }            data['data'].update(status)            return data        record(type='http', category='requests', processor=processor)       # 记录当前请求的数据值        return real_putrequest(self, method, url, *args, **kwargs)          # 调用发送请求函数    def getresponse(self, *args, **kwargs):        rv = real_getresponse(self, *args, **kwargs)                        # 调用本身处理的该函数处理        status = getattr(self, '_raven_status_dict', None)                  # 获取该属性值        if status is not None and 'status_code' not in status:              # 如果不为空            status['status_code'] = rv.status                               # 设置相关状态和原因            status['reason'] = rv.reason        return rv    HTTPConnection.putrequest = putrequest                                  # 重置该属性的函数    HTTPConnection.getresponse = getresponse                                def hook_libraries(libraries):    if libraries is None:                                                   # 如果传入的为空        libraries = hooked_libraries.keys()                                 # 使用默认的    for lib in libraries:        func = hooked_libraries.get(lib)                                    # 获取对应配置的函数        if func is None:            raise RuntimeError('Unknown library %r for hooking' % lib)         func()                                                              # 如果函数不为空则调用该函数

至此,主要的client的初始化方法都执行完成,初始化生成的client就设置为一个全局的实例.

初始化完成后,在view处理请求报错的时候就会通过在WSGIHandler中包装的exception来进行处理,即convert_exception_to_response函数进行报错处理;

def convert_exception_to_response(get_response):    """    Wrap the given get_response callable in exception-to-response conversion.    All exceptions will be converted. All known 4xx exceptions (Http404,    PermissionDenied, MultiPartParserError, SuspiciousOperation) will be    converted to the appropriate response, and all other exceptions will be    converted to 500 responses.    This decorator is automatically applied to all middleware to ensure that    no middleware leaks an exception and that the next middleware in the stack    can rely on getting a response instead of an exception.    """    @wraps(get_response, assigned=available_attrs(get_response))    def inner(request):        try:            response = get_response(request)                            # 通过view处理请求        except Exception as exc:            response = response_for_exception(request, exc)             # 转换报错信息        return response                                                 # 返回处理报错信息    return inner def response_for_exception(request, exc):    if isinstance(exc, Http404):                                                                # 判断是否是404        if settings.DEBUG:                                                                      # 判断是否是调试状态            response = debug.technical_404_response(request, exc)                               # 返回详细的调试出错信息        else:            response = get_exception_response(request, get_resolver(get_urlconf()), 404, exc)   # 获取基本的报错信息    elif isinstance(exc, PermissionDenied):                                                     # 是否是权限不允许        logger.warning(            'Forbidden (Permission denied): %s', request.path,            extra={'status_code': 403, 'request': request},        )        response = get_exception_response(request, get_resolver(get_urlconf()), 403, exc)       # 转换成403    elif isinstance(exc, MultiPartParserError):        logger.warning(            'Bad request (Unable to parse request body): %s', request.path,            extra={'status_code': 400, 'request': request},        )        response = get_exception_response(request, get_resolver(get_urlconf()), 400, exc)    elif isinstance(exc, SuspiciousOperation):        # The request logger receives events for any problematic request        # The security logger receives events for all SuspiciousOperations        security_logger = logging.getLogger('django.security.%s' % exc.__class__.__name__)        security_logger.error(            force_text(exc),            extra={'status_code': 400, 'request': request},        )        if settings.DEBUG:            response = debug.technical_500_response(request, *sys.exc_info(), status_code=400)        else:            response = get_exception_response(request, get_resolver(get_urlconf()), 400, exc)    elif isinstance(exc, SystemExit):                                                           # 是否是系统退出        # Allow sys.exit() to actually exit. See tickets #1023 and #4701        raise    else:        signals.got_request_exception.send(sender=None, request=request)        response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())  # 处理其他异常信息    return response                                                                                 # 返回异常信息

其中,无论是调用handler_uncaught_exception或者get_exception_response方法本质都会调用到handle_uncaught_exception函数,继续查看该函数;

def handle_uncaught_exception(request, resolver, exc_info):    """    Processing for any otherwise uncaught exceptions (those that will    generate HTTP 500 responses).    """    if settings.DEBUG_PROPAGATE_EXCEPTIONS:        raise    logger.error(        'Internal Server Error: %s', request.path,        exc_info=exc_info,        extra={'status_code': 500, 'request': request},    )                                                                       # 打印error的日志此时的logger是获取的为django.request,在django.request中配置了sentry的error处理handler    if settings.DEBUG:        return debug.technical_500_response(request, *exc_info)             # 根据是否是调试模式返回详细信息    # If Http500 handler is not installed, reraise the last exception.    if resolver.urlconf_module is None:        six.reraise(*exc_info)    # Return an HttpResponse that displays a friendly error message.    callback, param_dict = resolver.resolve_error_handler(500)              # 展示友好的错误信息    return callback(request, **param_dict)

此时根据上一篇博文的分析内容可知,此时就是调用了SentryHandler进行打印日志的输出,由于日志输出的handler的执行流程会执行到emit函数,即调用了SentryHandler的emit函数进行具体的业务处理;

class SentryHandler(BaseSentryHandler):    def __init__(self, *args, **kwargs):        # TODO(dcramer): we'd like to avoid this duplicate code, but we need        # to currently defer loading client due to Django loading patterns.        self.tags = kwargs.pop('tags', None)        logging.Handler.__init__(self, level=kwargs.get('level', logging.NOTSET))   # 调用父类logging.Handler的初始化方法    @memoize                                                                        # 记住结果的装饰器    def client(self):        # Import must be lazy for deffered Django loading        from raven.contrib.django.models import client        return client                                                               # 返回client    def _emit(self, record):        request = getattr(record, 'request', None)                                  # 获取record的request属性        return super(SentryHandler, self)._emit(record, request=request)            # 调用父类的_emit方法class SentryHandler(logging.Handler, object):    def __init__(self, *args, **kwargs):        client = kwargs.get('client_cls', Client)                                   # 获取client属性值        if len(args) == 1:            arg = args[0]            if isinstance(arg, string_types):                self.client = client(dsn=arg, **kwargs)                             # 字符串类型直接初始化            elif isinstance(arg, Client):                                           # 是否是Client实例                self.client = arg            else:                raise ValueError('The first argument to %s must be either a '                                 'Client instance or a DSN, got %r instead.' %                                 (self.__class__.__name__, arg,))        elif 'client' in kwargs:            self.client = kwargs['client']                                          # 检查client是否在键参数中        else:            self.client = client(*args, **kwargs)                                   # 初始化client实例        self.tags = kwargs.pop('tags', None)        logging.Handler.__init__(self, level=kwargs.get('level', logging.NOTSET))   # 调用Handler的初始化方法    def can_record(self, record):        return not (            record.name == 'raven' or            record.name.startswith(('sentry.errors', 'raven.'))        )                                                                           # 检查record是否为raven    def emit(self, record):        try:            # Beware to python3 bug (see #10805) if exc_info is (None, None, None)            self.format(record)                                                     # 先序列化            if not self.can_record(record):                                         # 是否可以记录                print(to_string(record.message), file=sys.stderr)                return            return self._emit(record)                                               # 发送记录        except Exception:            if self.client.raise_send_errors:                raise            print("Top level Sentry exception caught - failed "                  "creating log record", file=sys.stderr)            print(to_string(record.msg), file=sys.stderr)            print(to_string(traceback.format_exc()), file=sys.stderr)    ...     def _emit(self, record, **kwargs):        data, extra = extract_extra(record)                                 # 获取extra参数值        stack = getattr(record, 'stack', None)                              # 获取record的stack属性值        if stack is True:            stack = iter_stack_frames()                                     # 获取栈信息        if stack:            stack = self._get_targetted_stack(stack, record)                # 如果有则记录跟踪栈信息        date = datetime.datetime.utcfromtimestamp(record.created)           # 获取序列化的创建时间        event_type = 'raven.events.Message'                                 # 消息类型        handler_kwargs = {            'params': record.args,        }        try:            handler_kwargs['message'] = text_type(record.msg)               # 转换需要记录的信息        except UnicodeDecodeError:            # Handle binary strings where it should be unicode...            handler_kwargs['message'] = repr(record.msg)[1:-1]        try:            handler_kwargs['formatted'] = text_type(record.message)        # 格式化信息        except UnicodeDecodeError:            # Handle binary strings where it should be unicode...            handler_kwargs['formatted'] = repr(record.message)[1:-1]        # If there's no exception being processed, exc_info may be a 3-tuple of None        # http://docs.python.org/library/sys.html#sys.exc_info        if record.exc_info and all(record.exc_info):            # capture the standard message first so that we ensure            # the event is recorded as an exception, in addition to having our            # message interface attached            handler = self.client.get_handler(event_type)                   # 获取handler的处理类型并转换成对应的信息格式            data.update(handler.capture(**handler_kwargs))                  # 更新data对应的参数值            event_type = 'raven.events.Exception'                           # 更改类型            handler_kwargs = {'exc_info': record.exc_info}                  # 填充键参数        data['level'] = record.levelno                                      # 日志等级        data['logger'] = record.name                                        # 日志名称        if hasattr(record, 'tags'):                                         # 获取record的tags            kwargs['tags'] = record.tags        elif self.tags:            kwargs['tags'] = self.tags                                      # 获取tags值        kwargs.update(handler_kwargs)                                       # 更新位置参数值        return self.client.capture(            event_type, stack=stack, data=data,            extra=extra, date=date, **kwargs)                               # 调用client的capture发送信息

在进行相关信息的格式化与初始化之后就调用了client的capture方法来将信息发送出去,此时发送出去的具体执行函数流程如下;

def capture(self, event_type, request=None, **kwargs):    if 'data' not in kwargs:                                                # 检查data是否存在键参数中        kwargs['data'] = data = {}    else:        data = kwargs['data']                                               # 获取data    if request is None:                                                     # 如果request为空        request = getattr(SentryLogMiddleware.thread, 'request', None)      # 通过中间件获取对应的request值    is_http_request = isinstance(request, HttpRequest)                      # 检查request是否是HttpRequest实例    if is_http_request:                                                     # 如果是HttpRequest实例        data.update(self.get_data_from_request(request))                    # 从request中获取对应的用户信息    if kwargs.get('exc_info'):                                              # 获取exc_info对应的数据        exc_value = kwargs['exc_info'][1]                                   # 获取对应的值        # As of r16833 (Django) all exceptions may contain a        # ``django_template_source`` attribute (rather than the legacy        # ``TemplateSyntaxError.source`` check) which describes        # template information.  As of Django 1.9 or so the new        # template debug thing showed up.        if hasattr(exc_value, 'django_template_source') or \           ((isinstance(exc_value, TemplateSyntaxError) and            isinstance(getattr(exc_value, 'source', None),                       (tuple, list)) and            isinstance(exc_value.source[0], Origin))) or \           hasattr(exc_value, 'template_debug'):            source = getattr(exc_value, 'django_template_source',                             getattr(exc_value, 'source', None))            debug = getattr(exc_value, 'template_debug', None)            if source is None:                self.logger.info('Unable to get template source from exception')            data.update(get_data_from_template(source, debug))    result = super(DjangoClient, self).capture(event_type, **kwargs)        # 调用父类的capture方法    if is_http_request and result:                                          # 如果是http_request并且result有值        # attach the sentry object to the request        request.sentry = {            'project_id': data.get('project', self.remote.project),            'id': result,        }                                                                   # 设置request中的sentry属性值    return resultdef capture(self, event_type, data=None, date=None, time_spent=None,            extra=None, stack=None, tags=None, **kwargs):    if not self.is_enabled():                                                   # 检查sentry配置的远端配置信息是否完整        return    exc_info = kwargs.get('exc_info')                                           # 获取exec_info信息值    if exc_info is not None:                                                    #         if self.skip_error_for_logging(exc_info):                               # 是否跳过            return        elif not self.should_capture(exc_info):                                 # 是否会被忽略            self.logger.info(                'Not capturing exception due to filters: %s', exc_info[0],                exc_info=sys.exc_info())            return        self.record_exception_seen(exc_info)                                    # 添加到忽略列表中                              data = self.build_msg(        event_type, data, date, time_spent, extra, stack, tags=tags,        **kwargs)                                                               # 序列化相关字典信息并返回    self.send(**data)                                                           # 发送该获取的字典信息    return data['event_id']                                                     # 返回事件iddef send(self, auth_header=None, **data):    """    Serializes the message and passes the payload onto ``send_encoded``.    """    message = self.encode(data)                                                 # 对需要发送的数据进行编码    return self.send_encoded(message, auth_header=auth_header)                  # 带哦用发送函数def send_encoded(self, message, auth_header=None, **kwargs):    """    Given an already serialized message, signs the message and passes the    payload off to ``send_remote``.    """    client_string = 'raven-python/%s' % (raven.VERSION,)                        # 获取版本信息    if not auth_header:        timestamp = time.time()                                                 # 如果没有头部信息 获取当前时间信息        auth_header = get_auth_header(            protocol=self.protocol_version,            timestamp=timestamp,            client=client_string,            api_key=self.remote.public_key,            api_secret=self.remote.secret_key,        )                                                                       # 获取认证的信息    headers = {        'User-Agent': client_string,        'X-Sentry-Auth': auth_header,        'Content-Encoding': self.get_content_encoding(),        'Content-Type': 'application/octet-stream',    }                                                                           # 获取请求头部信息    return self.send_remote(        url=self.remote.store_endpoint,        data=message,        headers=headers,        **kwargs    )                                                                           # 调用发送函数def send_remote(self, url, data, headers=None):    # If the client is configured to raise errors on sending,    # the implication is that the backoff and retry strategies    # will be handled by the calling application    if headers is None:                                                         # 检查头部是否为空        headers = {}    if not self.raise_send_errors and not self.state.should_try():        data = self.decode(data)        self._log_failed_submission(data)        return    self.logger.debug('Sending message of length %d to %s', len(data), url)    def failed_send(e):        self._failed_send(e, url, self.decode(data))    try:        transport = self.remote.get_transport()                                 # 获取发送请求的方式 默认为ThreadedHTTPTransport类发送        if transport.async:                                                     # 是否是异步发送            transport.async_send(data, headers, self._successful_send,                                 failed_send)        else:            transport.send(data, headers)                                       # 同步发送信息            self._successful_send()                                             # 请求成功    except Exception as e:        if self.raise_send_errors:            raise        failed_send(e)                                                          # 发送失败

由于此时调用的是ThreadedHTTPTransport的发送方式,继续查看该类;

class ThreadedHTTPTransport(AsyncTransport, HTTPTransport):    scheme = ['http', 'https', 'threaded+http', 'threaded+https']    def get_worker(self):        if not hasattr(self, '_worker') or not self._worker.is_alive():            self._worker = AsyncWorker()                                # 获取worker        return self._worker                                             # 返回该worker    def send_sync(self, data, headers, success_cb, failure_cb):        try:            super(ThreadedHTTPTransport, self).send(data, headers)      # 异步发送数据        except Exception as e:            failure_cb(e)        else:            success_cb()    def async_send(self, data, headers, success_cb, failure_cb):        self.get_worker().queue(            self.send_sync, data, headers, success_cb, failure_cb)      # 加入到执行队列中class AsyncWorker(object):    _terminator = object()    def __init__(self, shutdown_timeout=DEFAULT_TIMEOUT):        check_threads()        self._queue = Queue(-1)                                         # 初始化队列        self._lock = threading.Lock()                                   # 获取线程锁        self._thread = None                                                     self._thread_for_pid = None        self.options = {            'shutdown_timeout': shutdown_timeout,        }                                                               # 获取过期超时时间        self.start()                                                    # 开始运行    def is_alive(self):        if self._thread_for_pid != os.getpid():                         # 检查当前的pid是否相同            return False        return self._thread and self._thread.is_alive()                 # 检查当前线程是否任然存货    def _ensure_thread(self):        if self.is_alive():                                             # 保证当前线程执行            return        self.start()                                                    # 开始    def main_thread_terminated(self):        self._lock.acquire()                                                # 获取线程锁        try:            if not self.is_alive():                # thread not started or already stopped - nothing to do                return                                                      # 如果存货则什么都不做            # wake the processing thread up            self._queue.put_nowait(self._terminator)                        # 在队列中写入终止            timeout = self.options['shutdown_timeout']                      # 获取超时时间            # wait briefly, initially            initial_timeout = 0.1            if timeout < initial_timeout:                initial_timeout = timeout                                   # 设置超时时间            if not self._timed_queue_join(initial_timeout):                 #                 # if that didn't work, wait a bit longer                # NB that size is an approximation, because other threads may                # add or remove items                size = self._queue.qsize()                print("Sentry is attempting to send %i pending error messages"                      % size)                print("Waiting up to %s seconds" % timeout)                if os.name == 'nt':                    print("Press Ctrl-Break to quit")                else:                    print("Press Ctrl-C to quit")                self._timed_queue_join(timeout - initial_timeout)            self._thread = None        finally:            self._lock.release()                                            # 释放锁    def _timed_queue_join(self, timeout):        """        implementation of Queue.join which takes a 'timeout' argument        returns true on success, false on timeout        """        deadline = time() + timeout        queue = self._queue        queue.all_tasks_done.acquire()        try:            while queue.unfinished_tasks:                delay = deadline - time()                if delay <= 0:                    # timed out                    return False                queue.all_tasks_done.wait(timeout=delay)            return True        finally:            queue.all_tasks_done.release()    def start(self):        """        Starts the task thread.        """        self._lock.acquire()                                                        # 获取线程锁        try:            if not self.is_alive():                                                 # 检查自己是否存货                self._thread = threading.Thread(target=self._target, name="raven.AsyncWorker")  # 开启一个线程 执行self._target线程                self._thread.setDaemon(True)                                        # 设置为守护线程                self._thread.start()                                                # 开始执行                self._thread_for_pid = os.getpid()                                  # 获取pid        finally:            self._lock.release()                                                    # 释放线程锁            atexit.register(self.main_thread_terminated)    def stop(self, timeout=None):        """        Stops the task thread. Synchronous!        """        self._lock.acquire()        try:            if self._thread:                self._queue.put_nowait(self._terminator)                self._thread.join(timeout=timeout)                self._thread = None                self._thread_for_pid = None        finally:            self._lock.release()    def queue(self, callback, *args, **kwargs):        self._ensure_thread()        self._queue.put_nowait((callback, args, kwargs))                            # 将要处理的内容传入队列中    def _target(self):        while True:            record = self._queue.get()                                              # 遍历循环获取队列中的值            try:                if record is self._terminator:                                      # 检查是否停止执行                    break                callback, args, kwargs = record                                     # 获取参数                try:                    callback(*args, **kwargs)                                       # 调用回调函数                except Exception:                    logger.error('Failed processing job', exc_info=True)            finally:                self._queue.task_done()                                             # 任务完成            sleep(0)

由于raven自己重写了Queue方法,此时的就是启动了一个线程专门用户处理队列中的请求,然后通过传入callback和对应的参数值,就直接调用回调方法执行该方法就是send_sync;

def send_sync(self, data, headers, success_cb, failure_cb):    try:        super(ThreadedHTTPTransport, self).send(data, headers)                   # 调用父类的send方法,其实调用HTTPTransport的send方法    except Exception as e:        failure_cb(e)    else:        success_cb()class HTTPTransport(Transport):    scheme = ['sync+http', 'sync+https']    def __init__(self, parsed_url, timeout=defaults.TIMEOUT, verify_ssl=True,                 ca_certs=defaults.CA_BUNDLE):        self._parsed_url = parsed_url        self._url = parsed_url.geturl().rsplit('+', 1)[-1]        if isinstance(timeout, string_types):            timeout = int(timeout)        if isinstance(verify_ssl, string_types):            verify_ssl = bool(int(verify_ssl))        self.timeout = timeout        self.verify_ssl = verify_ssl        self.ca_certs = ca_certs    def send(self, data, headers):        """        Sends a request to a remote webserver using HTTP POST.        """        req = urllib2.Request(self._url, headers=headers)                       # 获取头部信息        try:            response = urlopen(                url=req,                data=data,                timeout=self.timeout,                verify_ssl=self.verify_ssl,                ca_certs=self.ca_certs,            )                                                                   # 将数据提交到远端        except urllib2.HTTPError as exc:            msg = exc.headers.get('x-sentry-error')            code = exc.getcode()            if code == 429:                try:                    retry_after = int(exc.headers.get('retry-after'))                except (ValueError, TypeError):                    retry_after = 0                raise RateLimited(msg, retry_after)            elif msg:                raise APIError(msg, code)            else:                raise        return response                                                         # 返回处理结果

至此,一个错误的获取到发送的整个流程就已经完成,raven做了大量细节的工作,其中有许多细节没有一一分析全面,但是大概的执行流程已经完成,大家有兴趣课自行详细查阅对应的源码分析。

总结

本文主要讲述了Django项目中的sentry对应的错误跟踪的分析过程,raven针对Django框架做了很多的配置化的内容进行了完成,主要的思想就是通过Django的日志系统进行了配置,将Django项目中的错误,提交到sentry对应的项目网站上,方便开发人员跟踪,鉴于本人才疏学浅,如有疏漏请批评指正。

转载地址:https://blog.csdn.net/qq_33339479/article/details/85096785 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Python标准库asyncio模块基本原理浅析
下一篇:Django项目日志概述

发表评论

最新留言

感谢大佬
[***.8.128.20]2024年03月29日 15时05分53秒