seitime-frappe/frappe/utils/error.py
2019-03-07 14:06:22 +05:30

213 lines
5.6 KiB
Python

# -*- 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 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 < (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')
def get_frame_locals():
traceback = sys.exc_info()[2]
frames = []
if traceback:
frames = inspect.getinnerframes(traceback, context=0)
_locals = ['Locals (most recent call last):']
for frame, filename, lineno, function, __, __ in frames:
if '/apps/' in filename:
_locals.append('File "{}", line {}, in {}\n{}'.format(filename, lineno, function, json.dumps(frame.f_locals, default=str, indent=4)))
return '\n'.join(_locals)