From 8956832010dca26c04f89ca3fe112bbd8bdfb922 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 | 24 ++ src/www/qserv/js/Common.js | 2 +- src/www/qserv/js/QservMonitoringDashboard.js | 3 + src/www/qserv/js/QservWorkerMySQLQueries.js | 214 ++++++++++++++++++ 4 files changed, 242 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..9cf81833d --- /dev/null +++ b/src/www/qserv/css/QservWorkerMySQLQueries.css @@ -0,0 +1,24 @@ +#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 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..d9dfda489 --- /dev/null +++ b/src/www/qserv/js/QservWorkerMySQLQueries.js @@ -0,0 +1,214 @@ +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); + } + 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 queryInspectTitle = "Click to see detailed info (progress, messages, etc.) on the query."; + const COL_Id = 0, COL_Command = 4, COL_Time = 5, COL_State = 6, COL_Info = 7; + let thead = this._table().children('thead'); + let tbody = this._table().children('tbody'); + if (_.isEmpty(queries.columns)) { + thead.html(''); + tbody.html(''); + return; + } + let html = ''; + for (let i in queries.columns) { + html += `${queries.columns[i]}`; + } + html += ''; + thead.html(html); + html = ''; + for (let i in queries.rows) { + let row = queries.rows[i]; + html = ''; + for (let j in row) { + html += `
${row[j]}
`; + } + html += ''; + } + tbody.html(html); + let displayQuery = function(e) { + let button = $(e.currentTarget); + let queryId = button.parent().parent().attr("id"); + Fwk.find("Status", "Query Inspector").set_query_id(queryId); + Fwk.show("Status", "Query Inspector"); + }; + tbody.find("button.inspect-query").click(displayQuery); + } + } + return QservWorkerMySQLQueries; +});