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.
 
 
 
 
 
 

190 regels
4.7 KiB

  1. # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. import datetime
  4. import inspect
  5. import json
  6. import re
  7. import time
  8. from collections import Counter
  9. import sqlparse
  10. import frappe
  11. from frappe import _
  12. from frappe.database.database import is_query_type
  13. RECORDER_INTERCEPT_FLAG = "recorder-intercept"
  14. RECORDER_REQUEST_SPARSE_HASH = "recorder-requests-sparse"
  15. RECORDER_REQUEST_HASH = "recorder-requests"
  16. TRACEBACK_PATH_PATTERN = re.compile(".*/apps/")
  17. def sql(*args, **kwargs):
  18. start_time = time.time()
  19. result = frappe.db._sql(*args, **kwargs)
  20. end_time = time.time()
  21. stack = list(get_current_stack_frames())
  22. query = sqlparse.format(str(frappe.db.last_query).strip(), keyword_case="upper", reindent=True)
  23. # Collect EXPLAIN for executed query
  24. if is_query_type(query, ("select", "update", "delete")):
  25. # Only SELECT/UPDATE/DELETE queries can be "EXPLAIN"ed
  26. explain_result = frappe.db._sql(f"EXPLAIN {query}", as_dict=True)
  27. else:
  28. explain_result = []
  29. data = {
  30. "query": query,
  31. "stack": stack,
  32. "explain_result": explain_result,
  33. "time": start_time,
  34. "duration": float(f"{(end_time - start_time) * 1000:.3f}"),
  35. }
  36. frappe.local._recorder.register(data)
  37. return result
  38. def get_current_stack_frames():
  39. try:
  40. current = inspect.currentframe()
  41. frames = inspect.getouterframes(current, context=10)
  42. for frame, filename, lineno, function, context, index in list(reversed(frames))[:-2]:
  43. if "/apps/" in filename:
  44. yield {
  45. "filename": TRACEBACK_PATH_PATTERN.sub("", filename),
  46. "lineno": lineno,
  47. "function": function,
  48. }
  49. except Exception:
  50. pass
  51. def record():
  52. if __debug__:
  53. if frappe.cache().get_value(RECORDER_INTERCEPT_FLAG):
  54. frappe.local._recorder = Recorder()
  55. def dump():
  56. if __debug__:
  57. if hasattr(frappe.local, "_recorder"):
  58. frappe.local._recorder.dump()
  59. class Recorder:
  60. def __init__(self):
  61. self.uuid = frappe.generate_hash(length=10)
  62. self.time = datetime.datetime.now()
  63. self.calls = []
  64. self.path = frappe.request.path
  65. self.cmd = frappe.local.form_dict.cmd or ""
  66. self.method = frappe.request.method
  67. self.headers = dict(frappe.local.request.headers)
  68. self.form_dict = frappe.local.form_dict
  69. _patch()
  70. def register(self, data):
  71. self.calls.append(data)
  72. def dump(self):
  73. request_data = {
  74. "uuid": self.uuid,
  75. "path": self.path,
  76. "cmd": self.cmd,
  77. "time": self.time,
  78. "queries": len(self.calls),
  79. "time_queries": float("{:0.3f}".format(sum(call["duration"] for call in self.calls))),
  80. "duration": float(f"{(datetime.datetime.now() - self.time).total_seconds() * 1000:0.3f}"),
  81. "method": self.method,
  82. }
  83. frappe.cache().hset(RECORDER_REQUEST_SPARSE_HASH, self.uuid, request_data)
  84. frappe.publish_realtime(
  85. event="recorder-dump-event", message=json.dumps(request_data, default=str)
  86. )
  87. self.mark_duplicates()
  88. request_data["calls"] = self.calls
  89. request_data["headers"] = self.headers
  90. request_data["form_dict"] = self.form_dict
  91. frappe.cache().hset(RECORDER_REQUEST_HASH, self.uuid, request_data)
  92. def mark_duplicates(self):
  93. counts = Counter([call["query"] for call in self.calls])
  94. for index, call in enumerate(self.calls):
  95. call["index"] = index
  96. call["exact_copies"] = counts[call["query"]]
  97. def _patch():
  98. frappe.db._sql = frappe.db.sql
  99. frappe.db.sql = sql
  100. def do_not_record(function):
  101. def wrapper(*args, **kwargs):
  102. if hasattr(frappe.local, "_recorder"):
  103. del frappe.local._recorder
  104. frappe.db.sql = frappe.db._sql
  105. return function(*args, **kwargs)
  106. return wrapper
  107. def administrator_only(function):
  108. def wrapper(*args, **kwargs):
  109. if frappe.session.user != "Administrator":
  110. frappe.throw(_("Only Administrator is allowed to use Recorder"))
  111. return function(*args, **kwargs)
  112. return wrapper
  113. @frappe.whitelist()
  114. @do_not_record
  115. @administrator_only
  116. def status(*args, **kwargs):
  117. return bool(frappe.cache().get_value(RECORDER_INTERCEPT_FLAG))
  118. @frappe.whitelist()
  119. @do_not_record
  120. @administrator_only
  121. def start(*args, **kwargs):
  122. frappe.cache().set_value(RECORDER_INTERCEPT_FLAG, 1)
  123. @frappe.whitelist()
  124. @do_not_record
  125. @administrator_only
  126. def stop(*args, **kwargs):
  127. frappe.cache().delete_value(RECORDER_INTERCEPT_FLAG)
  128. @frappe.whitelist()
  129. @do_not_record
  130. @administrator_only
  131. def get(uuid=None, *args, **kwargs):
  132. if uuid:
  133. result = frappe.cache().hget(RECORDER_REQUEST_HASH, uuid)
  134. else:
  135. result = list(frappe.cache().hgetall(RECORDER_REQUEST_SPARSE_HASH).values())
  136. return result
  137. @frappe.whitelist()
  138. @do_not_record
  139. @administrator_only
  140. def export_data(*args, **kwargs):
  141. return list(frappe.cache().hgetall(RECORDER_REQUEST_HASH).values())
  142. @frappe.whitelist()
  143. @do_not_record
  144. @administrator_only
  145. def delete(*args, **kwargs):
  146. frappe.cache().delete_value(RECORDER_REQUEST_SPARSE_HASH)
  147. frappe.cache().delete_value(RECORDER_REQUEST_HASH)