diff --git a/frappe/core/doctype/recorder/recorder_list.js b/frappe/core/doctype/recorder/recorder_list.js index 528ebc5089..8ec9f2ae40 100644 --- a/frappe/core/doctype/recorder/recorder_list.js +++ b/frappe/core/doctype/recorder/recorder_list.js @@ -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. /api/method/erpnext", + }, + { + 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. email_queue.pull", + }, { 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. /api/method/erpnext", - }, ], (values) => { frappe.xcall("frappe.recorder.start", values).then(() => { diff --git a/frappe/recorder.py b/frappe/recorder.py index b9301c30ac..bcc3f55b9e 100644 --- a/frappe/recorder.py +++ b/frappe/recorder.py @@ -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()