From 205a59e1ddb5becca1fefba1c2b504d89be6087b Mon Sep 17 00:00:00 2001 From: Jasper H <50981003+Houwie7000@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:00:35 +0100 Subject: [PATCH] [OGUI-1455] SQL queries logging. (#2675) * log the SQL query performed by the user in a non-prepared statement way --- InfoLogger/lib/services/QueryService.js | 3 + .../lib/utils/preparedStatementParser.js | 56 +++++++++++++++++++ .../services/mocha-preparedStatementParser.js | 38 +++++++++++++ .../lib/services/mocha-query-service.test.js | 35 ++++++++---- 4 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 InfoLogger/lib/utils/preparedStatementParser.js create mode 100644 InfoLogger/test/lib/services/mocha-preparedStatementParser.js diff --git a/InfoLogger/lib/services/QueryService.js b/InfoLogger/lib/services/QueryService.js index 0d211ae23..9007265da 100644 --- a/InfoLogger/lib/services/QueryService.js +++ b/InfoLogger/lib/services/QueryService.js @@ -15,6 +15,7 @@ const mariadb = require('mariadb'); const { LogManager } = require('@aliceo2/web-ui'); const { fromSqlToNativeError } = require('../utils/fromSqlToNativeError'); +const { processPreparedSQLStatement } = require('../utils/preparedStatementParser'); class QueryService { /** @@ -82,6 +83,8 @@ class QueryService { const requestRows = `SELECT * FROM \`messages\` ${criteriaString} ORDER BY \`TIMESTAMP\` LIMIT ?;`; const startTime = Date.now(); // ms + this._logger.debugMessage(`SQL to execute: ${processPreparedSQLStatement(requestRows, values, limit)}`); + let rows = []; try { rows = await this._pool.query( diff --git a/InfoLogger/lib/utils/preparedStatementParser.js b/InfoLogger/lib/utils/preparedStatementParser.js new file mode 100644 index 000000000..8113c46fd --- /dev/null +++ b/InfoLogger/lib/utils/preparedStatementParser.js @@ -0,0 +1,56 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * Translate the SQL prepared statement to a regular SQL query. + * This function is to be used for logging purposes ONLY. + * @param {string} requestRows - The prepared SQL statement. + * @param {object} values - Values for the prepared SQL statement. + * @param {number} limit - Configured limit of the sql query results. + * @returns {string} The resulting SQL query as a string. + */ +function processPreparedSQLStatement(requestRows, values, limit) { + let sqlQuery = requestRows; + + const iterator = values.values(); + for (const value of iterator) { + if (Array.isArray(value)) { + sqlQuery = sqlQuery.replace('?', convertArrayToString(value)); + } else { + sqlQuery = sqlQuery.replace('?', `'${value}'`); + } + } + sqlQuery = sqlQuery.replace('?', limit); + + return sqlQuery; +} + +/** + * Helper function that converts arrays to strings with a single quote around the values. + * This function can later be expanded to handle values other than strings in the array. + * @param {Array} array - Array to convert to string. + * @returns {string} a string representation of the input array. + */ +function convertArrayToString(array) { + let processedArray = ''; + array.forEach((v) => { + if (typeof v == 'string') { + processedArray += `'${v}',`; + } + }); + processedArray = processedArray.substring(0, processedArray.length - 1); + return processedArray; +} + +module.exports.processPreparedSQLStatement = processPreparedSQLStatement; diff --git a/InfoLogger/test/lib/services/mocha-preparedStatementParser.js b/InfoLogger/test/lib/services/mocha-preparedStatementParser.js new file mode 100644 index 000000000..e5029e8b7 --- /dev/null +++ b/InfoLogger/test/lib/services/mocha-preparedStatementParser.js @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const assert = require('assert'); +const { processPreparedSQLStatement } = require('../../../lib/utils/preparedStatementParser.js'); + +describe('preparedStatementParser() - test suite', () => { + it('should be able to fill in a prepared statement', async () => { + const requestedRows = 'SELECT * FROM `messages` WHERE `timestamp`>=? AND `timestamp`<=? AND `hostname` = ? ' + + 'AND NOT(`hostname` = ? AND `hostname` IS NOT NULL) AND `severity` IN (?) ORDER BY `TIMESTAMP` LIMIT 10'; + const values = [ + '1563794601.351', + '1563794661.354', + 'test', + 'testEx', + [ + 'D', + 'W', + ], + ]; + const sqlProcessedResult = processPreparedSQLStatement(requestedRows, values, 10); + const expectedSqlResult = "SELECT * FROM `messages` WHERE `timestamp`>='1563794601.351' AND `timestamp`" + + "<='1563794661.354' AND `hostname` = 'test' AND NOT(`hostname` = 'testEx' AND `hostname` IS NOT" + + " NULL) AND `severity` IN ('D','W') ORDER BY `TIMESTAMP` LIMIT 10"; + assert.strictEqual(sqlProcessedResult, expectedSqlResult); + }); +}); diff --git a/InfoLogger/test/lib/services/mocha-query-service.test.js b/InfoLogger/test/lib/services/mocha-query-service.test.js index 21cebb18b..b901cff9e 100644 --- a/InfoLogger/test/lib/services/mocha-query-service.test.js +++ b/InfoLogger/test/lib/services/mocha-query-service.test.js @@ -18,7 +18,7 @@ const config = require('../../../config-default.js'); const { QueryService } = require('../../../lib/services/QueryService.js'); const { UnauthorizedAccessError, TimeoutError } = require('@aliceo2/web-ui'); -describe(`'QueryService' test suite`, () => { +describe('\'QueryService\' test suite', () => { const filters = { timestamp: { since: -5, @@ -73,7 +73,7 @@ describe(`'QueryService' test suite`, () => { }; const emptySqlDataSource = new QueryService(undefined, {}); - describe(`'checkConnection()' - test suite`, () => { + describe('\'checkConnection()\' - test suite', () => { it('should reject with error when simple query fails', async () => { const sqlDataSource = new QueryService(config.mysql); sqlDataSource._isAvailable = true; @@ -230,9 +230,24 @@ describe(`'QueryService' test suite`, () => { }; assert.deepStrictEqual(result, expectedResult); }); + + it('should log every executed sql query as debug', async () => { + const sqlDataSource = new QueryService(config.mysql); + sqlDataSource._logger = { + debugMessage: sinon.stub(), + }; + sqlDataSource._pool = { + query: sinon.stub().resolves([{ hostname: 'test', severity: 'W' }]), + }; + await sqlDataSource.queryFromFilters(realFilters, { limit: 10 }); + const completeSqlQuery = "SELECT * FROM `messages` WHERE `timestamp`>='1563794601.351' AND" + + " `timestamp`<='1563794661.354' AND `hostname` = 'test' AND NOT(`hostname` = 'testEx' AND" + + " `hostname` IS NOT NULL) AND `severity` IN ('D','W') ORDER BY `TIMESTAMP` LIMIT 10;"; + assert.strictEqual(sqlDataSource._logger.debugMessage.calledWith(`SQL to execute: ${completeSqlQuery}`), true); + }); }); - describe('queryGroupCountLogsBySeverity() - test suite', ()=> { + describe('queryGroupCountLogsBySeverity() - test suite', () => { it(`should successfully return stats when queried for all known severities even if none is some are not returned by data service`, async () => { const dataService = new QueryService(config.mysql); @@ -255,13 +270,13 @@ describe(`'QueryService' test suite`, () => { it('should throw error if data service throws SQL', async () => { const dataService = new QueryService(config.mysql); dataService._pool = - { - query: sinon.stub().rejects({ - code: 'ER_ACCESS_DENIED_ERROR', - errno: 1045, - sqlMessage: 'Access denied', - }), - }; + { + query: sinon.stub().rejects({ + code: 'ER_ACCESS_DENIED_ERROR', + errno: 1045, + sqlMessage: 'Access denied', + }), + }; await assert.rejects( dataService.queryGroupCountLogsBySeverity(51234),