refactor(Recorder): Cleanup

This commit is contained in:
Aditya Hase 2019-02-17 20:43:19 +05:30
parent 1e1456afa0
commit 614a8f0b46
4 changed files with 93 additions and 213 deletions

View file

@ -575,7 +575,7 @@ def browse(context, site):
def start_recording(context):
for site in context.sites:
frappe.init(site=site)
frappe.cache().set("recorder-intercept", 1)
frappe.recorder.start()
@click.command('stop-recording')
@ -583,7 +583,7 @@ def start_recording(context):
def stop_recording(context):
for site in context.sites:
frappe.init(site=site)
frappe.cache().delete("recorder-intercept")
frappe.recorder.stop()
commands = [

View file

@ -33,7 +33,7 @@
<div class="list-row-col ellipsis list-subject level ">
<span class="level-item">{{ columns[0].label }}</span>
</div>
<div class="list-row-col ellipsis hidden-xs" v-for="(column, index) in columns.slice(1)" :key="index">
<div class="list-row-col ellipsis hidden-xs" v-for="(column, index) in columns.slice(1)" :key="index" :class="{'text-right': column.number}">
<span>{{ column.label }}</span>
</div>
</div>
@ -44,7 +44,7 @@
</div>
<div class="result-list">
<router-link class="list-row-container" v-for="(request, index) in paginated(sorted(filtered(requests)))" :key="index" :to="{name: 'request-detail', params: {id: request.id}}" tag="div" v-bind="request">
<router-link class="list-row-container" v-for="(request, index) in paginated(sorted(filtered(requests)))" :key="index" :to="{name: 'request-detail', params: {id: request.uuid}}" tag="div" v-bind="request">
<div class="level list-row small">
<div class="level-left ellipsis">
<div class="list-row-col ellipsis list-subject level ">
@ -52,7 +52,7 @@
{{ request[columns[0].slug] }}
</span>
</div>
<div class="list-row-col ellipsis" v-for="(column, index) in columns.slice(1)" :key="index">
<div class="list-row-col ellipsis" v-for="(column, index) in columns.slice(1)" :key="index" :class="{'text-right': column.number}">
<span class="ellipsis text-muted">{{ request[column.slug] }}</span>
</div>
</div>
@ -70,7 +70,7 @@
<div v-if="requests.length == 0" class="no-result text-muted flex justify-center align-center" style="">
<div class="msg-box no-border" v-if="status.status == 'Inactive'" >
<p>Recorder is Inactive</p>
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="record(true)">Start Recording</button></p>
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">Start Recording</button></p>
</div>
<div class="msg-box no-border" v-if="status.status == 'Active'" >
<p>No Requests found</p>
@ -107,11 +107,10 @@ export default {
requests: [],
columns: [
{label: "Time", slug: "time", sortable: true},
{label: "Duration", slug: "duration", sortable: true},
{label: "Time in Queries", slug: "time_queries", sortable: true},
{label: "Queries", slug: "queries", sortable: true},
{label: "Duration (ms)", slug: "duration", sortable: true, number: true},
{label: "Time in Queries (ms)", slug: "time_queries", sortable: true, number: true},
{label: "Queries", slug: "queries", sortable: true, number: true},
{label: "Method", slug: "method"},
],
query: {
sort: "time",
@ -127,11 +126,9 @@ export default {
color: "grey",
status: "Unknown",
},
last_fetched: null,
};
},
mounted() {
frappe.socketio.init(9000);
this.fetch_status();
this.refresh();
this.$root.page.set_secondary_action("Clear", () => {
@ -188,49 +185,35 @@ export default {
const sort = this.query.sort;
return requests.sort((a,b) => (a[sort] > b[sort]) ? order : -order);
},
sort: function(key) {
if(key == this.query.sort) {
this.query.order = (this.query.order == "asc") ? "desc" : "asc";
} else {
this.query.order = "asc";
}
this.query.sort = key;
},
glyphicon: function(key) {
if(key == this.query.sort) {
return (this.query.order == "asc") ? "glyphicon-sort-by-attributes" : "glyphicon-sort-by-attributes-alt";
} else {
return "glyphicon-sort";
}
},
refresh: function() {
frappe.call("frappe.recorder.get").then( r => {
this.requests = r.message;
this.last_fetched = new Date();
});
frappe.call("frappe.recorder.get").then( r => this.requests = r.message);
},
update: function(message) {
this.requests.push(JSON.parse(message));
},
clear: function() {
frappe.call("frappe.recorder.delete");
this.refresh();
frappe.call("frappe.recorder.delete").then(r => this.refresh());
},
record: function(should_record) {
frappe.call({
method: "frappe.recorder.set_recorder_state",
args: {
should_record: should_record
}
}).then(r => this.update_status(r.message));
start: function() {
frappe.call("frappe.recorder.start").then(r => this.fetch_status());
},
stop: function() {
frappe.call("frappe.recorder.stop").then(r => this.fetch_status());
},
fetch_status: function() {
frappe.call("frappe.recorder.get_status").then(r => this.update_status(r.message));
frappe.call("frappe.recorder.status").then(r => this.update_status(r.message));
},
update_status: function(status) {
this.status = status;
update_status: function(result) {
if(result) {
this.status = {status: "Active", color: "green"}
} else {
this.status = {status: "Inactive", color: "red"}
}
this.$root.page.set_indicator(this.status.status, this.status.color);
if(this.status.status == "Active") {
frappe.realtime.on("recorder-dump-event", this.refresh);
frappe.realtime.on("recorder-dump-event", this.update);
} else {
frappe.realtime.off("recorder-dump-event", this.refresh);
frappe.realtime.off("recorder-dump-event", this.update);
}
this.update_buttons();
@ -238,22 +221,14 @@ export default {
update_buttons: function() {
if(this.status.status == "Active") {
this.$root.page.set_primary_action("Stop", () => {
this.record(false);
this.stop();
});
} else {
this.$root.page.set_primary_action("Start", () => {
this.record(true);
this.start();
});
}
},
},
filters: {
elipsize: function (value) {
if (!value) return '';
if (value.length > 30)
return value.substring(0, 30-3)+'...';
return value;
}
}
};
</script>

View file

@ -1,85 +1,13 @@
<template>
<div>
<div class="row form-section visible-section shaded-section">
<div class="section-body">
<div class="form-column col-sm-6">
<form>
<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="data_1" title="data_1">
<div class="form-group">
<div class="clearfix"> <label class="control-label" style="padding-right: 0px;">Path</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input" style="">{{ request.path }}</div>
</div>
</div>
</div>
<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="data_2" title="data_2">
<div class="form-group">
<div class="clearfix"> <label class="control-label" style="padding-right: 0px;">CMD</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input" style="">{{ request.cmd }}</div>
</div>
</div>
</div>
<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="data_3" title="data_3">
<div class="form-group">
<div class="clearfix"> <label class="control-label" style="padding-right: 0px;">Method</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input" style="">{{ request.method }}</div>
</div>
</div>
</div>
</form>
</div>
<div class="form-column col-sm-6">
<form>
<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="data_4" title="data_4">
<div class="form-group">
<div class="clearfix"> <label class="control-label" style="padding-right: 0px;">Duration</label>
</div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input" style="">{{ request.duration }}</div>
</div>
</div>
</div>
<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="queries_duration"
title="queries_duration">
<div class="form-group">
<div class="clearfix"> <label class="control-label" style="padding-right: 0px;">Queries Duration</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input" style="">{{ request.time_queries }}</div>
</div>
</div>
</div>
<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="time" title="time">
<div class="form-group">
<div class="clearfix"> <label class="control-label" style="padding-right: 0px;">Time</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input" style="">{{ request.time }}</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="row form-section visible-section">
<div class="section-body">
<div class="form-column col-sm-12">
<form>
<div class="frappe-control" data-fieldtype="Small Text" data-fieldname="request_headers" title="request_headers">
<div class="frappe-control input-max-width col-sm-6" :data-fieldtype="column.type" v-for="(column, index) in columns" :key="index">
<div class="form-group">
<div class="clearfix"> <label class="control-label" style="padding-right: 0px;">Request Headers</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input for-description" style=""><pre>{{ JSON.stringify(request.http.headers, null, 1) }}</pre></div>
</div>
</div>
</div>
<div class="frappe-control" data-fieldtype="Small Text" data-fieldname="reponse_headers" title="reponse_headers">
<div class="form-group">
<div class="clearfix"> <label class="control-label" style="padding-right: 0px;">Form Dict</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input for-description" style=""><pre>{{ JSON.stringify(request.http.data, null, 1) }}</pre></div>
</div>
<div class="clearfix"><label class="control-label">{{ column.label }}</label></div>
<div class="control-value like-disabled-input">{{ request[column.slug] }}</div>
</div>
</div>
</form>
@ -93,19 +21,19 @@
<div class="section-body">
<div class="form-column col-sm-12">
<form>
<div class="frappe-control" data-fieldtype="Table" data-fieldname="fields" title="fields">
<div data-fieldname="fields">
<div class="frappe-control" data-fieldtype="Table">
<div>
<div class="form-grid">
<div class="grid-heading-row">
<div class="grid-row">
<div class="data-row row">
<div class="row-index sortable-handle col col-xs-1">
<div class="row-index col col-xs-1">
<span>Index</span></div>
<div class="col grid-static-col col-xs-8">
<div class="static-area ellipsis">Query</div>
</div>
<div class="col grid-static-col col-xs-2">
<div class="static-area ellipsis">Duration</div>
<div class="static-area ellipsis">Duration (ms)</div>
</div>
</div>
</div>
@ -114,25 +42,21 @@
<div class="rows">
<div class="grid-row" :class="showing == index ? 'grid-row-open' : ''" v-for="(call, index) in request.calls" :key="index" v-bind="call">
<div class="data-row row" v-if="showing != index" style="display: block;" @click="showing = index" >
<div class="row-index sortable-handle col col-xs-1">
<span>{{ index }}</span></div>
<div class="col grid-static-col col-xs-8 " data-fieldname="code" data-fieldtype="Code">
<div class="static-area">
<span>{{ call.query }}</span>
</div>
<div class="row-index col col-xs-1"><span>{{ index }}</span></div>
<div class="col grid-static-col col-xs-8" data-fieldtype="Code">
<div class="static-area"><span>{{ call.query }}</span></div>
</div>
<div class="col grid-static-col col-xs-2 " data-fieldname="duration" data-fieldtype="Time">
<div class="col grid-static-col col-xs-2">
<div class="static-area ellipsis">{{ call.duration }}</div>
</div>
<div class="col col-xs-1 sortable-handle"><a class="close btn-open-row">
<div class="col col-xs-1"><a class="close btn-open-row">
<span class="octicon octicon-triangle-down"></span></a>
</div>
</div>
<div class="form-in-grid">
<div class="grid-form-heading" @click="showing = null">
<div class="grid-form-heading" @click="showing = null">
<div class="toolbar grid-header-toolbar">
<span class="panel-title">
SQL Query #<span class="grid-form-row-index">{{ index }}</span></span>
<span class="panel-title">SQL Query #<span class="grid-form-row-index">{{ index }}</span></span>
<div class="btn btn-default btn-xs pull-right" style="margin-left: 7px;">
<span class="hidden-xs octicon octicon-triangle-up"></span>
</div>
@ -148,26 +72,20 @@
<form>
<div class="frappe-control">
<div class="form-group">
<div class="clearfix"><label class="control-label" style="padding-right: 0px;">Query</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input for-description"><pre>{{ call.query }}</pre></div>
</div>
<div class="clearfix"><label class="control-label">Query</label></div>
<div class="control-value like-disabled-input for-description"><pre>{{ call.query }}</pre></div>
</div>
</div>
<div class="frappe-control input-max-width">
<div class="form-group">
<div class="clearfix"><label class="control-label" style="padding-right: 0px;">Duration</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input">{{ call.duration }}</div>
</div>
<div class="clearfix"><label class="control-label">Duration (ms)</label></div>
<div class="control-value like-disabled-input">{{ call.duration }}</div>
</div>
</div>
<div class="frappe-control">
<div class="form-group">
<div class="clearfix"><label class="control-label" style="padding-right: 0px;">Stack Trace</label></div>
<div class="control-input-wrapper">
<div class="control-value like-disabled-input for-description"><pre>{{ call.stack }}</pre></div>
</div>
<div class="clearfix"><label class="control-label">Stack Trace</label></div>
<div class="control-value like-disabled-input for-description"><pre>{{ call.stack }}</pre></div>
</div>
</div>
</form>
@ -181,7 +99,7 @@
</div>
</div>
</div>
<div class="grid-empty text-center hide">No Data</div>
<div v-if="request.calls.length == 0" class="grid-empty text-center">No Data</div>
</div>
</div>
</div>
@ -198,6 +116,16 @@ export default {
name: "RequestDetail",
data() {
return {
columns: [
{label: "Path", slug: "path", type: "Data"},
{label: "CMD", slug: "cmd", type: "Data"},
{label: "Time", slug: "time", type: "Time"},
{label: "Duration (ms)", slug: "duration", type: "Float"},
{label: "Number of Queries", slug: "queries", type: "Int"},
{label: "Time in Queries (ms)", slug: "time_queries", type: "Float"},
{label: "Request Headers", slug: "headers", type: "Small Text"},
{label: "Form Dict", slug: "form_dict", type: "Small Text"},
],
showing: null,
request: {
calls: [],
@ -208,7 +136,7 @@ export default {
frappe.call({
method: "frappe.recorder.get",
args: {
id: this.$route.params.id
uuid: this.$route.params.id
}
}).then( r => {
this.request = r.message

View file

@ -11,24 +11,20 @@ import sqlparse
import datetime
RECORDER_INTERCEPT_FLAG = "recorder-intercept"
RECORDER_REQUEST_SPARSE_HASH = "recorder-requests-sparse"
RECORDER_REQUEST_HASH = "recorder-requests"
def sql(*args, **kwargs):
# Execute wrapped function as is
# Record arguments as well as return value
# Record start and end time as well
start_time = time.time()
result = frappe.db._sql(*args, **kwargs)
end_time = time.time()
stack = "".join(traceback.format_stack())
# Big hack here
# PyMysql stores exact DB query in cursor._executed
# Assumes that function refers to frappe.db.sql
# __self__ will refer to frappe.db
# Rest is trivial
query = frappe.db._cursor._executed
query = sqlparse.format(query.strip(), keyword_case="upper", reindent=True)
data = {
"query": query,
"stack": stack,
@ -36,35 +32,30 @@ def sql(*args, **kwargs):
"duration": float("{:.3f}".format((end_time - start_time) * 1000)),
}
# Record all calls, Will be later stored in cache
frappe.local._recorder.register(data)
return result
def record():
if frappe.cache().get("recorder-intercept"):
if frappe.cache().get_value(RECORDER_INTERCEPT_FLAG):
frappe.local._recorder = Recorder()
def dump():
if hasattr(frappe.local, "_recorder"):
frappe.local._recorder.dump()
frappe.publish_realtime(event="recorder-dump-event")
class Recorder():
def __init__(self):
self.id = frappe.generate_hash(length=10)
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.request = {
"headers": dict(frappe.local.request.headers),
"data": frappe.local.form_dict,
}
self.headers = dict(frappe.local.request.headers)
self.form_dict = frappe.local.form_dict
_patch()
def register(self, data):
@ -72,7 +63,7 @@ class Recorder():
def dump(self):
request_data = {
"id": self.id,
"uuid": self.uuid,
"path": self.path,
"cmd": self.cmd,
"time": self.time,
@ -81,11 +72,13 @@ class Recorder():
"duration": float("{:0.3f}".format((datetime.datetime.now() - self.time).total_seconds() * 1000)),
"method": self.method,
}
frappe.cache().lpush("recorder-requests", json.dumps(request_data, default=str))
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))
request_data["calls"] = self.calls
request_data["http"] = self.request
frappe.cache().set("recorder-request-{}".format(self.id), json.dumps(request_data, default=str))
request_data["headers"] = self.headers
request_data["form_dict"] = self.form_dict
frappe.cache().hset(RECORDER_REQUEST_HASH, self.uuid, request_data)
def _patch():
@ -93,21 +86,6 @@ def _patch():
frappe.db.sql = sql
def compress(data):
if data:
if isinstance(data[0], dict):
keys = list(data[0].keys())
values = list()
for row in data:
values.append([row.get(key) for key in keys])
else:
keys = [column[0] for column in frappe.db._cursor.description]
values = data
else:
keys, values = [], []
return {"keys": keys, "values": values}
def do_not_record(function):
def wrapper(*args, **kwargs):
if hasattr(frappe.local, "_recorder"):
@ -119,35 +97,34 @@ def do_not_record(function):
@frappe.whitelist()
@do_not_record
def get_status(*args, **kwargs):
if frappe.cache().get("recorder-intercept"):
return {"status": "Active", "color": "green"}
return {"status": "Inactive", "color": "red"}
def status(*args, **kwargs):
return bool(frappe.cache().get_value(RECORDER_INTERCEPT_FLAG))
@frappe.whitelist()
@do_not_record
def set_recorder_state(should_record, *args, **kwargs):
if should_record == "true":
frappe.cache().set("recorder-intercept", 1)
return {"status": "Active", "color": "green"}
else:
frappe.cache().delete("recorder-intercept")
return {"status": "Inactive", "color": "red"}
def start(*args, **kwargs):
frappe.cache().set_value(RECORDER_INTERCEPT_FLAG, 1)
@frappe.whitelist()
@do_not_record
def get(id=None, *args, **kwargs):
if id:
result = json.loads(frappe.cache().get("recorder-request-{}".format(id)).decode())
def stop(*args, **kwargs):
frappe.cache().delete_value(RECORDER_INTERCEPT_FLAG)
@frappe.whitelist()
@do_not_record
def get(uuid=None, *args, **kwargs):
if uuid:
result = frappe.cache().hget(RECORDER_REQUEST_HASH, uuid)
else:
requests = frappe.cache().lrange("recorder-requests", 0, -1)
result = list(map(lambda request: json.loads(request.decode()), requests))
result = frappe.cache().hgetall(RECORDER_REQUEST_SPARSE_HASH).values()
return result
@frappe.whitelist()
@do_not_record
def delete(*args, **kwargs):
frappe.cache().delete_value("recorder-requests")
frappe.cache().delete_value(RECORDER_REQUEST_SPARSE_HASH)
frappe.cache().delete_value(RECORDER_REQUEST_HASH)