debian-python-raven/raven/base.py

801 lines
25 KiB
Python

"""
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', '<no message value>')
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