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.
 
 
 
 
 
 

197 lines
4.8 KiB

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