From 85b4ba137ca21b6de49a1649644e234fafabfa80 Mon Sep 17 00:00:00 2001 From: Igor Gaponenko Date: Wed, 9 Aug 2023 19:17:07 +0000 Subject: [PATCH] Web Dashboard: added a page for displaying MySQL queries at workers --- src/www/qserv/css/QservWorkerMySQLQueries.css | 27 ++ src/www/qserv/js/Common.js | 2 +- src/www/qserv/js/QservMonitoringDashboard.js | 3 + src/www/qserv/js/QservWorkerMySQLQueries.js | 238 ++++++++++++++++++ 4 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 src/www/qserv/css/QservWorkerMySQLQueries.css create mode 100644 src/www/qserv/js/QservWorkerMySQLQueries.js diff --git a/src/www/qserv/css/QservWorkerMySQLQueries.css b/src/www/qserv/css/QservWorkerMySQLQueries.css new file mode 100644 index 000000000..3e681745b --- /dev/null +++ b/src/www/qserv/css/QservWorkerMySQLQueries.css @@ -0,0 +1,27 @@ +#fwk-worker-mysql-queries-controls label { + font-weight: bold; +} +table#fwk-worker-mysql-queries caption { + caption-side: top; + text-align: right; + padding-top: 0; +} +table#fwk-worker-mysql-queries > thead > tr > th.sticky { + position:sticky; + top:80px; + z-index:2; +} +table#fwk-worker-mysql-queries tbody th, +table#fwk-worker-mysql-queries tbody td { + vertical-align:middle; +} +table#fwk-worker-mysql-queries pre { + padding: 0; + margin: 0; +} +table#fwk-worker-mysql-queries tbody > tr > td.query_toggler:hover { + cursor:pointer; +} +table#fwk-worker-mysql-queries caption.updating { + background-color: #ffeeba; +} diff --git a/src/www/qserv/js/Common.js b/src/www/qserv/js/Common.js index d74d31fb7..6fd085e40 100644 --- a/src/www/qserv/js/Common.js +++ b/src/www/qserv/js/Common.js @@ -3,7 +3,7 @@ define([ function(sqlFormatter) { class Common { - static RestAPIVersion = 23; + static RestAPIVersion = 24; static query2text(query, expanded) { if (expanded) { return sqlFormatter.format(query, Common._sqlFormatterConfig); diff --git a/src/www/qserv/js/QservMonitoringDashboard.js b/src/www/qserv/js/QservMonitoringDashboard.js index 3e94a2cdc..674b62f8e 100644 --- a/src/www/qserv/js/QservMonitoringDashboard.js +++ b/src/www/qserv/js/QservMonitoringDashboard.js @@ -43,6 +43,7 @@ require([ 'qserv/StatusUserQueries', 'qserv/QservCss', 'qserv/QservMySQLConnections', + 'qserv/QservWorkerMySQLQueries', 'qserv/QservWorkerQueries', 'qserv/QservWorkerSchedulers', 'qserv/QservWorkerSchedulerHist', @@ -81,6 +82,7 @@ function(CSSLoader, StatusUserQueries, QservCss, QservMySQLConnections, + QservWorkerMySQLQueries, QservWorkerQueries, QservWorkerSchedulers, QservWorkerSchedulerHist, @@ -169,6 +171,7 @@ function(CSSLoader, { name: 'Workers', apps: [ new QservMySQLConnections('MySQL Connections'), + new QservWorkerMySQLQueries('MySQL Queries'), new QservWorkerQueries('Queries in Worker Queues'), new QservWorkerSchedulers('Schedulers'), new QservWorkerSchedulerHist('Scheduler Histograms'), diff --git a/src/www/qserv/js/QservWorkerMySQLQueries.js b/src/www/qserv/js/QservWorkerMySQLQueries.js new file mode 100644 index 000000000..a5d033351 --- /dev/null +++ b/src/www/qserv/js/QservWorkerMySQLQueries.js @@ -0,0 +1,238 @@ +define([ + 'webfwk/CSSLoader', + 'webfwk/Fwk', + 'webfwk/FwkApplication', + 'qserv/Common', + 'underscore'], + +function(CSSLoader, + Fwk, + FwkApplication, + Common, + _) { + + CSSLoader.load('qserv/css/QservWorkerMySQLQueries.css'); + + class QservWorkerMySQLQueries extends FwkApplication { + + constructor(name) { + super(name); + this._queryId2Expanded = {}; // Store 'true' to allow persistent state for the expanded + // queries between updates. + this._id2query = {}; // Store query text for each identifier. The dictionary gets + // updated at each refresh of the page. + } + fwk_app_on_show() { + console.log('show: ' + this.fwk_app_name); + this.fwk_app_on_update(); + } + fwk_app_on_hide() { + console.log('hide: ' + this.fwk_app_name); + } + fwk_app_on_update() { + if (this.fwk_app_visible) { + this._init(); + if (this._prev_update_sec === undefined) { + this._prev_update_sec = 0; + } + let now_sec = Fwk.now().sec; + if (now_sec - this._prev_update_sec > this._update_interval_sec()) { + this._prev_update_sec = now_sec; + this._init(); + this._load(); + } + } + } + _init() { + if (this._initialized === undefined) this._initialized = false; + if (this._initialized) return; + this._initialized = true; + let html = ` +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+ + + + + + + + + + + + +
IdTimeStateQuery
Loading...
+
+
`; + let cont = this.fwk_app_container.html(html); + cont.find(".form-control-selector").change(() => { + this._load(); + }); + cont.find("button#reset-controls-form").click(() => { + this._set_update_interval_sec(10); + this._load(); + }); + } + _form_control(elem_type, id) { + if (this._form_control_obj === undefined) this._form_control_obj = {}; + if (!_.has(this._form_control_obj, id)) { + this._form_control_obj[id] = this.fwk_app_container.find(elem_type + '#' + id); + } + return this._form_control_obj[id]; + } + _update_interval_sec() { return this._form_control('select', 'update-interval').val(); } + _set_update_interval_sec(val) { this._form_control('select', 'update-interval').val(val); } + _worker() { return this._form_control('select', 'worker').val(); } + _set_worker(val) { this._form_control('select', 'worker').val(val); } + _set_workers(workers) { + const prev_worker = this._worker(); + let html = ''; + for (let i in workers) { + const worker = workers[i]; + const selected = (_.isEmpty(prev_worker) && (i === 0)) || + (!_.isEmpty(prev_worker) && (prev_worker === worker)); + html += ` + `; + } + this._form_control('select', 'worker').html(html); + } + _table() { + if (this._table_obj === undefined) { + this._table_obj = this.fwk_app_container.find('table#fwk-worker-mysql-queries'); + } + return this._table_obj; + } + _load() { + if (this._loading === undefined) this._loading = false; + if (this._loading) return; + this._loading = true; + this._table().children('caption').addClass('updating'); + Fwk.web_service_GET( + "/replication/config", + {version: Common.RestAPIVersion}, + (data) => { + let workers = []; + for (let i in data.config.workers) { + workers.push(data.config.workers[i].name); + } + this._set_workers(workers); + this._load_queries(); + }, + (msg) => { + console.log('request failed', this.fwk_app_name, msg); + this._table().children('caption').html('No Response'); + this._table().children('caption').removeClass('updating'); + this._loading = false; + } + ); + } + _load_queries() { + Fwk.web_service_GET( + "/replication/qserv/worker/db/" + this._worker(), + { timeout_sec: 2, version: Common.RestAPIVersion + }, + (data) => { + if (data.success) { + this._display(data.status.queries); + Fwk.setLastUpdate(this._table().children('caption')); + } else { + console.log('request failed', this.fwk_app_name, data.error); + this._table().children('caption').html('' + data.error + ''); + } + this._table().children('caption').removeClass('updating'); + this._loading = false; + }, + (msg) => { + console.log('request failed', this.fwk_app_name, msg); + this._table().children('caption').html('No Response'); + this._table().children('caption').removeClass('updating'); + this._loading = false; + } + ); + } + _display(queries) { + const queryCopyTitle = "Click to copy the query text to the clipboard."; + const COL_Id = 0, COL_Command = 4, COL_Time = 5, COL_State = 6, COL_Info = 7; + let tbody = this._table().children('tbody'); + if (_.isEmpty(queries.columns)) { + tbody.html(''); + return; + } + this._id2query = {}; + let html = ''; + for (let i in queries.rows) { + let row = queries.rows[i]; + if (row[COL_Command] !== 'Query') continue; + let queryId = row[COL_Id]; + let query = row[COL_Info]; + this._id2query[queryId] = query; + const expanded = (queryId in this._queryId2Expanded) && this._queryId2Expanded[queryId]; + const queryToggleTitle = "Click to toggle query formatting."; + const queryStyle = "color:#4d4dff;"; + html += ` + +
${queryId}
+
${row[COL_Time]}
+
${row[COL_State]}
+ + + +
` + this._query2text(queryId, expanded) + `

+`;
+            }
+            tbody.html(html);
+            let that = this;
+            let copyQueryToClipboard = function(e) {
+                let button = $(e.currentTarget);
+                let queryId = button.parent().parent().attr("id");
+                let query = that._id2query[queryId];
+                navigator.clipboard.writeText(query,
+                    () => {},
+                    () => { alert("Failed to write the query to the clipboard. Please copy the text manually: " + query); }
+                );
+            };
+            let toggleQueryDisplay = function(e) {
+                let td = $(e.currentTarget);
+                let pre = td.find("pre.query");
+                const queryId = td.parent().attr("id");
+                const expanded = !((queryId in that._queryId2Expanded) && that._queryId2Expanded[queryId]);
+                pre.text(that._query2text(queryId, expanded));
+                that._queryId2Expanded[queryId] = expanded;
+            };
+            tbody.find("button.copy-query").click(copyQueryToClipboard);
+            tbody.find("td.query_toggler").click(toggleQueryDisplay);
+        }
+        _query2text(queryId, expanded) {
+            return Common.query2text(this._id2query[queryId], expanded);
+        }
+    }
+    return QservWorkerMySQLQueries;
+});