You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

237 line
6.5 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2015, Maxwell Morais and contributors
  3. # For license information, please see license.txt
  4. from __future__ import unicode_literals
  5. import os
  6. import sys
  7. import traceback
  8. import functools
  9. import frappe
  10. from frappe.utils import cstr, encode
  11. import inspect
  12. import linecache
  13. import pydoc
  14. import cgitb
  15. import datetime
  16. import json
  17. import six
  18. def make_error_snapshot(exception):
  19. if frappe.conf.disable_error_snapshot:
  20. return
  21. logger = frappe.logger(with_more_info=True)
  22. try:
  23. error_id = '{timestamp:s}-{ip:s}-{hash:s}'.format(
  24. timestamp=cstr(datetime.datetime.now()),
  25. ip=frappe.local.request_ip or '127.0.0.1',
  26. hash=frappe.generate_hash(length=3)
  27. )
  28. snapshot_folder = get_error_snapshot_path()
  29. frappe.create_folder(snapshot_folder)
  30. snapshot_file_path = os.path.join(snapshot_folder, "{0}.json".format(error_id))
  31. snapshot = get_snapshot(exception)
  32. with open(encode(snapshot_file_path), 'wb') as error_file:
  33. error_file.write(encode(frappe.as_json(snapshot)))
  34. logger.error('New Exception collected with id: {}'.format(error_id))
  35. except Exception as e:
  36. logger.error('Could not take error snapshot: {0}'.format(e), exc_info=True)
  37. def get_snapshot(exception, context=10):
  38. """
  39. Return a dict describing a given traceback (based on cgitb.text)
  40. """
  41. etype, evalue, etb = sys.exc_info()
  42. if isinstance(etype, six.class_types):
  43. etype = etype.__name__
  44. # creates a snapshot dict with some basic information
  45. s = {
  46. 'pyver': 'Python {version:s}: {executable:s} (prefix: {prefix:s})'.format(
  47. version = sys.version.split()[0],
  48. executable = sys.executable,
  49. prefix = sys.prefix
  50. ),
  51. 'timestamp': cstr(datetime.datetime.now()),
  52. 'traceback': traceback.format_exc(),
  53. 'frames': [],
  54. 'etype': cstr(etype),
  55. 'evalue': cstr(repr(evalue)),
  56. 'exception': {},
  57. 'locals': {}
  58. }
  59. # start to process frames
  60. records = inspect.getinnerframes(etb, 5)
  61. for frame, file, lnum, func, lines, index in records:
  62. file = file and os.path.abspath(file) or '?'
  63. args, varargs, varkw, locals = inspect.getargvalues(frame)
  64. call = ''
  65. if func != '?':
  66. call = inspect.formatargvalues(args, varargs, varkw, locals, formatvalue=lambda value: '={}'.format(pydoc.text.repr(value)))
  67. # basic frame information
  68. f = {'file': file, 'func': func, 'call': call, 'lines': {}, 'lnum': lnum}
  69. def reader(lnum=[lnum]):
  70. try:
  71. return linecache.getline(file, lnum[0])
  72. finally:
  73. lnum[0] += 1
  74. vars = cgitb.scanvars(reader, frame, locals)
  75. # if it is a view, replace with generated code
  76. # if file.endswith('html'):
  77. # lmin = lnum > context and (lnum - context) or 0
  78. # lmax = lnum + context
  79. # lines = code.split("\n")[lmin:lmax]
  80. # index = min(context, lnum) - 1
  81. if index is not None:
  82. i = lnum - index
  83. for line in lines:
  84. f['lines'][i] = line.rstrip()
  85. i += 1
  86. # dump local variable (referenced in current line only)
  87. f['dump'] = {}
  88. for name, where, value in vars:
  89. if name in f['dump']:
  90. continue
  91. if value is not cgitb.__UNDEF__:
  92. if where == 'global':
  93. name = 'global {name:s}'.format(name=name)
  94. elif where != 'local':
  95. name = where + ' ' + name.split('.')[-1]
  96. f['dump'][name] = pydoc.text.repr(value)
  97. else:
  98. f['dump'][name] = 'undefined'
  99. s['frames'].append(f)
  100. # add exception type, value and attributes
  101. if isinstance(evalue, BaseException):
  102. for name in dir(evalue):
  103. if name != 'messages' and not name.startswith('__'):
  104. value = pydoc.text.repr(getattr(evalue, name))
  105. s['exception'][name] = encode(value)
  106. # add all local values (of last frame) to the snapshot
  107. for name, value in locals.items():
  108. s['locals'][name] = value if isinstance(value, six.text_type) else pydoc.text.repr(value)
  109. return s
  110. def collect_error_snapshots():
  111. """Scheduled task to collect error snapshots from files and push into Error Snapshot table"""
  112. if frappe.conf.disable_error_snapshot:
  113. return
  114. try:
  115. path = get_error_snapshot_path()
  116. if not os.path.exists(path):
  117. return
  118. for fname in os.listdir(path):
  119. fullpath = os.path.join(path, fname)
  120. try:
  121. with open(fullpath, 'r') as filedata:
  122. data = json.load(filedata)
  123. except ValueError:
  124. # empty file
  125. os.remove(fullpath)
  126. continue
  127. for field in ['locals', 'exception', 'frames']:
  128. data[field] = frappe.as_json(data[field])
  129. doc = frappe.new_doc('Error Snapshot')
  130. doc.update(data)
  131. doc.save()
  132. frappe.db.commit()
  133. os.remove(fullpath)
  134. clear_old_snapshots()
  135. except Exception as e:
  136. make_error_snapshot(e)
  137. # prevent creation of unlimited error snapshots
  138. raise
  139. def clear_old_snapshots():
  140. """Clear snapshots that are older than a month"""
  141. frappe.db.sql("""delete from `tabError Snapshot`
  142. where creation < (NOW() - INTERVAL '1' MONTH)""")
  143. path = get_error_snapshot_path()
  144. today = datetime.datetime.now()
  145. for file in os.listdir(path):
  146. p = os.path.join(path, file)
  147. ctime = datetime.datetime.fromtimestamp(os.path.getctime(p))
  148. if (today - ctime).days > 31:
  149. os.remove(os.path.join(path, p))
  150. def get_error_snapshot_path():
  151. return frappe.get_site_path('error-snapshots')
  152. def get_default_args(func):
  153. """Get default arguments of a function from its signature.
  154. """
  155. signature = inspect.signature(func)
  156. return {k: v.default
  157. for k, v in signature.parameters.items() if v.default is not inspect.Parameter.empty}
  158. def raise_error_on_no_output(error_message, error_type=None, keep_quiet=None):
  159. """Decorate any function to throw error incase of missing output.
  160. TODO: Remove keep_quiet flag after testing and fixing sendmail flow.
  161. :param error_message: error message to raise
  162. :param error_type: type of error to raise
  163. :param keep_quiet: control error raising with external factor.
  164. :type error_message: str
  165. :type error_type: Exception Class
  166. :type keep_quiet: function
  167. >>> @raise_error_on_no_output("Ingradients missing")
  168. ... def get_indradients(_raise_error=1): return
  169. ...
  170. >>> get_ingradients()
  171. `Exception Name`: Ingradients missing
  172. """
  173. def decorator_raise_error_on_no_output(func):
  174. @functools.wraps(func)
  175. def wrapper_raise_error_on_no_output(*args, **kwargs):
  176. response = func(*args, **kwargs)
  177. if callable(keep_quiet) and keep_quiet():
  178. return response
  179. default_kwargs = get_default_args(func)
  180. default_raise_error = default_kwargs.get('_raise_error')
  181. raise_error = kwargs.get('_raise_error') if '_raise_error' in kwargs else default_raise_error
  182. if (not response) and raise_error:
  183. frappe.throw(error_message, error_type or Exception)
  184. return response
  185. return wrapper_raise_error_on_no_output
  186. return decorator_raise_error_on_no_output