feat: Jobs/Request filtering

This commit is contained in:
Ankush Menat 2024-02-03 14:46:55 +05:30
parent e494cb4f6e
commit 009b06d974
2 changed files with 86 additions and 46 deletions

View file

@ -10,12 +10,7 @@ frappe.listview_settings["Recorder"] = {
}
listview.page.add_button(__("Clear"), () => {
frappe.call({
method: "frappe.recorder.delete",
callback: function () {
listview.refresh();
},
});
frappe.xcall("frappe.recorder.delete").then(listview.refresh);
});
listview.page.add_menu_item(__("Import"), () => {
@ -111,6 +106,51 @@ frappe.listview_settings["Recorder"] = {
let me = this;
frappe.prompt(
[
{
fieldtype: "Section Break",
fieldname: "req_job_section",
},
{
fieldtype: "Column Break",
fieldname: "web_request_columns",
label: "Web Requests",
},
{
fieldname: "record_requests",
fieldtype: "Check",
label: "Record Web Requests",
default: 1,
},
{
fieldname: "request_filter",
fieldtype: "Data",
label: "Request path filter",
default: "/",
depends_on: "record_requests",
description:
"This will be used for filtering paths which will be recorded. You can use this to avoid slowing down other traffic. e.g. <code>/api/method/erpnext</code>",
},
{
fieldtype: "Column Break",
fieldname: "background_col",
label: "Background Jobs",
},
{
fieldname: "record_jobs",
fieldtype: "Check",
label: "Record Background Jobs",
default: 1,
},
{
fieldname: "jobs_filter",
fieldtype: "Data",
label: "Background Jobs filter",
default: "",
depends_on: "record_jobs",
description:
"This will be used for filtering jobs which will be recorded. You can use this to avoid slowing down other jobs. e.g. <code>email_queue.pull</code>",
},
{
fieldtype: "Section Break",
fieldname: "sql_section",
@ -119,7 +159,7 @@ frappe.listview_settings["Recorder"] = {
{
fieldname: "record_sql",
fieldtype: "Check",
label: "Record SQL Queries",
label: "Record SQL queries",
default: 1,
},
{
@ -147,18 +187,6 @@ frappe.listview_settings["Recorder"] = {
description:
"Warning: cProfile adds a lot of overhead. For best results, disable stack capturing when using cProfile.",
},
{
fieldtype: "Section Break",
fieldname: "filter_section",
},
{
fieldname: "filter",
fieldtype: "Data",
label: "Filter Path",
default: "/",
description:
"This will be used for filtering paths which will be recorded. You can use this to avoid slowing down other traffic. e.g. <code>/api/method/erpnext</code>",
},
],
(values) => {
frappe.xcall("frappe.recorder.start", values).then(() => {

View file

@ -29,11 +29,18 @@ RECORDER_AUTO_DISABLE = 5 * 60
@dataclass
class RecorderConfig:
record_sql: bool = True
capture_stack: bool = True
profile: bool = False
explain: bool = True
filter: str = "/"
record_requests: bool = True # Record web request
record_jobs: bool = True # record background jobs
record_sql: bool = True # Record SQL queries
capture_stack: bool = True # Recod call stack of SQL queries
profile: bool = False # Run cProfile
explain: bool = True # Provide explain output of SQL queries
request_filter: str = "/" # Filter request paths
jobs_filter: str = "" # Filter background jobs
def __post_init__(self):
if not (self.record_jobs or self.record_requests):
frappe.throw("You must record one of jobs or requests")
def store(self):
frappe.cache.set_value(RECORDER_CONFIG_FLAG, self, expires_in_sec=RECORDER_AUTO_DISABLE)
@ -171,24 +178,23 @@ def dump():
class Recorder:
def __init__(self):
self.config = RecorderConfig.retrieve()
self.uuid = frappe.generate_hash(length=10)
self.time = datetime.datetime.now()
self.calls = []
self._patched_sql = False
self.profiler = None
if self.config.profile:
self.profiler = cProfile.Profile()
self.profiler.enable()
self._recording = True
if frappe.request:
if (
self.config.record_requests
and frappe.request
and self.config.request_filter in frappe.request.path
):
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
self.event_type = "HTTP Request"
elif frappe.job:
elif self.config.record_jobs and frappe.job and self.config.jobs_filter in frappe.job.method:
self.event_type = "Background Job"
self.path = frappe.job.method
self.cmd = None
@ -196,17 +202,20 @@ class Recorder:
self.headers = None
self.form_dict = None
else:
self.event_type = None
self.path = None
self.cmd = None
self.method = None
self.headers = None
self.form_dict = None
self._recording = False
return
self.uuid = frappe.generate_hash(length=10)
self.time = datetime.datetime.now()
if self.config.record_sql:
self._patch_sql()
self._patched_sql = True
if self.config.profile:
self.profiler = cProfile.Profile()
self.profiler.enable()
def register(self, data):
self.calls.append(data)
@ -228,6 +237,8 @@ class Recorder:
return profile
def dump(self):
if not self._recording:
return
profiler_output = self.process_profiler()
request_data = {
@ -242,11 +253,6 @@ class Recorder:
"event_type": self.event_type,
}
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),
user="Administrator",
)
request_data["calls"] = self.calls
request_data["headers"] = self.headers
@ -299,22 +305,28 @@ def status(*args, **kwargs):
@do_not_record
@administrator_only
def start(
record_jobs: bool = True,
record_requests: bool = True,
record_sql: bool = True,
profile: bool = False,
capture_stack: bool = True,
explain: bool = True,
filter: str = "/",
request_filter: str = "/",
jobs_filter: str = "",
*args,
**kwargs,
):
frappe.cache.set_value(RECORDER_INTERCEPT_FLAG, 1, expires_in_sec=RECORDER_AUTO_DISABLE)
RecorderConfig(
record_requests=int(record_requests),
record_jobs=int(record_jobs),
record_sql=int(record_sql),
profile=int(profile),
capture_stack=int(capture_stack),
explain=int(explain),
filter=filter,
request_filter=request_filter,
jobs_filter=jobs_filter,
).store()
frappe.cache.set_value(RECORDER_INTERCEPT_FLAG, 1, expires_in_sec=RECORDER_AUTO_DISABLE)
@frappe.whitelist()