No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 
 
 

235 líneas
6.4 KiB

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