Hello community, here is the log from the commit of package python3-raven for openSUSE:Factory checked in at 2016-05-30 09:58:35 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python3-raven (Old) and /work/SRC/openSUSE:Factory/.python3-raven.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python3-raven" Changes: -------- --- /work/SRC/openSUSE:Factory/python3-raven/python3-raven.changes 2016-05-25 21:26:06.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.python3-raven.new/python3-raven.changes 2016-05-30 09:58:40.000000000 +0200 @@ -1,0 +2,44 @@ +Sat May 28 19:34:06 UTC 2016 - [email protected] + +- update to version 5.19.0: + * remove duration from SQL query breadcrumbs. This was not rendered + in the UI and will come back in future versions of Sentry with a + different interface. + * resolved a bug that caused crumbs to be recorded incorrectly. + +- changes from version 5.18.0: + * Breadcrumbs are now attempted to be deduplicated to catch some + common cases where log messages just spam up the breadcrumbs. + * Improvements to the public breadcrumbs API and stabilized some. + * Automatically activate the context on calls to `merge` + +- changes from version 5.17.0: + * if breadcrumbs fail to process due to an error they are now + skipped. + +- changes from version 5.16.0: + * exc_info is no longer included in logger based breadcrumbs. + * log the entire logger name as category. + * added a `enable_breadcrumbs` flag to the client to allow the + enabling or disabling of breadcrumbs quickly. + * corrected an issue where python interpreters with bytecode writing + enabled would report incorrect logging locations when breadcrumb + patching for logging was enabled. + +- changes from version 5.15.0: + * Improve thread binding for the context. This makes the main + thread never deactivate the client automatically on clear which + means that more code should automatically support breadcrumbs + without changes. + +- changes from version 5.14.0: + * Added support for reading git sha's from packed references. + * Detect disabled thread support for uwsgi. + * Added preliminary support for breadcrumbs. + +- changes from version 5.13.0: + * Resolved an issue where Raven would fail with an exception if the + package name did not match the setuptools name in some isolated + cases. + +------------------------------------------------------------------- @@ -6 +49,0 @@ - Old: ---- raven-5.12.0.tar.gz New: ---- raven-5.19.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python3-raven.spec ++++++ --- /var/tmp/diff_new_pack.HxsDAj/_old 2016-05-30 09:58:43.000000000 +0200 +++ /var/tmp/diff_new_pack.HxsDAj/_new 2016-05-30 09:58:43.000000000 +0200 @@ -17,7 +17,7 @@ Name: python3-raven -Version: 5.12.0 +Version: 5.19.0 Release: 0 Url: https://pypi.python.org/pypi/raven Summary: A client for Sentry ++++++ raven-5.12.0.tar.gz -> raven-5.19.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/PKG-INFO new/raven-5.19.0/PKG-INFO --- old/raven-5.12.0/PKG-INFO 2016-03-30 00:07:14.000000000 +0200 +++ new/raven-5.19.0/PKG-INFO 2016-05-27 18:55:30.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: raven -Version: 5.12.0 +Version: 5.19.0 Summary: Raven is a client for Sentry (https://getsentry.com) Home-page: https://github.com/getsentry/raven-python Author: Sentry diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/__init__.py new/raven-5.19.0/raven/__init__.py --- old/raven-5.12.0/raven/__init__.py 2015-07-12 10:30:42.000000000 +0200 +++ new/raven-5.19.0/raven/__init__.py 2016-04-22 23:47:13.000000000 +0200 @@ -9,10 +9,6 @@ import os import os.path -from raven.base import * # NOQA -from raven.conf import * # NOQA -from raven.versioning import * # NOQA - __all__ = ('VERSION', 'Client', 'get_version') @@ -55,3 +51,9 @@ __build__ = get_revision() __docformat__ = 'restructuredtext en' + + +# Declare child imports last to prevent recursion +from raven.base import * # NOQA +from raven.conf import * # NOQA +from raven.versioning import * # NOQA diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/_compat.py new/raven-5.19.0/raven/_compat.py --- old/raven-5.12.0/raven/_compat.py 2016-01-13 17:52:15.000000000 +0100 +++ new/raven-5.19.0/raven/_compat.py 2016-05-03 00:45:35.000000000 +0200 @@ -175,3 +175,10 @@ def with_metaclass(meta, base=object): """Create a base class with a metaclass.""" return meta("NewBase", (base,), {}) + + +def get_code(func): + rv = getattr(func, '__code__', getattr(func, 'func_code', None)) + if rv is None: + raise TypeError('Could not get code from %r' % type(func).__name__) + return rv diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/base.py new/raven-5.19.0/raven/base.py --- old/raven-5.12.0/raven/base.py 2016-03-30 00:06:55.000000000 +0200 +++ new/raven-5.19.0/raven/base.py 2016-05-19 19:41:50.000000000 +0200 @@ -25,10 +25,14 @@ else: import contextlib2 as contextlib +try: + from thread import get_ident as get_thread_ident +except ImportError: + from _thread import get_ident as get_thread_ident + import raven from raven.conf import defaults from raven.conf.remote import RemoteConfig -from raven.context import Context from raven.exceptions import APIError, RateLimited from raven.utils import json, get_versions, get_auth_header, merge_dicts from raven._compat import text_type, iteritems @@ -47,6 +51,11 @@ PLATFORM_NAME = 'python' +SDK_VALUE = { + 'name': 'raven-python', + 'version': raven.VERSION, +} + # singleton for the client Raven = None @@ -128,7 +137,8 @@ _registry = TransportRegistry(transports=default_transports) def __init__(self, dsn=None, raise_send_errors=False, transport=None, - install_sys_hook=True, **options): + install_sys_hook=True, install_logging_hook=True, + hook_libraries=None, enable_breadcrumbs=True, **options): global Raven o = options @@ -181,11 +191,22 @@ if Raven is None: Raven = self - self._context = Context() + # 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() + if install_logging_hook: + self.install_logging_hook() + + self.hook_libraries(hook_libraries) + def set_dsn(self, dsn=None, transport=None): if not dsn and os.environ.get('SENTRY_DSN'): msg = "Configuring Raven from environment variable 'SENTRY_DSN'" @@ -219,6 +240,14 @@ __excepthook__(*exc_info) sys.excepthook = handle_exception + def install_logging_hook(self): + from raven.breadcrumbs import install_logging_hook + install_logging_hook() + + def hook_libraries(self, libraries): + from raven.breadcrumbs import hook_libraries + hook_libraries(libraries) + @classmethod def register_scheme(cls, scheme, transport_class): cls._registry.register_scheme(scheme, transport_class) @@ -429,6 +458,17 @@ data.setdefault('time_spent', time_spent) data.setdefault('event_id', event_id) data.setdefault('platform', PLATFORM_NAME) + data.setdefault('sdk', SDK_VALUE) + + # insert breadcrumbs + if self.enable_breadcrumbs: + crumbs = self.context.breadcrumbs.get_buffer() + if crumbs: + # Make sure we send the crumbs here as "values" as we use the + # raven client internally in sentry and the alternative + # submission option of a list here is not supported by the + # internal sender. + data.setdefault('breadcrumbs', {'values': crumbs}) return data @@ -676,7 +716,7 @@ 'Content-Type': 'application/octet-stream', } - self.send_remote( + return self.send_remote( url=self.remote.store_endpoint, data=message, headers=headers, @@ -791,6 +831,17 @@ DeprecationWarning) return self.context(**kwargs) + def captureBreadcrumb(self, *args, **kwargs): + """Records a breadcrumb with the current context. They will be + sent with the next event. + """ + # Note: framework integration should not call this method but + # instead use the raven.breadcrumbs.record_breadcrumb function + # which will record to the correct client automatically. + self.context.breadcrumbs.record(*args, **kwargs) + + capture_breadcrumb = captureBreadcrumb + class DummyClient(Client): "Sends messages into an empty void" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/breadcrumbs.py new/raven-5.19.0/raven/breadcrumbs.py --- old/raven-5.12.0/raven/breadcrumbs.py 1970-01-01 01:00:00.000000000 +0100 +++ new/raven-5.19.0/raven/breadcrumbs.py 2016-05-27 18:26:35.000000000 +0200 @@ -0,0 +1,349 @@ +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, timestamp=None, level=None, message=None, + category=None, data=None, type=None, processor=None): + if not (message or data or processor): + raise ValueError('You must pass either `message`, `data`, ' + 'or `processor`') + if timestamp is None: + timestamp = time.time() + self.buffer.append(({ + 'type': type or 'default', + '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, *args, **kwargs): + # Legacy alias + kwargs['type'] = type + return record(*args, **kwargs) + + +def record(message=None, timestamp=None, level=None, category=None, + data=None, type=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(timestamp, level, message, category, + data, type, 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(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(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 + + 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(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() + + +import raven.context diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/context.py new/raven-5.19.0/raven/context.py --- old/raven-5.12.0/raven/context.py 2016-03-30 00:06:55.000000000 +0200 +++ new/raven-5.19.0/raven/context.py 2016-05-19 19:41:50.000000000 +0200 @@ -7,36 +7,27 @@ """ from __future__ import absolute_import -import time - from collections import Mapping, Iterable -from datetime import datetime from threading import local +from weakref import ref as weakref from raven._compat import iteritems +try: + from thread import get_ident as get_thread_ident +except ImportError: + from _thread import get_ident as get_thread_ident + -class BreadcrumbBuffer(object): +_active_contexts = local() - def __init__(self, limit=100): - self.buffer = [] - self.limit = limit - - def record(self, type, data=None, timestamp=None): - if timestamp is None: - timestamp = time.time() - elif isinstance(timestamp, datetime): - timestamp = datetime - - self.buffer.append({ - 'type': type, - 'timestamp': timestamp, - 'data': data or {}, - }) - del self.buffer[:-self.limit] - def clear(self): - del self.buffer[:] +def get_active_contexts(): + """Returns all the active contexts for the current thread.""" + try: + return list(_active_contexts.contexts) + except AttributeError: + return [] class Context(local, Mapping, Iterable): @@ -51,9 +42,36 @@ >>> finally: >>> context.clear() """ - def __init__(self): + + def __init__(self, client=None): + breadcrumbs = raven.breadcrumbs.make_buffer( + client is None or client.enable_breadcrumbs) + if client is not None: + client = weakref(client) + self._client = client + # Because the thread auto activates the thread local this also + # means that we auto activate this thing. Only if someone decides + # to deactivate manually later another call to activate is + # technically necessary. + self.activate() self.data = {} self.exceptions_to_skip = set() + self.breadcrumbs = breadcrumbs + + @property + def client(self): + if self._client is None: + return None + return self._client() + + def __hash__(self): + return id(self) + + def __eq__(self, other): + return self is other + + def __ne__(self, other): + return not self.__eq__(other) def __getitem__(self, key): return self.data[key] @@ -67,7 +85,27 @@ def __repr__(self): return '<%s: %s>' % (type(self).__name__, self.data) - def merge(self, data): + def __enter__(self): + self.activate() + return self + + def __exit__(self, exc_type, exc_value, tb): + self.deactivate() + + def activate(self, sticky=False): + if sticky: + self._sticky_thread = get_thread_ident() + _active_contexts.__dict__.setdefault('contexts', set()).add(self) + + def deactivate(self): + try: + _active_contexts.contexts.discard(self) + except AttributeError: + pass + + def merge(self, data, activate=True): + if activate: + self.activate() d = self.data for key, value in iteritems(data): if key in ('tags', 'extra'): @@ -83,6 +121,21 @@ def get(self): return self.data - def clear(self): + def clear(self, deactivate=None): self.data = {} self.exceptions_to_skip.clear() + self.breadcrumbs.clear() + + # If the caller did not specify if it wants to deactivate the + # context for the thread we only deactivate it if we're not the + # thread that created the context (main thread). + if deactivate is None: + client = self.client + if client is not None: + deactivate = get_thread_ident() != client.main_thread_id + + if deactivate: + self.deactivate() + + +import raven.breadcrumbs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/contrib/celery/__init__.py new/raven-5.19.0/raven/contrib/celery/__init__.py --- old/raven-5.12.0/raven/contrib/celery/__init__.py 2016-01-27 20:19:13.000000000 +0100 +++ new/raven-5.19.0/raven/contrib/celery/__init__.py 2016-05-27 18:24:21.000000000 +0200 @@ -9,6 +9,7 @@ import logging +from celery.exceptions import SoftTimeLimitExceeded from celery.signals import after_setup_logger, task_failure from raven.handlers.logging import SentryHandler @@ -24,15 +25,21 @@ def register_signal(client): - def process_failure_signal(sender, task_id, args, kwargs, **kw): + def process_failure_signal(sender, task_id, args, kwargs, einfo, **kw): # This signal is fired inside the stack so let raven do its magic + if isinstance(einfo.exception, SoftTimeLimitExceeded): + fingerprint = ['celery', 'SoftTimeLimitExceeded', sender] + else: + fingerprint = None client.captureException( extra={ 'task_id': task_id, 'task': sender, 'args': args, 'kwargs': kwargs, - }) + }, + fingerprint=fingerprint, + ) task_failure.connect(process_failure_signal, weak=False) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/contrib/django/client.py new/raven-5.19.0/raven/contrib/django/client.py --- old/raven-5.12.0/raven/contrib/django/client.py 2016-01-21 20:02:59.000000000 +0100 +++ new/raven-5.19.0/raven/contrib/django/client.py 2016-05-27 18:24:21.000000000 +0200 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ raven.contrib.django.client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -8,6 +9,7 @@ from __future__ import absolute_import +import time import logging from django.conf import settings @@ -26,13 +28,114 @@ from raven.contrib.django.utils import get_data_from_template, get_host from raven.contrib.django.middleware import SentryLogMiddleware from raven.utils.wsgi import get_headers, get_environ +from raven.utils import once +from raven import breadcrumbs +from raven._compat import string_types, binary_type __all__ = ('DjangoClient',) +class _FormatConverter(object): + + def __init__(self, param_mapping): + self.param_mapping = param_mapping + self.params = [] + + def __getitem__(self, val): + self.params.append(self.param_mapping.get(val)) + return '%s' + + +def format_sql(sql, params): + rv = [] + + if isinstance(params, dict): + conv = _FormatConverter(params) + sql = sql % conv + params = conv.params + + for param in params or (): + if param is None: + rv.append('NULL') + elif isinstance(param, string_types): + if isinstance(param, binary_type): + param = param.decode('utf-8', 'replace') + if len(param) > 256: + param = param[:256] + u'…' + rv.append("'%s'" % param.replace("'", "''")) + else: + rv.append(repr(param)) + + return sql, rv + + +@once +def install_sql_hook(): + """If installed this causes Django's queries to be captured.""" + try: + from django.db.backends.utils import CursorWrapper + except ImportError: + from django.db.backends.util import CursorWrapper + + try: + real_execute = CursorWrapper.execute + real_executemany = CursorWrapper.executemany + except AttributeError: + # XXX(mitsuhiko): On some very old django versions (<1.6) this + # trickery would have to look different but I can't be bothered. + return + + def record_sql(vendor, alias, start, duration, sql, params): + def processor(data): + real_sql, real_params = format_sql(sql, params) + if real_params: + real_sql = real_sql % tuple(real_params) + # maybe category to 'django.%s.%s' % (vendor, alias or + # 'default') ? + data.update({ + 'message': real_sql, + 'category': 'query', + }) + breadcrumbs.record(processor=processor) + + def record_many_sql(vendor, alias, start, sql, param_list): + duration = time.time() - start + for params in param_list: + record_sql(vendor, alias, start, duration, sql, params) + + def execute(self, sql, params=None): + start = time.time() + try: + return real_execute(self, sql, params) + finally: + record_sql(self.db.vendor, getattr(self.db, 'alias', None), + start, time.time() - start, sql, params) + + def executemany(self, sql, param_list): + start = time.time() + try: + return real_executemany(self, sql, param_list) + finally: + record_many_sql(self.db.vendor, getattr(self.db, 'alias', None), + start, sql, param_list) + + CursorWrapper.execute = execute + CursorWrapper.executemany = executemany + breadcrumbs.ignore_logger('django.db.backends') + + class DjangoClient(Client): logger = logging.getLogger('sentry.errors.client.django') + def __init__(self, *args, **kwargs): + install_sql_hook = kwargs.pop('install_sql_hook', True) + Client.__init__(self, *args, **kwargs) + if install_sql_hook: + self.install_sql_hook() + + def install_sql_hook(self): + install_sql_hook() + def get_user_info(self, user): if hasattr(user, 'is_authenticated') and \ not user.is_authenticated(): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/contrib/django/models.py new/raven-5.19.0/raven/contrib/django/models.py --- old/raven-5.12.0/raven/contrib/django/models.py 2016-01-13 17:52:15.000000000 +0100 +++ new/raven-5.19.0/raven/contrib/django/models.py 2016-05-03 00:45:35.000000000 +0200 @@ -181,7 +181,11 @@ def register_handlers(): - from django.core.signals import got_request_exception + from django.core.signals import got_request_exception, request_started + + def before_request(*args, **kwargs): + client.context.activate() + request_started.connect(before_request, weak=False) # HACK: support Sentry's internal communication if 'sentry' in settings.INSTALLED_APPS: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/contrib/tornado/__init__.py new/raven-5.19.0/raven/contrib/tornado/__init__.py --- old/raven-5.12.0/raven/contrib/tornado/__init__.py 2016-01-12 20:23:55.000000000 +0100 +++ new/raven-5.19.0/raven/contrib/tornado/__init__.py 2016-04-22 23:47:13.000000000 +0200 @@ -38,9 +38,9 @@ data = self.build_msg(*args, **kwargs) - self.send(callback=kwargs.get('callback', None), **data) + future = self.send(callback=kwargs.get('callback', None), **data) - return (data['event_id'],) + return (data['event_id'], future) def send(self, auth_header=None, callback=None, **data): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/transport/threaded.py new/raven-5.19.0/raven/transport/threaded.py --- old/raven-5.12.0/raven/transport/threaded.py 2015-12-10 20:09:34.000000000 +0100 +++ new/raven-5.19.0/raven/transport/threaded.py 2016-05-03 00:35:28.000000000 +0200 @@ -16,7 +16,7 @@ from raven.transport.base import AsyncTransport from raven.transport.http import HTTPTransport -from raven.utils.compat import Queue +from raven.utils.compat import Queue, check_threads DEFAULT_TIMEOUT = 10 @@ -27,6 +27,7 @@ _terminator = object() def __init__(self, shutdown_timeout=DEFAULT_TIMEOUT): + check_threads() self._queue = Queue(-1) self._lock = threading.Lock() self._thread = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/utils/__init__.py new/raven-5.19.0/raven/utils/__init__.py --- old/raven-5.12.0/raven/utils/__init__.py 2016-01-13 17:52:15.000000000 +0100 +++ new/raven-5.19.0/raven/utils/__init__.py 2016-05-03 00:45:35.000000000 +0200 @@ -9,6 +9,8 @@ from raven._compat import iteritems, string_types import logging +import threading +from functools import update_wrapper try: import pkg_resources except ImportError: @@ -65,7 +67,7 @@ # pull version from pkg_resources if distro exists try: return pkg_resources.get_distribution(module_name).version - except pkg_resources.DistributionNotFound: + except Exception: pass if hasattr(app, 'get_version'): @@ -167,3 +169,22 @@ if n not in d: d[n] = self.func(obj) return d[n] + + +def once(func): + """Runs a thing once and once only.""" + lock = threading.Lock() + + def new_func(*args, **kwargs): + if new_func.called: + return + with lock: + if new_func.called: + return + rv = func(*args, **kwargs) + new_func.called = True + return rv + + new_func = update_wrapper(new_func, func) + new_func.called = False + return new_func diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/utils/compat.py new/raven-5.19.0/raven/utils/compat.py --- old/raven-5.12.0/raven/utils/compat.py 2015-07-12 10:30:42.000000000 +0200 +++ new/raven-5.19.0/raven/utils/compat.py 2016-05-03 00:35:28.000000000 +0200 @@ -46,3 +46,17 @@ from urllib import parse as _urlparse # NOQA urlparse = _urlparse + + +def check_threads(): + try: + from uwsgi import opt + except ImportError: + return + + if str(opt.get('enable-threads', '0')).lower() in ('false', 'off', 'no', '0'): + from warnings import warn + warn(Warning('We detected the use of uwsgi with disabled threads. ' + 'This will cause issues with the transport you are ' + 'trying to use. Please enable threading for uwsgi. ' + '(Enable the "enable-threads" flag).')) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/utils/stacks.py new/raven-5.19.0/raven/utils/stacks.py --- old/raven-5.12.0/raven/utils/stacks.py 2016-03-25 22:20:21.000000000 +0100 +++ new/raven-5.19.0/raven/utils/stacks.py 2016-05-27 18:24:21.000000000 +0200 @@ -5,7 +5,7 @@ :copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -from __future__ import absolute_import +from __future__ import absolute_import, division import inspect import linecache @@ -198,18 +198,48 @@ Returns ``frames``. """ - frames_len = len(frames) + frames_len = 0 + app_frames = [] + system_frames = [] + for frame in frames: + frames_len += 1 + if frame.get('in_app'): + app_frames.append(frame) + else: + system_frames.append(frame) if frames_len <= frame_allowance: return frames - half_max = int(frame_allowance / 2) + remaining = frames_len - frame_allowance + app_count = len(app_frames) + system_allowance = max(frame_allowance - app_count, 0) + if system_allowance: + half_max = int(system_allowance / 2) + # prioritize trimming system frames + for frame in system_frames[half_max:-half_max]: + frame.pop('vars', None) + frame.pop('pre_context', None) + frame.pop('post_context', None) + remaining -= 1 + + else: + for frame in system_frames: + frame.pop('vars', None) + frame.pop('pre_context', None) + frame.pop('post_context', None) + remaining -= 1 + + if not remaining: + return frames + + app_allowance = app_count - remaining + half_max = int(app_allowance / 2) - for n in range(half_max, frames_len - half_max): - # remove heavy components - frames[n].pop('vars', None) - frames[n].pop('pre_context', None) - frames[n].pop('post_context', None) + for frame in app_frames[half_max:-half_max]: + frame.pop('vars', None) + frame.pop('pre_context', None) + frame.pop('post_context', None) return frames diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven/versioning.py new/raven-5.19.0/raven/versioning.py --- old/raven-5.12.0/raven/versioning.py 2016-01-13 17:52:15.000000000 +0100 +++ new/raven-5.19.0/raven/versioning.py 2016-05-03 00:34:02.000000000 +0200 @@ -28,8 +28,9 @@ head = text_type(fp.read()).strip() if head.startswith('ref: '): + head = head[5:] revision_file = os.path.join( - path, '.git', *head.rsplit(' ', 1)[-1].split('/') + path, '.git', *head.split('/') ) else: return head @@ -40,6 +41,25 @@ if not os.path.exists(os.path.join(path, '.git')): raise InvalidGitRepository( '%s does not seem to be the root of a git repository' % (path,)) + + # Check for our .git/packed-refs' file since a `git gc` may have run + # https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery + packed_file = os.path.join(path, '.git', 'packed-refs') + if os.path.exists(packed_file): + with open(packed_file, 'r') as fh: + for line in fh: + line = line.rstrip() + if not line: + continue + if line[:1] in ('#', '^'): + continue + try: + revision, ref = line.split(' ', 1) + except ValueError: + continue + if ref == head: + return text_type(revision) + raise InvalidGitRepository( 'Unable to find ref to head "%s" in repository' % (head,)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven.egg-info/PKG-INFO new/raven-5.19.0/raven.egg-info/PKG-INFO --- old/raven-5.12.0/raven.egg-info/PKG-INFO 2016-03-30 00:07:14.000000000 +0200 +++ new/raven-5.19.0/raven.egg-info/PKG-INFO 2016-05-27 18:55:29.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: raven -Version: 5.12.0 +Version: 5.19.0 Summary: Raven is a client for Sentry (https://getsentry.com) Home-page: https://github.com/getsentry/raven-python Author: Sentry diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/raven.egg-info/SOURCES.txt new/raven-5.19.0/raven.egg-info/SOURCES.txt --- old/raven-5.12.0/raven.egg-info/SOURCES.txt 2016-03-30 00:07:14.000000000 +0200 +++ new/raven-5.19.0/raven.egg-info/SOURCES.txt 2016-05-27 18:55:29.000000000 +0200 @@ -6,6 +6,7 @@ raven/__init__.py raven/_compat.py raven/base.py +raven/breadcrumbs.py raven/context.py raven/events.py raven/exceptions.py @@ -101,6 +102,8 @@ tests/__init__.py tests/base/__init__.py tests/base/tests.py +tests/breadcrumbs/__init__.py +tests/breadcrumbs/tests.py tests/conf/__init__.py tests/conf/tests.py tests/context/__init__.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/setup.py new/raven-5.19.0/setup.py --- old/raven-5.12.0/setup.py 2016-03-30 00:06:55.000000000 +0200 +++ new/raven-5.19.0/setup.py 2016-05-27 18:37:52.000000000 +0200 @@ -97,7 +97,7 @@ setup( name='raven', - version='5.12.0', + version='5.19.0', author='Sentry', author_email='[email protected]', url='https://github.com/getsentry/raven-python', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/tests/breadcrumbs/tests.py new/raven-5.19.0/tests/breadcrumbs/tests.py --- old/raven-5.12.0/tests/breadcrumbs/tests.py 1970-01-01 01:00:00.000000000 +0100 +++ new/raven-5.19.0/tests/breadcrumbs/tests.py 2016-05-27 18:37:12.000000000 +0200 @@ -0,0 +1,121 @@ +import sys +import logging + +from raven.utils.testutils import TestCase + +from raven.base import Client +from raven import breadcrumbs + +from io import StringIO + + +class BreadcrumbTestCase(TestCase): + + def test_crumb_buffer(self): + for enable in 1, 0: + client = Client('http://foo:[email protected]/0', + enable_breadcrumbs=enable) + with client.context: + breadcrumbs.record(type='foo', data={'bar': 'baz'}, + message='aha', category='huhu') + crumbs = client.context.breadcrumbs.get_buffer() + assert len(crumbs) == enable + + def test_log_crumb_reporting(self): + client = Client('http://foo:[email protected]/0') + with client.context: + log = logging.getLogger('whatever.foo') + log.info('This is a message with %s!', 'foo', blah='baz') + crumbs = client.context.breadcrumbs.get_buffer() + + assert len(crumbs) == 1 + assert crumbs[0]['type'] == 'default' + assert crumbs[0]['category'] == 'whatever.foo' + assert crumbs[0]['data'] == {'blah': 'baz'} + assert crumbs[0]['message'] == 'This is a message with foo!' + + def test_log_location(self): + out = StringIO() + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + handler = logging.StreamHandler(out) + handler.setFormatter(logging.Formatter( + u'%(name)s|%(filename)s|%(funcName)s|%(lineno)d|' + u'%(levelname)s|%(message)s')) + logger.addHandler(handler) + + client = Client('http://foo:[email protected]/0') + with client.context: + logger.info('Hello World!') + lineno = sys._getframe().f_lineno - 1 + + items = out.getvalue().strip().split('|') + assert items[0] == 'tests.breadcrumbs.tests' + assert items[1].rstrip('co') == 'tests.py' + assert items[2] == 'test_log_location' + assert int(items[3]) == lineno + assert items[4] == 'INFO' + assert items[5] == 'Hello World!' + + def test_broken_logging(self): + client = Client('http://foo:[email protected]/0') + with client.context: + log = logging.getLogger('whatever.foo') + log.info('This is a message with %s. %s!', 42) + crumbs = client.context.breadcrumbs.get_buffer() + + assert len(crumbs) == 1 + assert crumbs[0]['type'] == 'default' + assert crumbs[0]['category'] == 'whatever.foo' + assert crumbs[0]['message'] == 'This is a message with %s. %s!' + + def test_dedup_logging(self): + client = Client('http://foo:[email protected]/0') + with client.context: + log = logging.getLogger('whatever.foo') + log.info('This is a message with %s!', 42) + log.info('This is a message with %s!', 42) + log.info('This is a message with %s!', 42) + log.info('This is a message with %s!', 23) + log.info('This is a message with %s!', 23) + log.info('This is a message with %s!', 23) + log.info('This is a message with %s!', 42) + crumbs = client.context.breadcrumbs.get_buffer() + + assert len(crumbs) == 3 + assert crumbs[0]['type'] == 'default' + assert crumbs[0]['category'] == 'whatever.foo' + assert crumbs[0]['message'] == 'This is a message with 42!' + assert crumbs[1]['type'] == 'default' + assert crumbs[1]['category'] == 'whatever.foo' + assert crumbs[1]['message'] == 'This is a message with 23!' + assert crumbs[2]['type'] == 'default' + assert crumbs[2]['category'] == 'whatever.foo' + assert crumbs[2]['message'] == 'This is a message with 42!' + + def test_manual_record(self): + client = Client('http://foo:[email protected]/0') + with client.context: + def processor(data): + assert data['message'] == 'whatever' + assert data['level'] == 'warning' + assert data['category'] == 'category' + assert data['type'] == 'the_type' + assert data['data'] == {'foo': 'bar'} + data['data']['extra'] = 'something' + + breadcrumbs.record(message='whatever', + level='warning', + category='category', + data={'foo': 'bar'}, + type='the_type', + processor=processor) + + crumbs = client.context.breadcrumbs.get_buffer() + assert len(crumbs) == 1 + data = crumbs[0] + assert data['message'] == 'whatever' + assert data['level'] == 'warning' + assert data['category'] == 'category' + assert data['type'] == 'the_type' + assert data['data'] == {'foo': 'bar', 'extra': 'something'} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/raven-5.12.0/tests/context/tests.py new/raven-5.19.0/tests/context/tests.py --- old/raven-5.12.0/tests/context/tests.py 2015-07-12 10:30:42.000000000 +0200 +++ new/raven-5.19.0/tests/context/tests.py 2016-05-03 23:12:50.000000000 +0200 @@ -1,5 +1,7 @@ +import threading from raven.utils.testutils import TestCase +from raven.base import Client from raven.context import Context @@ -35,3 +37,36 @@ 'biz': 'baz', } } + + def test_thread_binding(self): + client = Client() + called = [] + + class TestContext(Context): + + def activate(self): + Context.activate(self) + called.append('activate') + + def deactivate(self): + called.append('deactivate') + Context.deactivate(self) + + # The main thread activates the context but clear does not + # deactivate. + context = TestContext(client) + context.clear() + assert called == ['activate'] + + # But another thread does. + del called[:] + + def test_thread(): + # This activate is unnecessary as the first activate happens + # automatically + context.activate() + context.clear() + t = threading.Thread(target=test_thread) + t.start() + t.join() + assert called == ['activate', 'activate', 'deactivate']
