# -*- coding: utf-8 -*- # Copyright (c) 2015, Maxwell Morais and contributors # For license information, please see license.txt from __future__ import unicode_literals import frappe from frappe.utils import cstr, encode import os import sys import inspect import traceback import linecache import pydoc import cgitb import types import datetime import json import six def make_error_snapshot(exception): if frappe.conf.disable_error_snapshot: return logger = frappe.logger(__name__, with_more_info=False) try: error_id = '{timestamp:s}-{ip:s}-{hash:s}'.format( timestamp=cstr(datetime.datetime.now()), ip=frappe.local.request_ip or '127.0.0.1', hash=frappe.generate_hash(length=3) ) snapshot_folder = get_error_snapshot_path() frappe.create_folder(snapshot_folder) snapshot_file_path = os.path.join(snapshot_folder, "{0}.json".format(error_id)) snapshot = get_snapshot(exception) with open(encode(snapshot_file_path), 'wb') as error_file: error_file.write(encode(frappe.as_json(snapshot))) logger.error('New Exception collected with id: {}'.format(error_id)) except Exception as e: logger.error('Could not take error snapshot: {0}'.format(e), exc_info=True) def get_snapshot(exception, context=10): """ Return a dict describing a given traceback (based on cgitb.text) """ etype, evalue, etb = sys.exc_info() if isinstance(etype, six.class_types): etype = etype.__name__ # creates a snapshot dict with some basic information s = { 'pyver': 'Python {version:s}: {executable:s} (prefix: {prefix:s})'.format( version = sys.version.split()[0], executable = sys.executable, prefix = sys.prefix ), 'timestamp': cstr(datetime.datetime.now()), 'traceback': traceback.format_exc(), 'frames': [], 'etype': cstr(etype), 'evalue': cstr(repr(evalue)), 'exception': {}, 'locals': {} } # start to process frames records = inspect.getinnerframes(etb, 5) for frame, file, lnum, func, lines, index in records: file = file and os.path.abspath(file) or '?' args, varargs, varkw, locals = inspect.getargvalues(frame) call = '' if func != '?': call = inspect.formatargvalues(args, varargs, varkw, locals, formatvalue=lambda value: '={}'.format(pydoc.text.repr(value))) # basic frame information f = {'file': file, 'func': func, 'call': call, 'lines': {}, 'lnum': lnum} def reader(lnum=[lnum]): try: return linecache.getline(file, lnum[0]) finally: lnum[0] += 1 vars = cgitb.scanvars(reader, frame, locals) # if it is a view, replace with generated code # if file.endswith('html'): # lmin = lnum > context and (lnum - context) or 0 # lmax = lnum + context # lines = code.split("\n")[lmin:lmax] # index = min(context, lnum) - 1 if index is not None: i = lnum - index for line in lines: f['lines'][i] = line.rstrip() i += 1 # dump local variable (referenced in current line only) f['dump'] = {} for name, where, value in vars: if name in f['dump']: continue if value is not cgitb.__UNDEF__: if where == 'global': name = 'global {name:s}'.format(name=name) elif where != 'local': name = where + ' ' + name.split('.')[-1] f['dump'][name] = pydoc.text.repr(value) else: f['dump'][name] = 'undefined' s['frames'].append(f) # add exception type, value and attributes if isinstance(evalue, BaseException): for name in dir(evalue): # prevent py26 DeprecationWarning if (name != 'messages' or sys.version_info < (2.6)) and not name.startswith('__'): value = pydoc.text.repr(getattr(evalue, name)) # render multilingual string properly if type(value)==str and value.startswith(b"u'"): value = eval(value) s['exception'][name] = encode(value) # add all local values (of last frame) to the snapshot for name, value in locals.items(): if type(value)==str and value.startswith(b"u'"): value = eval(value) s['locals'][name] = pydoc.text.repr(value) return s def collect_error_snapshots(): """Scheduled task to collect error snapshots from files and push into Error Snapshot table""" if frappe.conf.disable_error_snapshot: return try: path = get_error_snapshot_path() if not os.path.exists(path): return for fname in os.listdir(path): fullpath = os.path.join(path, fname) try: with open(fullpath, 'r') as filedata: data = json.load(filedata) except ValueError: # empty file os.remove(fullpath) continue for field in ['locals', 'exception', 'frames']: data[field] = frappe.as_json(data[field]) doc = frappe.new_doc('Error Snapshot') doc.update(data) doc.save() frappe.db.commit() os.remove(fullpath) clear_old_snapshots() except Exception as e: make_error_snapshot(e) # prevent creation of unlimited error snapshots raise def clear_old_snapshots(): """Clear snapshots that are older than a month""" frappe.db.sql("""delete from `tabError Snapshot` where creation < date_sub(now(), interval 1 month)""") path = get_error_snapshot_path() today = datetime.datetime.now() for file in os.listdir(path): p = os.path.join(path, file) ctime = datetime.datetime.fromtimestamp(os.path.getctime(p)) if (today - ctime).days > 31: os.remove(os.path.join(path, p)) def get_error_snapshot_path(): return frappe.get_site_path('error-snapshots')