""" raven.base ~~~~~~~~~~ :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 import zlib import logging import os import sys import time import uuid import warnings from datetime import datetime from pprint import pformat from types import FunctionType if sys.version_info >= (3, 2): import contextlib else: import contextlib2 as contextlib 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 from raven.utils.encoding import to_unicode from raven.utils.serializer import transform from raven.utils.stacks import get_stack_info, iter_stack_frames, get_culprit from raven.transport.registry import TransportRegistry, default_transports # enforce imports to avoid obscure stacktraces with MemoryError import raven.events # NOQA __all__ = ('Client',) __excepthook__ = None PLATFORM_NAME = 'python' # singleton for the client Raven = None class ModuleProxyCache(dict): def __missing__(self, key): module, class_name = key.rsplit('.', 1) handler = getattr(__import__( module, {}, {}, [class_name]), class_name) self[key] = handler return handler class ClientState(object): ONLINE = 1 ERROR = 0 def __init__(self): self.status = self.ONLINE self.last_check = None self.retry_number = 0 self.retry_after = 0 def should_try(self): if self.status == self.ONLINE: return True interval = self.retry_after or min(self.retry_number, 6) ** 2 if time.time() - self.last_check > interval: return True return False def set_fail(self, retry_after=0): self.status = self.ERROR self.retry_number += 1 self.last_check = time.time() self.retry_after = retry_after def set_success(self): self.status = self.ONLINE self.last_check = None self.retry_number = 0 self.retry_after = 0 def did_fail(self): return self.status == self.ERROR class Client(object): """ The base Raven client. Will read default configuration from the environment variable ``SENTRY_DSN`` if available. >>> from raven import Client >>> # Read configuration from ``os.environ['SENTRY_DSN']`` >>> client = Client() >>> # Specify a DSN explicitly >>> client = Client(dsn='https://public_key:secret_key@sentry.local/project_id') >>> # Record an exception >>> try: >>> 1/0 >>> except ZeroDivisionError: >>> ident = client.get_ident(client.captureException()) >>> print "Exception caught; reference is %s" % ident """ 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, **options): global Raven o = options self.configure_logging() 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) self.include_paths = set(o.get('include_paths') or []) self.exclude_paths = set(o.get('exclude_paths') or []) self.name = text_type(o.get('name') or o.get('machine') or defaults.NAME) self.auto_log_stacks = bool( o.get('auto_log_stacks') or defaults.AUTO_LOG_STACKS) self.capture_locals = bool( o.get('capture_locals', defaults.CAPTURE_LOCALS)) self.string_max_length = int( o.get('string_max_length') or defaults.MAX_LENGTH_STRING) self.list_max_length = int( o.get('list_max_length') or defaults.MAX_LENGTH_LIST) self.site = o.get('site') self.include_versions = o.get('include_versions', True) self.processors = o.get('processors') if self.processors is None: self.processors = defaults.PROCESSORS context = o.get('context') if context is None: context = {'sys.argv': sys.argv[:]} self.extra = context self.tags = o.get('tags') or {} self.environment = o.get('environment') or None self.release = o.get('release') or os.environ.get('HEROKU_SLUG_COMMIT') self.module_cache = ModuleProxyCache() if not self.is_enabled(): self.logger.info( 'Raven is not configured (logging is disabled). Please see the' ' documentation for more information.') if Raven is None: Raven = self self._context = Context() if install_sys_hook: self.install_sys_hook() def set_dsn(self, dsn=None, transport=None): if dsn is None and os.environ.get('SENTRY_DSN'): msg = "Configuring Raven from environment variable 'SENTRY_DSN'" self.logger.debug(msg) dsn = os.environ['SENTRY_DSN'] if dsn not in self._transport_cache: if dsn is None: 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)) def install_sys_hook(self): global __excepthook__ if __excepthook__ is None: __excepthook__ = sys.excepthook def handle_exception(*exc_info): self.captureException(exc_info=exc_info) __excepthook__(*exc_info) sys.excepthook = handle_exception @classmethod def register_scheme(cls, scheme, transport_class): cls._registry.register_scheme(scheme, transport_class) def configure_logging(self): for name in ('raven', 'sentry'): logger = logging.getLogger(name) if logger.handlers: continue logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.INFO) def get_processors(self): for processor in self.processors: yield self.module_cache[processor](self) def get_module_versions(self): if not self.include_versions: return {} version_info = sys.version_info modules = get_versions(self.include_paths) modules['python'] = '{0}.{1}.{2}'.format( version_info[0], version_info[1], version_info[2], ) return modules def get_ident(self, result): """ Returns a searchable string representing a message. >>> result = client.capture(**kwargs) >>> ident = client.get_ident(result) """ warnings.warn('Client.get_ident is deprecated. The event ID is now ' 'returned as the result of capture.', DeprecationWarning) return result def get_handler(self, name): return self.module_cache[name](self) def get_public_dsn(self, scheme=None): """ Returns a public DSN which is consumable by raven-js >>> # Return scheme-less DSN >>> print client.get_public_dsn() >>> # Specify a scheme to use (http or https) >>> print client.get_public_dsn('https') """ if not self.is_enabled(): return url = self.remote.get_public_dsn() if not scheme: return url return '%s:%s' % (scheme, url) def _get_exception_key(self, exc_info): return ( exc_info[0], id(exc_info[1]), id(exc_info[2].tb_frame.f_code), id(exc_info[2]), exc_info[2].tb_lasti, ) def skip_error_for_logging(self, exc_info): key = self._get_exception_key(exc_info) return key in self.context.exceptions_to_skip def record_exception_seen(self, exc_info): key = self._get_exception_key(exc_info) self.context.exceptions_to_skip.add(key) def build_msg(self, event_type, data=None, date=None, time_spent=None, extra=None, stack=None, public_key=None, tags=None, fingerprint=None, **kwargs): """ Captures, processes and serializes an event into a dict object The result of ``build_msg`` should be a standardized dict, with all default values available. """ # create ID client-side so that it can be passed to application event_id = uuid.uuid4().hex data = merge_dicts(self.context.data, data) data.setdefault('tags', {}) data.setdefault('extra', {}) if '.' not in event_type: # Assume it's a builtin event_type = 'raven.events.%s' % event_type handler = self.get_handler(event_type) result = handler.capture(**kwargs) # data (explicit) culprit takes over auto event detection culprit = result.pop('culprit', None) if data.get('culprit'): culprit = data['culprit'] for k, v in iteritems(result): if k not in data: data[k] = v # auto_log_stacks only applies to events that are not exceptions # due to confusion about which stack is which and the automatic # application of stacktrace to exception objects by Sentry if stack is None and 'exception' not in data: stack = self.auto_log_stacks if stack and 'stacktrace' not in data: if stack is True: frames = iter_stack_frames() else: frames = stack stack_info = get_stack_info( frames, transformer=self.transform, capture_locals=self.capture_locals, ) data.update({ 'stacktrace': stack_info, }) if self.include_paths: for frame in self._iter_frames(data): if frame.get('in_app') is not None: continue path = frame.get('module') if not path: continue if path.startswith('raven.'): frame['in_app'] = False else: frame['in_app'] = ( any(path.startswith(x) for x in self.include_paths) and not any(path.startswith(x) for x in self.exclude_paths) ) if not culprit: if 'stacktrace' in data: culprit = get_culprit(data['stacktrace']['frames']) elif 'exception' in data: stacktrace = data['exception']['values'][0].get('stacktrace') if stacktrace: culprit = get_culprit(stacktrace['frames']) if not data.get('level'): data['level'] = kwargs.get('level') or logging.ERROR if not data.get('server_name'): data['server_name'] = self.name if not data.get('modules'): data['modules'] = self.get_module_versions() if self.release is not None: data['release'] = self.release if self.environment is not None: data['environment'] = self.environment data['tags'] = merge_dicts(self.tags, data['tags'], tags) data['extra'] = merge_dicts(self.extra, data['extra'], extra) # Legacy support for site attribute site = data.pop('site', None) or self.site if site: data['tags'].setdefault('site', site) if culprit: data['culprit'] = culprit if fingerprint: data['fingerprint'] = fingerprint # Run the data through processors for processor in self.get_processors(): data.update(processor.process(data)) if 'message' not in data: data['message'] = kwargs.get('message', handler.to_string(data)) # tags should only be key=>u'value' for key, value in iteritems(data['tags']): data['tags'][key] = to_unicode(value) # extra data can be any arbitrary value for k, v in iteritems(data['extra']): data['extra'][k] = self.transform(v) # It's important date is added **after** we serialize data.setdefault('project', self.remote.project) data.setdefault('timestamp', date or datetime.utcnow()) data.setdefault('time_spent', time_spent) data.setdefault('event_id', event_id) data.setdefault('platform', PLATFORM_NAME) return data def transform(self, data): return transform( data, list_max_length=self.list_max_length, string_max_length=self.string_max_length) @property def context(self): """ Updates this clients thread-local context for future events. >>> def view_handler(view_func, *args, **kwargs): >>> client.context.merge(tags={'key': 'value'}) >>> try: >>> return view_func(*args, **kwargs) >>> finally: >>> client.context.clear() """ return self._context def user_context(self, data): """ Update the user context for future events. >>> client.user_context({'email': 'foo@example.com'}) """ return self.context.merge({ 'user': data, }) def http_context(self, data, **kwargs): """ Update the http context for future events. >>> client.http_context({'url': 'http://example.com'}) """ return self.context.merge({ 'request': data, }) def extra_context(self, data, **kwargs): """ Update the extra context for future events. >>> client.extra_context({'foo': 'bar'}) """ return self.context.merge({ 'extra': data, }) def tags_context(self, data, **kwargs): """ Update the tags context for future events. >>> client.tags_context({'version': '1.0'}) """ return self.context.merge({ 'tags': data, }) def capture(self, event_type, data=None, date=None, time_spent=None, extra=None, stack=None, tags=None, **kwargs): """ Captures and processes an event and pipes it off to SentryClient.send. To use structured data (interfaces) with capture: >>> capture('raven.events.Message', message='foo', data={ >>> 'request': { >>> 'url': '...', >>> 'data': {}, >>> 'query_string': '...', >>> 'method': 'POST', >>> }, >>> 'logger': 'logger.name', >>> }, extra={ >>> 'key': 'value', >>> }) The finalized ``data`` structure contains the following (some optional) builtin values: >>> { >>> # the culprit and version information >>> 'culprit': 'full.module.name', # or /arbitrary/path >>> >>> # all detectable installed modules >>> 'modules': { >>> 'full.module.name': 'version string', >>> }, >>> >>> # arbitrary data provided by user >>> 'extra': { >>> 'key': 'value', >>> } >>> } :param event_type: the module path to the Event class. Builtins can use shorthand class notation and exclude the full module path. :param data: the data base, useful for specifying structured data interfaces. Any key which contains a '.' will be assumed to be a data interface. :param date: the datetime of this event :param time_spent: a integer value representing the duration of the event (in milliseconds) :param extra: a dictionary of additional standard metadata :param stack: a stacktrace for the event :param tags: dict of extra tags :return: a tuple with a 32-length string identifying this event """ if not self.is_enabled(): return exc_info = kwargs.get('exc_info') if exc_info is not None: if self.skip_error_for_logging(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'] def is_enabled(self): """ Return a boolean describing whether the client should attempt to send events. """ return self.remote.is_active() def _iter_frames(self, data): if 'stacktrace' in data: for frame in data['stacktrace']['frames']: yield frame if 'exception' in data: for frame in data['exception']['values'][0]['stacktrace']['frames']: yield frame def _successful_send(self): self.state.set_success() def _failed_send(self, exc, url, data): retry_after = 0 if isinstance(exc, APIError): if isinstance(exc, RateLimited): retry_after = exc.retry_after self.error_logger.error( 'Sentry responded with an API error: %s(%s)', type(exc).__name__, exc.message) else: self.error_logger.error( 'Sentry responded with an error: %s (url: %s)\n%s', exc, url, pformat(data), exc_info=True ) self._log_failed_submission(data) self.state.set_fail(retry_after=retry_after) def _log_failed_submission(self, data): """ Log a reasonable representation of an event that should have been sent to Sentry """ message = data.pop('message', '') output = [message] if 'exception' in data and 'stacktrace' in data['exception']['values'][0]: # try to reconstruct a reasonable version of the exception for frame in data['exception']['values'][0]['stacktrace']['frames']: output.append(' File "%(fn)s", line %(lineno)s, in %(func)s' % { 'fn': frame['filename'], 'lineno': frame['lineno'], 'func': frame['function'], }) self.uncaught_logger.error(output) 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() 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) def 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`` for each server specified in the servers configuration. """ 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', } self.send_remote( url=self.remote.store_endpoint, data=message, headers=headers, **kwargs ) def get_content_encoding(self): return 'deflate' def encode(self, data): """ Serializes ``data`` into a raw string. """ return zlib.compress(json.dumps(data).encode('utf8')) def decode(self, data): """ Unserializes a string, ``data``. """ return json.loads(zlib.decompress(data).decode('utf8')) def captureMessage(self, message, **kwargs): """ Creates an event from ``message``. >>> client.captureMessage('My event just happened!') """ return self.capture('raven.events.Message', message=message, **kwargs) def captureException(self, exc_info=None, **kwargs): """ Creates an event from an exception. >>> try: >>> exc_info = sys.exc_info() >>> client.captureException(exc_info) >>> finally: >>> del exc_info If exc_info is not provided, or is set to True, then this method will perform the ``exc_info = sys.exc_info()`` and the requisite clean-up for you. ``kwargs`` are passed through to ``.capture``. """ if exc_info is None: exc_info = sys.exc_info() return self.capture( 'raven.events.Exception', exc_info=exc_info, **kwargs) def capture_exceptions(self, function_or_exceptions=None, **kwargs): """ Wrap a function or code block in try/except and automatically call ``.captureException`` if it raises an exception, then the exception is reraised. By default, it will capture ``Exception`` >>> @client.capture_exceptions >>> def foo(): >>> raise Exception() >>> with client.capture_exceptions(): >>> raise Exception() You can also specify exceptions to be caught specifically >>> @client.capture_exceptions((IOError, LookupError)) >>> def bar(): >>> ... >>> with client.capture_exceptions((IOError, LookupError)): >>> ... ``kwargs`` are passed through to ``.captureException``. """ function = None exceptions = (Exception,) if isinstance(function_or_exceptions, FunctionType): function = function_or_exceptions elif function_or_exceptions is not None: exceptions = function_or_exceptions # In python3.2 contextmanager acts both as contextmanager and decorator @contextlib.contextmanager def make_decorator(exceptions): try: yield except exceptions: self.captureException(**kwargs) raise decorator = make_decorator(exceptions) if function: return decorator(function) return decorator def captureQuery(self, query, params=(), engine=None, **kwargs): """ Creates an event for a SQL query. >>> client.captureQuery('SELECT * FROM foo') """ return self.capture( 'raven.events.Query', query=query, params=params, engine=engine, **kwargs) def captureExceptions(self, **kwargs): warnings.warn( 'captureExceptions is deprecated, used context() instead.', DeprecationWarning) return self.context(**kwargs) class DummyClient(Client): "Sends messages into an empty void" def send(self, **kwargs): return None