1""" 2raven.utils.stacks 3~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 5:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details. 6:license: BSD, see LICENSE for more details. 7""" 8from __future__ import absolute_import, division 9 10import inspect 11import linecache 12import re 13import os 14import sys 15 16from raven.utils.serializer import transform 17from raven.utils.compat import iteritems 18 19 20_coding_re = re.compile(r'coding[:=]\s*([-\w.]+)') 21 22 23def get_lines_from_file(filename, lineno, context_lines, 24 loader=None, module_name=None): 25 """ 26 Returns context_lines before and after lineno from file. 27 Returns (pre_context_lineno, pre_context, context_line, post_context). 28 """ 29 source = None 30 if loader is not None and hasattr(loader, "get_source"): 31 try: 32 source = loader.get_source(module_name) 33 except (ImportError, IOError): 34 # Traceback (most recent call last): 35 # File "/Users/dcramer/Development/django-sentry/sentry/client/handlers.py", line 31, in emit 36 # get_client().create_from_record(record, request=request) 37 # File "/Users/dcramer/Development/django-sentry/sentry/client/base.py", line 325, in create_from_record 38 # data['__sentry__']['frames'] = varmap(shorten, get_stack_info(stack)) 39 # File "/Users/dcramer/Development/django-sentry/sentry/utils/stacks.py", line 112, in get_stack_info 40 # pre_context_lineno, pre_context, context_line, post_context = get_lines_from_file(filename, lineno, 7, loader, module_name) 41 # File "/Users/dcramer/Development/django-sentry/sentry/utils/stacks.py", line 24, in get_lines_from_file 42 # source = loader.get_source(module_name) 43 # File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/pkgutil.py", line 287, in get_source 44 # fullname = self._fix_name(fullname) 45 # File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/pkgutil.py", line 262, in _fix_name 46 # "module %s" % (self.fullname, fullname)) 47 # ImportError: Loader for module cProfile cannot handle module __main__ 48 source = None 49 if source is not None: 50 source = source.splitlines() 51 52 if source is None: 53 try: 54 source = linecache.getlines(filename) 55 except (OSError, IOError): 56 return None, None, None 57 58 if not source: 59 return None, None, None 60 61 lower_bound = max(0, lineno - context_lines) 62 upper_bound = min(lineno + 1 + context_lines, len(source)) 63 64 try: 65 pre_context = [ 66 line.strip('\r\n') 67 for line in source[lower_bound:lineno] 68 ] 69 context_line = source[lineno].strip('\r\n') 70 post_context = [ 71 line.strip('\r\n') 72 for line in source[(lineno + 1):upper_bound] 73 ] 74 except IndexError: 75 # the file may have changed since it was loaded into memory 76 return None, None, None 77 78 return ( 79 slim_string(pre_context), 80 slim_string(context_line), 81 slim_string(post_context) 82 ) 83 84 85def _getitem_from_frame(f_locals, key, default=None): 86 """ 87 f_locals is not guaranteed to have .get(), but it will always 88 support __getitem__. Even if it doesn't, we return ``default``. 89 """ 90 try: 91 return f_locals[key] 92 except Exception: 93 return default 94 95 96def to_dict(dictish): 97 """ 98 Given something that closely resembles a dictionary, we attempt 99 to coerce it into a propery dictionary. 100 """ 101 if hasattr(dictish, 'iterkeys'): 102 m = dictish.iterkeys 103 elif hasattr(dictish, 'keys'): 104 m = dictish.keys 105 else: 106 raise ValueError(dictish) 107 108 return dict((k, dictish[k]) for k in m()) 109 110 111def iter_traceback_frames(tb): 112 """ 113 Given a traceback object, it will iterate over all 114 frames that do not contain the ``__traceback_hide__`` 115 local variable. 116 """ 117 # Some versions of celery have hacked traceback objects that might 118 # miss tb_frame. 119 while tb and hasattr(tb, 'tb_frame'): 120 # support for __traceback_hide__ which is used by a few libraries 121 # to hide internal frames. 122 f_locals = getattr(tb.tb_frame, 'f_locals', {}) 123 if not _getitem_from_frame(f_locals, '__traceback_hide__'): 124 yield tb.tb_frame, getattr(tb, 'tb_lineno', None) 125 tb = tb.tb_next 126 127 128def iter_stack_frames(frames=None): 129 """ 130 Given an optional list of frames (defaults to current stack), 131 iterates over all frames that do not contain the ``__traceback_hide__`` 132 local variable. 133 """ 134 if not frames: 135 frames = inspect.stack()[1:] 136 137 for frame, lineno in ((f[0], f[2]) for f in reversed(frames)): 138 f_locals = getattr(frame, 'f_locals', {}) 139 if not _getitem_from_frame(f_locals, '__traceback_hide__'): 140 yield frame, lineno 141 142 143def get_frame_locals(frame, transformer=transform, max_var_size=4096): 144 f_locals = getattr(frame, 'f_locals', None) 145 if not f_locals: 146 return None 147 148 if not isinstance(f_locals, dict): 149 # XXX: Genshi (and maybe others) have broken implementations of 150 # f_locals that are not actually dictionaries 151 try: 152 f_locals = to_dict(f_locals) 153 except Exception: 154 return None 155 156 f_vars = {} 157 f_size = 0 158 for k, v in iteritems(f_locals): 159 v = transformer(v) 160 v_size = len(repr(v)) 161 if v_size + f_size < max_var_size: 162 f_vars[k] = v 163 f_size += v_size 164 return f_vars 165 166 167def slim_frame_data(frames, frame_allowance=25): 168 """ 169 Removes various excess metadata from middle frames which go beyond 170 ``frame_allowance``. 171 172 Returns ``frames``. 173 """ 174 frames_len = 0 175 app_frames = [] 176 system_frames = [] 177 for frame in frames: 178 frames_len += 1 179 if frame.get('in_app'): 180 app_frames.append(frame) 181 else: 182 system_frames.append(frame) 183 184 if frames_len <= frame_allowance: 185 return frames 186 187 remaining = frames_len - frame_allowance 188 app_count = len(app_frames) 189 system_allowance = max(frame_allowance - app_count, 0) 190 if system_allowance: 191 half_max = int(system_allowance / 2) 192 # prioritize trimming system frames 193 for frame in system_frames[half_max:-half_max]: 194 frame.pop('vars', None) 195 frame.pop('pre_context', None) 196 frame.pop('post_context', None) 197 remaining -= 1 198 199 else: 200 for frame in system_frames: 201 frame.pop('vars', None) 202 frame.pop('pre_context', None) 203 frame.pop('post_context', None) 204 remaining -= 1 205 206 if remaining: 207 app_allowance = app_count - remaining 208 half_max = int(app_allowance / 2) 209 210 for frame in app_frames[half_max:-half_max]: 211 frame.pop('vars', None) 212 frame.pop('pre_context', None) 213 frame.pop('post_context', None) 214 215 return frames 216 217 218def slim_string(value, length=512): 219 if not value: 220 return value 221 if len(value) > length: 222 return value[:length - 3] + '...' 223 return value[:length] 224 225 226def get_stack_info(frames, transformer=transform, capture_locals=True, 227 frame_allowance=25): 228 """ 229 Given a list of frames, returns a list of stack information 230 dictionary objects that are JSON-ready. 231 232 We have to be careful here as certain implementations of the 233 _Frame class do not contain the necessary data to lookup all 234 of the information we want. 235 """ 236 __traceback_hide__ = True # NOQA 237 238 result = [] 239 for frame_info in frames: 240 # Old, terrible API 241 if isinstance(frame_info, (list, tuple)): 242 frame, lineno = frame_info 243 244 else: 245 frame = frame_info 246 lineno = frame_info.f_lineno 247 248 # Support hidden frames 249 f_locals = getattr(frame, 'f_locals', {}) 250 if _getitem_from_frame(f_locals, '__traceback_hide__'): 251 continue 252 253 f_globals = getattr(frame, 'f_globals', {}) 254 255 f_code = getattr(frame, 'f_code', None) 256 if f_code: 257 abs_path = frame.f_code.co_filename 258 function = frame.f_code.co_name 259 else: 260 abs_path = None 261 function = None 262 263 loader = _getitem_from_frame(f_globals, '__loader__') 264 module_name = _getitem_from_frame(f_globals, '__name__') 265 266 if lineno: 267 lineno -= 1 268 269 if lineno is not None and abs_path: 270 pre_context, context_line, post_context = \ 271 get_lines_from_file(abs_path, lineno, 5, loader, module_name) 272 else: 273 pre_context, context_line, post_context = None, None, None 274 275 # Try to pull a relative file path 276 # This changes /foo/site-packages/baz/bar.py into baz/bar.py 277 try: 278 base_filename = sys.modules[module_name.split('.', 1)[0]].__file__ 279 filename = abs_path.split( 280 base_filename.rsplit(os.sep, 2)[0], 1)[-1].lstrip(os.sep) 281 except Exception: 282 filename = abs_path 283 284 if not filename: 285 filename = abs_path 286 287 frame_result = { 288 'abs_path': abs_path, 289 'filename': filename, 290 'module': module_name or None, 291 'function': function or '<unknown>', 292 'lineno': lineno + 1, 293 } 294 if capture_locals: 295 f_vars = get_frame_locals(frame, transformer=transformer) 296 if f_vars: 297 frame_result['vars'] = f_vars 298 299 if context_line is not None: 300 frame_result.update({ 301 'pre_context': pre_context, 302 'context_line': context_line, 303 'post_context': post_context, 304 }) 305 result.append(frame_result) 306 307 stackinfo = { 308 'frames': slim_frame_data(result, frame_allowance=frame_allowance), 309 } 310 311 return stackinfo 312