Skip to content

Instantly share code, notes, and snippets.

@stroykova
Last active July 30, 2016 19:53
Show Gist options
  • Save stroykova/267f0e049ee8d35ddeef5768002579ca to your computer and use it in GitHub Desktop.
Save stroykova/267f0e049ee8d35ddeef5768002579ca to your computer and use it in GitHub Desktop.
Raven like stack trace formatting
"""
https://github.com/getsentry/raven-python/blob/master/raven/base.py
"""
from __future__ import absolute_import
import linecache
import logging
import sys
from contextlib import closing
logger = logging.getLogger('sentry.errors.serializer')
class Serializer(object):
logger = logger
def __init__(self, manager):
self.manager = manager
self.context = set()
self.serializers = []
for serializer in manager.serializers:
self.serializers.append(serializer(self))
def close(self):
del self.serializers
del self.context
def transform(self, value, **kwargs):
"""
Primary function which handles recursively transforming
values via their serializers
"""
if value is None:
return None
objid = id(value)
if objid in self.context:
return '<...>'
self.context.add(objid)
try:
for serializer in self.serializers:
if serializer.can(value):
try:
return serializer.serialize(value, **kwargs)
except Exception as e:
logger.exception(e)
return str(type(value))
# if all else fails, lets use the repr of the object
try:
return repr(value)
except Exception as e:
logger.exception(e)
# It's common case that a model's __unicode__ definition
# may try to query the database which if it was not
# cleaned up correctly, would hit a transaction aborted
# exception
return str(type(value))
finally:
self.context.remove(objid)
class SerializationManager(object):
logger = logger
def __init__(self):
self.__registry = []
self.__serializers = {}
@property
def serializers(self):
# XXX: Would serializers ever need state that we shouldn't cache them?
for serializer in self.__registry:
yield serializer
def register(self, serializer):
if serializer not in self.__registry:
self.__registry.append(serializer)
return serializer
manager = SerializationManager()
def transform(value, manager=manager, **kwargs):
with closing(Serializer(manager)) as serializer:
return serializer.transform(value, **kwargs)
def slim_frame_data(frames, frame_allowance=25):
"""
Removes various excess metadata from middle frames which go beyond
``frame_allowance``.
Returns ``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
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 frame in app_frames[half_max:-half_max]:
frame.pop('vars', None)
frame.pop('pre_context', None)
frame.pop('post_context', None)
return frames
def get_frame_locals(frame, transformer=transform, max_var_size=4096):
f_locals = getattr(frame, 'f_locals', None)
if not f_locals:
return None
if not isinstance(f_locals, dict):
# XXX: Genshi (and maybe others) have broken implementations of
# f_locals that are not actually dictionaries
try:
f_locals = dict(f_locals)
except Exception:
return None
f_vars = {}
f_size = 0
for k, v in f_locals.iteritems():
v = transformer(v)
v_size = len(repr(v))
if v_size + f_size < 4096:
f_vars[k] = v
f_size += v_size
return f_vars
def slim_string(value, length=512):
if not value:
return value
if len(value) > length:
return value[:length - 3] + '...'
return value[:length]
def get_lines_from_file(filename, lineno, context_lines,
loader=None, module_name=None):
"""
Returns context_lines before and after lineno from file.
Returns (pre_context_lineno, pre_context, context_line, post_context).
"""
source = None
if loader is not None and hasattr(loader, "get_source"):
try:
source = loader.get_source(module_name)
except ImportError:
source = None
if source is not None:
source = source.splitlines()
if source is None:
try:
source = linecache.getlines(filename)
except (OSError, IOError):
return None, None, None
if not source:
return None, None, None
lower_bound = max(0, lineno - context_lines)
upper_bound = min(lineno + 1 + context_lines, len(source))
try:
pre_context = [
line.strip('\r\n')
for line in source[lower_bound:lineno]
]
context_line = source[lineno].strip('\r\n')
post_context = [
line.strip('\r\n')
for line in source[(lineno + 1):upper_bound]
]
except IndexError:
# the file may have changed since it was loaded into memory
return None, None, None
return (
slim_string(pre_context),
slim_string(context_line),
slim_string(post_context)
)
def get_stack_info(frames, transformer=transform, capture_locals=True,
frame_allowance=25):
"""
Given a list of frames, returns a list of stack information
dictionary objects that are JSON-ready.
We have to be careful here as certain implementations of the
_Frame class do not contain the necessary data to lookup all
of the information we want.
"""
__traceback_hide__ = True # NOQA
result = []
for frame_info in frames:
# Old, terrible API
if isinstance(frame_info, (list, tuple)):
frame, lineno = frame_info
else:
frame = frame_info
lineno = frame_info.f_lineno
# Support hidden frames
f_locals = getattr(frame, 'f_locals', {})
if _getitem_from_frame(f_locals, '__traceback_hide__'):
continue
f_globals = getattr(frame, 'f_globals', {})
f_code = getattr(frame, 'f_code', None)
if f_code:
abs_path = frame.f_code.co_filename
function = frame.f_code.co_name
else:
abs_path = None
function = None
loader = _getitem_from_frame(f_globals, '__loader__')
module_name = _getitem_from_frame(f_globals, '__name__')
if lineno:
lineno -= 1
if lineno is not None and abs_path:
pre_context, context_line, post_context = \
get_lines_from_file(abs_path, lineno, 5, loader, module_name)
else:
pre_context, context_line, post_context = None, None, None
# Try to pull a relative file path
# This changes /foo/site-packages/baz/bar.py into baz/bar.py
try:
base_filename = sys.modules[module_name.split('.', 1)[0]].__file__
filename = abs_path.split(
base_filename.rsplit('/', 2)[0], 1)[-1].lstrip("/")
except:
filename = abs_path
if not filename:
filename = abs_path
frame_result = {
'abs_path': abs_path,
'filename': filename,
'module': module_name or None,
'function': function or '<unknown>',
'lineno': lineno + 1,
}
if capture_locals:
f_vars = get_frame_locals(frame, transformer=transformer)
if f_vars:
frame_result['vars'] = f_vars
if context_line is not None:
frame_result.update({
'pre_context': pre_context,
'context_line': context_line,
'post_context': post_context,
})
result.append(frame_result)
stackinfo = {
'frames': slim_frame_data(result, frame_allowance=frame_allowance),
}
return stackinfo
def _getitem_from_frame(f_locals, key, default=None):
"""
f_locals is not guaranteed to have .get(), but it will always
support __getitem__. Even if it doesn't, we return ``default``.
"""
try:
return f_locals[key]
except Exception:
return default
def iter_traceback_frames(tb):
"""
Given a traceback object, it will iterate over all
frames that do not contain the ``__traceback_hide__``
local variable.
"""
# Some versions of celery have hacked traceback objects that might
# miss tb_frame.
while tb and hasattr(tb, 'tb_frame'):
# support for __traceback_hide__ which is used by a few libraries
# to hide internal frames.
f_locals = getattr(tb.tb_frame, 'f_locals', {})
if not _getitem_from_frame(f_locals, '__traceback_hide__'):
yield tb.tb_frame, getattr(tb, 'tb_lineno', None)
tb = tb.tb_next
class BaseEvent(object):
def __init__(self):
self.logger = logging.getLogger(__name__)
def to_string(self, data):
raise NotImplementedError
def capture(self, **kwargs):
return {
}
def transform(self, value):
return transform(
value, list_max_length=50,
string_max_length=400)
class RavenException(BaseEvent):
"""
Exceptions store the following metadata:
- value: 'My exception value'
- type: 'ClassName'
- module '__builtin__' (i.e. __builtin__.TypeError)
- frames: a list of serialized frames (see _get_traceback_frames)
"""
@staticmethod
def to_string(data):
exc = data['exception']['values'][0]
if exc['value']:
return '%s: %s' % (exc['type'], exc['value'])
return exc['type']
def capture(self, exc_info=None, **kwargs):
if not exc_info or exc_info is True:
exc_info = sys.exc_info()
if not exc_info:
raise ValueError('No exception found')
exc_type, exc_value, exc_traceback = exc_info
try:
stack_info = get_stack_info(
iter_traceback_frames(exc_traceback),
transformer=self.transform,
capture_locals=True,
)
exc_module = getattr(exc_type, '__module__', None)
if exc_module:
exc_module = str(exc_module)
exc_type = getattr(exc_type, '__name__', '<unknown>')
return {
'level': kwargs.get('level', logging.ERROR),
'exception': {
'values': [{
'value': str(exc_value),
'type': str(exc_type),
'module': str(exc_module),
'stacktrace': stack_info,
}],
},
}
finally:
try:
del exc_type, exc_value, exc_traceback
except Exception as e:
self.logger.exception(e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment