Last active
July 30, 2016 19:53
-
-
Save stroykova/267f0e049ee8d35ddeef5768002579ca to your computer and use it in GitHub Desktop.
Raven like stack trace formatting
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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