Skip to content

Commit

Permalink
[OGUI-1455] SQL queries logging. (#2675)
Browse files Browse the repository at this point in the history
* log the SQL query performed by the user in a non-prepared statement way
  • Loading branch information
Houwie7000 authored Dec 3, 2024
1 parent faa1d72 commit 205a59e
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 10 deletions.
3 changes: 3 additions & 0 deletions InfoLogger/lib/services/QueryService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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(
Expand Down
56 changes: 56 additions & 0 deletions InfoLogger/lib/utils/preparedStatementParser.js
Original file line number Diff line number Diff line change
@@ -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;
38 changes: 38 additions & 0 deletions InfoLogger/test/lib/services/mocha-preparedStatementParser.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
35 changes: 25 additions & 10 deletions InfoLogger/test/lib/services/mocha-query-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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),
Expand Down

0 comments on commit 205a59e

Please sign in to comment.