from __future__ import absolute_import import time import logging from types import FunctionType from raven._compat import iteritems, get_code, text_type, string_types from raven.utils import once special_logger_handlers = {} logger = logging.getLogger('raven') def event_payload_considered_equal(a, b): return ( a['type'] == b['type'] and a['level'] == b['level'] and a['message'] == b['message'] and a['category'] == b['category'] and a['data'] == b['data'] ) class BreadcrumbBuffer(object): def __init__(self, limit=100): self.buffer = [] self.limit = limit def record(self, type, timestamp=None, level=None, message=None, category=None, data=None, processor=None): if timestamp is None: timestamp = time.time() self.buffer.append(({ 'type': type, 'timestamp': timestamp, 'level': level, 'message': message, 'category': category, 'data': data, }, processor)) del self.buffer[:-self.limit] def clear(self): del self.buffer[:] def get_buffer(self): rv = [] for idx, (payload, processor) in enumerate(self.buffer): if processor is not None: try: processor(payload) except Exception: logger.exception('Failed to process breadcrumbs. Ignored') payload = None self.buffer[idx] = (payload, None) if payload is not None and \ (not rv or not event_payload_considered_equal(rv[-1], payload)): rv.append(payload) return rv class BlackholeBreadcrumbBuffer(BreadcrumbBuffer): def record(self, *args, **kwargs): pass def make_buffer(enabled=True): if enabled: return BreadcrumbBuffer() return BlackholeBreadcrumbBuffer() def record_breadcrumb(type, timestamp=None, level=None, message=None, category=None, data=None, processor=None): """Records a breadcrumb for all active clients. This is what integration code should use rather than invoking the `captureBreadcrumb` method on a specific client. """ if timestamp is None: timestamp = time.time() for ctx in raven.context.get_active_contexts(): ctx.breadcrumbs.record(type, timestamp, level, message, category, data, processor) def _record_log_breadcrumb(logger, level, msg, *args, **kwargs): handler = special_logger_handlers.get(logger.name) if handler is not None: rv = handler(logger, level, msg, args, kwargs) if rv: return def processor(data): formatted_msg = msg # If people log bad things, this can happen. Then just don't do # anything. try: formatted_msg = text_type(msg) if args: formatted_msg = msg % args except Exception: pass # We do not want to include exc_info as argument because it often # lies (set to a constant value like 1 or True) or even if it's a # tuple it will not be particularly useful for us as we cannot # process it anyways. kwargs.pop('exc_info', None) data.update({ 'message': formatted_msg, 'category': logger.name, 'level': logging.getLevelName(level).lower(), 'data': kwargs, }) record_breadcrumb('default', processor=processor) 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) new_func.__doc__ = func.__doc__ assert code.co_firstlineno == get_code(func).co_firstlineno assert new_func.__module__ == func.__module__ assert new_func.__name__ == func.__name__ new_func.__patched_for_raven__ = True return new_func def _patch_logger(): cls = logging.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) setattr(logging.Logger, method_name, new_func) logging.Logger.log = _wrap_logging_method( logging.Logger.log) @once def install_logging_hook(): """Installs the logging hook if it was not installed yet. Otherwise does nothing. """ _patch_logger() def ignore_logger(name_or_logger, allow_level=None): """Ignores a logger for the regular breadcrumb code. This is useful for framework integration code where some log messages should be specially handled. """ def handler(logger, level, msg, args, kwargs): if allow_level is not None and \ level >= allow_level: return False return True register_special_log_handler(name_or_logger, handler) def register_special_log_handler(name_or_logger, callback): """Registers a callback for log handling. The callback is invoked with give arguments: `logger`, `level`, `msg`, `args` and `kwargs` which are the values passed to the logging system. If the callback returns `True` the default handling is disabled. """ if isinstance(name_or_logger, string_types): name = name_or_logger else: name = name_or_logger.name special_logger_handlers[name] = callback hooked_libraries = {} def libraryhook(name): def decorator(f): f = once(f) hooked_libraries[name] = f return f return decorator @libraryhook('requests') def _hook_requests(): try: from requests.sessions import Session except ImportError: return real_send = Session.send def send(self, request, *args, **kwargs): def _record_request(response): record_breadcrumb('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 ignore_logger('requests.packages.urllib3.connectionpool', allow_level=logging.WARNING) @libraryhook('httplib') def _install_httplib(): try: from httplib import HTTPConnection except ImportError: from http.client import 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_breadcrumb('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() import raven.context