The license.txt file has been replaced with LICENSE for quite a while now. INAL but it didn't seem accurate to say "hey, checkout license.txt although there's no such file". Apart from this, there were inconsistencies in the headers altogether...this change brings consistency.
196 lines
4.8 KiB
Python
196 lines
4.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: MIT. See LICENSE
|
|
from collections import Counter
|
|
import datetime
|
|
import inspect
|
|
import json
|
|
import re
|
|
import time
|
|
import frappe
|
|
import sqlparse
|
|
|
|
from frappe import _
|
|
|
|
RECORDER_INTERCEPT_FLAG = "recorder-intercept"
|
|
RECORDER_REQUEST_SPARSE_HASH = "recorder-requests-sparse"
|
|
RECORDER_REQUEST_HASH = "recorder-requests"
|
|
|
|
|
|
def sql(*args, **kwargs):
|
|
start_time = time.time()
|
|
result = frappe.db._sql(*args, **kwargs)
|
|
end_time = time.time()
|
|
|
|
stack = list(get_current_stack_frames())
|
|
|
|
if frappe.db.db_type == "postgres":
|
|
query = frappe.db._cursor.query
|
|
else:
|
|
query = frappe.db._cursor._executed
|
|
|
|
query = sqlparse.format(query.strip(), keyword_case="upper", reindent=True)
|
|
|
|
# Collect EXPLAIN for executed query
|
|
if query.lower().strip().split()[0] in ("select", "update", "delete"):
|
|
# Only SELECT/UPDATE/DELETE queries can be "EXPLAIN"ed
|
|
explain_result = frappe.db._sql("EXPLAIN {}".format(query), as_dict=True)
|
|
else:
|
|
explain_result = []
|
|
|
|
data = {
|
|
"query": query,
|
|
"stack": stack,
|
|
"explain_result": explain_result,
|
|
"time": start_time,
|
|
"duration": float("{:.3f}".format((end_time - start_time) * 1000)),
|
|
}
|
|
|
|
frappe.local._recorder.register(data)
|
|
return result
|
|
|
|
|
|
def get_current_stack_frames():
|
|
try:
|
|
current = inspect.currentframe()
|
|
frames = inspect.getouterframes(current, context=10)
|
|
for frame, filename, lineno, function, context, index in list(reversed(frames))[:-2]:
|
|
if "/apps/" in filename:
|
|
yield {
|
|
"filename": re.sub(".*/apps/", "", filename),
|
|
"lineno": lineno,
|
|
"function": function,
|
|
}
|
|
except Exception:
|
|
pass
|
|
|
|
def record():
|
|
if __debug__:
|
|
if frappe.cache().get_value(RECORDER_INTERCEPT_FLAG):
|
|
frappe.local._recorder = Recorder()
|
|
|
|
|
|
def dump():
|
|
if __debug__:
|
|
if hasattr(frappe.local, "_recorder"):
|
|
frappe.local._recorder.dump()
|
|
|
|
|
|
class Recorder:
|
|
def __init__(self):
|
|
self.uuid = frappe.generate_hash(length=10)
|
|
self.time = datetime.datetime.now()
|
|
self.calls = []
|
|
self.path = frappe.request.path
|
|
self.cmd = frappe.local.form_dict.cmd or ""
|
|
self.method = frappe.request.method
|
|
self.headers = dict(frappe.local.request.headers)
|
|
self.form_dict = frappe.local.form_dict
|
|
_patch()
|
|
|
|
def register(self, data):
|
|
self.calls.append(data)
|
|
|
|
def dump(self):
|
|
request_data = {
|
|
"uuid": self.uuid,
|
|
"path": self.path,
|
|
"cmd": self.cmd,
|
|
"time": self.time,
|
|
"queries": len(self.calls),
|
|
"time_queries": float(
|
|
"{:0.3f}".format(sum(call["duration"] for call in self.calls))
|
|
),
|
|
"duration": float(
|
|
"{:0.3f}".format((datetime.datetime.now() - self.time).total_seconds() * 1000)
|
|
),
|
|
"method": self.method,
|
|
}
|
|
frappe.cache().hset(RECORDER_REQUEST_SPARSE_HASH, self.uuid, request_data)
|
|
frappe.publish_realtime(
|
|
event="recorder-dump-event", message=json.dumps(request_data, default=str)
|
|
)
|
|
|
|
self.mark_duplicates()
|
|
|
|
request_data["calls"] = self.calls
|
|
request_data["headers"] = self.headers
|
|
request_data["form_dict"] = self.form_dict
|
|
frappe.cache().hset(RECORDER_REQUEST_HASH, self.uuid, request_data)
|
|
|
|
def mark_duplicates(self):
|
|
counts = Counter([call["query"] for call in self.calls])
|
|
for index, call in enumerate(self.calls):
|
|
call["index"] = index
|
|
call["exact_copies"] = counts[call["query"]]
|
|
|
|
|
|
def _patch():
|
|
frappe.db._sql = frappe.db.sql
|
|
frappe.db.sql = sql
|
|
|
|
|
|
def do_not_record(function):
|
|
def wrapper(*args, **kwargs):
|
|
if hasattr(frappe.local, "_recorder"):
|
|
del frappe.local._recorder
|
|
frappe.db.sql = frappe.db._sql
|
|
return function(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
def administrator_only(function):
|
|
def wrapper(*args, **kwargs):
|
|
if frappe.session.user != "Administrator":
|
|
frappe.throw(_("Only Administrator is allowed to use Recorder"))
|
|
return function(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
@frappe.whitelist()
|
|
@do_not_record
|
|
@administrator_only
|
|
def status(*args, **kwargs):
|
|
return bool(frappe.cache().get_value(RECORDER_INTERCEPT_FLAG))
|
|
|
|
|
|
@frappe.whitelist()
|
|
@do_not_record
|
|
@administrator_only
|
|
def start(*args, **kwargs):
|
|
frappe.cache().set_value(RECORDER_INTERCEPT_FLAG, 1)
|
|
|
|
|
|
@frappe.whitelist()
|
|
@do_not_record
|
|
@administrator_only
|
|
def stop(*args, **kwargs):
|
|
frappe.cache().delete_value(RECORDER_INTERCEPT_FLAG)
|
|
|
|
|
|
@frappe.whitelist()
|
|
@do_not_record
|
|
@administrator_only
|
|
def get(uuid=None, *args, **kwargs):
|
|
if uuid:
|
|
result = frappe.cache().hget(RECORDER_REQUEST_HASH, uuid)
|
|
else:
|
|
result = list(frappe.cache().hgetall(RECORDER_REQUEST_SPARSE_HASH).values())
|
|
return result
|
|
|
|
|
|
@frappe.whitelist()
|
|
@do_not_record
|
|
@administrator_only
|
|
def export_data(*args, **kwargs):
|
|
return list(frappe.cache().hgetall(RECORDER_REQUEST_HASH).values())
|
|
|
|
|
|
@frappe.whitelist()
|
|
@do_not_record
|
|
@administrator_only
|
|
def delete(*args, **kwargs):
|
|
frappe.cache().delete_value(RECORDER_REQUEST_SPARSE_HASH)
|
|
frappe.cache().delete_value(RECORDER_REQUEST_HASH)
|