From e9b3e1e96c50a7feb0dc1182ff0bff39f4147745 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 26 Oct 2026 12:45:05 +0100 Subject: [PATCH 1/4] Add new general middleware log function --- docs/backend-architecture.md | 17 ++++ lib/server/Loggers/FilterLogger.js | 63 ------------ lib/server/WebUiServer.js | 4 + .../InfoLoggerListener.middleware.js | 23 ----- lib/server/middleware/accessLog.middleware.js | 74 +++++++++++++++ lib/server/middleware/index.js | 2 + .../middleware/accessLog.middleware.test.js | 95 +++++++++++++++++++ test/lib/server/middleware/index.js | 2 + 8 files changed, 194 insertions(+), 86 deletions(-) delete mode 100644 lib/server/Loggers/FilterLogger.js delete mode 100644 lib/server/middleware/InfoLoggerListener.middleware.js create mode 100644 lib/server/middleware/accessLog.middleware.js create mode 100644 test/lib/server/middleware/accessLog.middleware.test.js diff --git a/docs/backend-architecture.md b/docs/backend-architecture.md index 73390eaf3a..5ce829a69c 100644 --- a/docs/backend-architecture.md +++ b/docs/backend-architecture.md @@ -35,3 +35,20 @@ Because the services might have inter-dependencies that can lead to cyclic depen ### Error management Currently, `usecases` returns `HTTP` oriented objects when an exception occurs, with `HTTP` error code and message. This is not viable anymore because those errors makes no sense in `gRPC`. To solve these issues, if an error occur in the service layer, an instance of a specific instance must be thrown and it must be converted to API-specific error in controllers or at an even higher level (for example `AuthenticatedOnly` error will be converted to error code `16` in `gRPC` and `401` in `HTTP`). + +### HTTP API access log + +Every request handled by the `HTTP` API (`/api`, not the static files) is logged by `accessLogMiddleware` once its response has been sent. The entry is a single-line `JSON`, logged by the `API` logger **to the local log only** (it is not forwarded to InfoLogger), so that it can be parsed and aggregated in OpenSearch: + +```json +{"type":"api_request","method":"GET","route":"/api/runs","status":200,"durationMs":42,"userId":7,"bytes":18234,"query":{"filter":{"runNumbers":"1,2"},"page":{"limit":"50"}}} +``` + +- `route`: the route pattern (for example `/api/runs/:runNumber`) and not the raw url, so that requests can be grouped per endpoint. When no route matched, it is the requested path +- `durationMs`: time between the reception of the request by the middleware and the end of the response +- `userId`: the id of the user, `null` if the request is not authenticated +- `bytes`: the `content-length` of the response, `null` if not provided +- `query`: the parsed query string (filters, pagination, sorting...), omitted when empty. The body is never logged +- `aborted`: `true` only if the connection was closed before the response was completely sent + +Nothing is logged in test mode. diff --git a/lib/server/Loggers/FilterLogger.js b/lib/server/Loggers/FilterLogger.js deleted file mode 100644 index 8a76ad208e..0000000000 --- a/lib/server/Loggers/FilterLogger.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * 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 { LogManager, LogLevel } = require('@aliceo2/web-ui'); -const { isInTestMode } = require('../../utilities/env-utils'); - -/** - * Logger dedicated to filter-related endpoint access events. - */ -class FilterLogger { - /** - * Creates an instance of FilterLogger. - * - * @param {boolean} [silent] if true, no message will be logged (defaults to true in test mode) - */ - constructor(silent = isInTestMode()) { - LogManager.configure({ infologger: true }); - this._logger = LogManager.getLogger('FILTERING'); - this._logLevel = LogLevel.OPERATIONS; - this._silent = silent; - } - - /** - * Logs an informational message about endpoint access and applied filters. - * - * @param {object} request the request received at any given endpoint. - * @param {string} request.path the endpoint that was accessed. - * @param {object} [request.session={}] the session of the user accessing the endpoint. - * @param {string|number} request.session.id identifier of the user accessing the endpoint. - * @param {object} [request.query={}] the request query, with the applied filters under `filter`. - * @returns {void} - */ - infoMessage({ path, session: { id } = {}, query = {} }) { - if (this._silent) { - return; - } - - const filters = query.filter ?? {}; - - let message = `Endpoint ${path} was accessed by `; - message += id ? `user ${id} ` : 'an unauthenticated user '; - - if (!Object.keys(filters).length) { - message += 'without filters'; - } else { - message += 'with the following filters:\n'; - message += JSON.stringify(filters); - } - - this._logger.infoMessage(message, { level: this._logLevel }); - } -} - -module.exports = new FilterLogger(); diff --git a/lib/server/WebUiServer.js b/lib/server/WebUiServer.js index a6c7661314..25751b710b 100644 --- a/lib/server/WebUiServer.js +++ b/lib/server/WebUiServer.js @@ -19,6 +19,7 @@ const { isInTestMode, isInDevMode } = require('../utilities/env-utils'); const { createOrUpdateUser } = require('./services/user/createOrUpdateUser.js'); const { LogManager } = require('@aliceo2/web-ui'); const { BkpRoles } = require('../domain/enums/BkpRoles.js'); +const { accessLogMiddleware } = require('./middleware/accessLog.middleware.js'); /** * WebUI implementation of the Server. @@ -42,6 +43,9 @@ class WebUiServer { autoListen: false, }, JwtConfig, OpenIdConfig.isValid() ? OpenIdConfig : null); + // Logs every API request once handled (not the statics) and after the jwt verification has been done + [this.http.router, this.http.routerPublic].forEach((router) => router.use(accessLogMiddleware())); + [this.http.router, this.http.routerPublic, this.http.routerStatics].forEach((router) => { router.use(async (request, response, next) => { if (isInTestMode()) { diff --git a/lib/server/middleware/InfoLoggerListener.middleware.js b/lib/server/middleware/InfoLoggerListener.middleware.js deleted file mode 100644 index 858b8a805d..0000000000 --- a/lib/server/middleware/InfoLoggerListener.middleware.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * 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. - */ - -/** - * Logger based middleware generator - * - * @param {Class} logger class that exposes an infoMessage function that recceives the request and then sends specific data to InfoLogger - * @return {(function(*, *, *): void)} the infoLoggerListener middleware - */ -exports.infoLoggerListenerMiddleware = (logger) => (request, _response, next) => { - logger.infoMessage(request); - next(); -}; diff --git a/lib/server/middleware/accessLog.middleware.js b/lib/server/middleware/accessLog.middleware.js new file mode 100644 index 0000000000..8750ccb406 --- /dev/null +++ b/lib/server/middleware/accessLog.middleware.js @@ -0,0 +1,74 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * 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 { LogManager } = require('@aliceo2/web-ui'); +const { isInTestMode } = require('../../utilities/env-utils'); + +const NANOSECONDS_PER_MILLISECOND = 1000000n; + +/** + * Express middleware logging one single-line JSON entry for each handled API request, once its response has been sent. + * + * The entry is sent to the local logger only (no log level is provided, so it is NOT forwarded to InfoLogger). It contains the method, the route + * pattern (not the raw url, to keep it aggregable), the status, the duration, the id of the user, the size of the response and the query + * + * @param {object} [configuration] the middleware configuration + * @param {{infoMessage: function(string): void}} [configuration.logger] the logger to use + * @param {boolean} [configuration.silent] if true, nothing is logged (defaults to true in test mode) + * @return {function(object, object, function): void} the access log middleware + */ +exports.accessLogMiddleware = ({ logger = LogManager.getLogger('API'), silent = isInTestMode() } = {}) => (request, response, next) => { + if (silent) { + next(); + return; + } + + const startTime = process.hrtime.bigint(); + // The base url (i.e. the router mount path) must be read now, as express restores it when leaving the router + const { baseUrl } = request; + // Needed as to not duplicate logging in case of both close and finish events triggering the log function + let logged = false; + + const log = () => { + if (logged) { + return; + } + logged = true; + + const { method, query, session, route, originalUrl } = request; + const contentLength = Number(response.getHeader('content-length')); + + const entry = { + type: 'api_request', + method, + route: route ? `${baseUrl}${route.path}` : originalUrl.split('?')[0], + status: response.statusCode, + durationMs: Number((process.hrtime.bigint() - startTime) / NANOSECONDS_PER_MILLISECOND), + userId: session?.id ?? null, + bytes: Number.isNaN(contentLength) ? null : contentLength, + }; + + if (!response.writableFinished) { + entry.aborted = true; + } + if (query && Object.keys(query).length > 0) { + entry.query = query; + } + + logger.infoMessage(JSON.stringify(entry)); + }; + + response.once('finish', log); + response.once('close', log); + next(); +}; diff --git a/lib/server/middleware/index.js b/lib/server/middleware/index.js index c77e211ebf..5b05cf62ba 100644 --- a/lib/server/middleware/index.js +++ b/lib/server/middleware/index.js @@ -12,7 +12,9 @@ */ const multerMiddleware = require('./multer.middleware'); +const { accessLogMiddleware } = require('./accessLog.middleware.js'); module.exports = { multerMiddleware, + accessLogMiddleware, }; diff --git a/test/lib/server/middleware/accessLog.middleware.test.js b/test/lib/server/middleware/accessLog.middleware.test.js new file mode 100644 index 0000000000..d289a4a198 --- /dev/null +++ b/test/lib/server/middleware/accessLog.middleware.test.js @@ -0,0 +1,95 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * 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 { EventEmitter } = require('events'); +const sinon = require('sinon'); +const { expect } = require('chai'); +const { accessLogMiddleware } = require('../../../../lib/server/middleware/accessLog.middleware.js'); + +/** + * Run a request through the access log middleware + * + * @param {object} request the request fields + * @param {object} [responseFields] the response fields + * @param {object} [configuration] the middleware configuration + * @return {{logger: object, next: Function, response: object}} the objects used to run the middleware + */ +const handle = (request, responseFields = {}, configuration = {}) => { + const logger = { infoMessage: sinon.fake() }; + const next = sinon.fake(); + const response = Object.assign(new EventEmitter(), { + statusCode: 200, + writableFinished: true, + getHeader: (name) => responseFields.headers?.[name], + }, responseFields); + + accessLogMiddleware({ logger, silent: false, ...configuration })(request, response, next); + return { logger, next, response }; +}; + +module.exports = () => { + it('should log a single line JSON entry with route pattern, status, duration, user, size and query once the response is sent', () => { + const query = { filter: { runNumbers: '1,2' }, page: { limit: '10' } }; + const { logger, next, response } = handle( + { method: 'GET', baseUrl: '/api', originalUrl: '/api/runs/12?a=1', route: { path: '/runs/:runNumber' }, session: { id: 7 }, query }, + { headers: { 'content-length': '123' }, statusCode: 201 }, + ); + + expect(next.calledOnce).to.be.true; + expect(logger.infoMessage.called).to.be.false; + + response.emit('finish'); + response.emit('close'); + + expect(logger.infoMessage.calledOnce).to.be.true; + const [message] = logger.infoMessage.firstCall.args; + expect(message).to.not.include('\n'); + + const { durationMs, ...entry } = JSON.parse(message); + expect(durationMs).to.be.a('number').and.to.be.at.least(0); + expect(entry).to.eql({ + type: 'api_request', + method: 'GET', + route: '/api/runs/:runNumber', + status: 201, + userId: 7, + bytes: 123, + query, + }); + }); + + it('should log null user and size, no query, and the path when the request is anonymous and no route matched', () => { + const { logger, response } = handle({ method: 'GET', baseUrl: '/api', originalUrl: '/api/unknown?x=1', query: {} }); + response.emit('finish'); + + const entry = JSON.parse(logger.infoMessage.firstCall.args[0]); + expect(entry).to.include({ route: '/api/unknown', userId: null, bytes: null }); + expect(entry).to.not.have.property('query'); + expect(entry).to.not.have.property('aborted'); + }); + + it('should flag as aborted a request whose connection is closed before the response is sent', () => { + const { logger, response } = handle({ method: 'GET', baseUrl: '/api', originalUrl: '/api/runs', route: { path: '/runs' } }, { writableFinished: false }); + response.emit('close'); + + expect(JSON.parse(logger.infoMessage.firstCall.args[0])).to.include({ aborted: true }); + }); + + it('should not log anything when silent', () => { + const { logger, next, response } = handle({ method: 'GET', baseUrl: '/api', originalUrl: '/api/runs' }, {}, { silent: true }); + response.emit('finish'); + + expect(next.calledOnce).to.be.true; + expect(logger.infoMessage.called).to.be.false; + }); +}; diff --git a/test/lib/server/middleware/index.js b/test/lib/server/middleware/index.js index 088ad5f790..913307a8de 100644 --- a/test/lib/server/middleware/index.js +++ b/test/lib/server/middleware/index.js @@ -12,7 +12,9 @@ */ const RbacMiddlewareTest = require('./rbac.middleware.test.js'); +const AccessLogMiddlewareTest = require('./accessLog.middleware.test.js'); module.exports = () => { describe('RBAC', RbacMiddlewareTest); + describe('Access log', AccessLogMiddlewareTest); }; From 0b06c8f309ce9daf7a701a8dbc5b3dca7399eeed Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 26 Oct 2026 12:45:36 +0100 Subject: [PATCH 2/4] Remove usage of old FilterLogger and middleware --- lib/server/routers/dataPasses.router.js | 4 +--- lib/server/routers/environments.router.js | 4 +--- lib/server/routers/lhcFills.router.js | 4 +--- lib/server/routers/lhcPeriodsStatistics.router.js | 4 +--- lib/server/routers/logs.router.js | 4 +--- lib/server/routers/qcFlag.router.js | 4 +--- lib/server/routers/runs.router.js | 4 +--- lib/server/routers/simulationPasses.router.js | 4 +--- 8 files changed, 8 insertions(+), 24 deletions(-) diff --git a/lib/server/routers/dataPasses.router.js b/lib/server/routers/dataPasses.router.js index 89e0e7cd58..34f97d2547 100644 --- a/lib/server/routers/dataPasses.router.js +++ b/lib/server/routers/dataPasses.router.js @@ -14,13 +14,11 @@ const { DataPassesController } = require('../controllers/dataPasses.controller.js'); const { rbacMiddleware } = require('../middleware/rbac.middleware.js'); const { BkpRoles } = require('../../domain/enums/BkpRoles.js'); -const FilterLogger = require('../Loggers/FilterLogger.js'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware.js'); exports.dataPassesRouter = { path: '/dataPasses', method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), DataPassesController.listDataPassesHandler], + controller: DataPassesController.listDataPassesHandler, children: [ { diff --git a/lib/server/routers/environments.router.js b/lib/server/routers/environments.router.js index 1c0769bc68..56d4066e73 100644 --- a/lib/server/routers/environments.router.js +++ b/lib/server/routers/environments.router.js @@ -12,13 +12,11 @@ */ const { EnvironmentsController } = require('../controllers'); -const FilterLogger = require('../Loggers/FilterLogger'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware'); module.exports = { method: 'get', path: '/environments', - controller: [infoLoggerListenerMiddleware(FilterLogger), EnvironmentsController.getAllEnvironments], + controller: EnvironmentsController.getAllEnvironments, children: [ { method: 'post', diff --git a/lib/server/routers/lhcFills.router.js b/lib/server/routers/lhcFills.router.js index 73bc50433e..2b33cedb8c 100644 --- a/lib/server/routers/lhcFills.router.js +++ b/lib/server/routers/lhcFills.router.js @@ -12,15 +12,13 @@ */ const { LhcFillsController } = require('../controllers'); -const FilterLogger = require('../Loggers/FilterLogger'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware'); module.exports = { path: '/lhcFills', children: [ { method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), LhcFillsController.listLhcFills], + controller: LhcFillsController.listLhcFills, }, { method: 'post', diff --git a/lib/server/routers/lhcPeriodsStatistics.router.js b/lib/server/routers/lhcPeriodsStatistics.router.js index feb0f4b058..073288903c 100644 --- a/lib/server/routers/lhcPeriodsStatistics.router.js +++ b/lib/server/routers/lhcPeriodsStatistics.router.js @@ -12,15 +12,13 @@ */ const { LhcPeriodStatisticsController } = require('../controllers/lhcPeriodStatistics.controller.js'); -const FilterLogger = require('../Loggers/FilterLogger.js'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware.js'); exports.lhcPeriodsRouter = { path: '/lhcPeriodsStatistics', children: [ { method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), LhcPeriodStatisticsController.listLhcPeriodStatisticsHandler], + controller: LhcPeriodStatisticsController.listLhcPeriodStatisticsHandler, }, { method: 'get', diff --git a/lib/server/routers/logs.router.js b/lib/server/routers/logs.router.js index 9c115f855e..d4381dc170 100644 --- a/lib/server/routers/logs.router.js +++ b/lib/server/routers/logs.router.js @@ -12,14 +12,12 @@ */ const { LogsController } = require('../controllers'); -const FilterLogger = require('../Loggers/FilterLogger'); const { multerMiddleware: { attachmentMiddleware } } = require('../middleware'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware'); module.exports = { method: 'get', path: '/logs', - controller: [infoLoggerListenerMiddleware(FilterLogger), LogsController.listLogs], + controller: LogsController.listLogs, children: [ { method: 'get', diff --git a/lib/server/routers/qcFlag.router.js b/lib/server/routers/qcFlag.router.js index f97a565c86..569a6802ec 100644 --- a/lib/server/routers/qcFlag.router.js +++ b/lib/server/routers/qcFlag.router.js @@ -13,8 +13,6 @@ const { BkpRoles } = require('../../domain/enums/BkpRoles.js'); const { QcFlagController } = require('../controllers/qcFlag.controller.js'); -const FilterLogger = require('../Loggers/FilterLogger.js'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware.js'); const { rbacMiddleware } = require('../middleware/rbac.middleware.js'); exports.qcFlagsRouter = { @@ -23,7 +21,7 @@ exports.qcFlagsRouter = { { path: 'gaq', method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), QcFlagController.getGaqQcFlagsHandler], + controller: QcFlagController.getGaqQcFlagsHandler, }, { path: 'summary', diff --git a/lib/server/routers/runs.router.js b/lib/server/routers/runs.router.js index b0db9f58bd..3fcdcd58b2 100644 --- a/lib/server/routers/runs.router.js +++ b/lib/server/routers/runs.router.js @@ -12,8 +12,6 @@ */ const { RunsController } = require('../controllers'); -const FilterLogger = require('../Loggers/FilterLogger'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware'); module.exports = { children: [ @@ -37,7 +35,7 @@ module.exports = { }, { method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), RunsController.listRuns], + controller: RunsController.listRuns, }, { method: 'get', diff --git a/lib/server/routers/simulationPasses.router.js b/lib/server/routers/simulationPasses.router.js index ccd7ba18c1..057a914fbc 100644 --- a/lib/server/routers/simulationPasses.router.js +++ b/lib/server/routers/simulationPasses.router.js @@ -12,8 +12,6 @@ */ const { SimulationPassesController } = require('../controllers/simulationPasses.controller.js'); -const FilterLogger = require('../Loggers/FilterLogger.js'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware.js'); exports.simulationPassesRouter = { path: '/simulationPasses', @@ -25,7 +23,7 @@ exports.simulationPassesRouter = { }, { method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), SimulationPassesController.listSimulationPassesHandler], + controller: SimulationPassesController.listSimulationPassesHandler, }, ], }; From 3ec799656f8bef296c25cf29913b04703ff00b66 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 26 Oct 2026 12:49:37 +0100 Subject: [PATCH 3/4] Remove token from being logged --- docs/backend-architecture.md | 2 +- lib/server/middleware/accessLog.middleware.js | 7 +++-- .../middleware/accessLog.middleware.test.js | 28 +++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/backend-architecture.md b/docs/backend-architecture.md index 5ce829a69c..f7df1f1dd0 100644 --- a/docs/backend-architecture.md +++ b/docs/backend-architecture.md @@ -48,7 +48,7 @@ Every request handled by the `HTTP` API (`/api`, not the static files) is logged - `durationMs`: time between the reception of the request by the middleware and the end of the response - `userId`: the id of the user, `null` if the request is not authenticated - `bytes`: the `content-length` of the response, `null` if not provided -- `query`: the parsed query string (filters, pagination, sorting...), omitted when empty. The body is never logged +- `query`: the parsed query string (filters, pagination, sorting...), omitted when empty. The `token` parameter is removed and the body is never logged - `aborted`: `true` only if the connection was closed before the response was completely sent Nothing is logged in test mode. diff --git a/lib/server/middleware/accessLog.middleware.js b/lib/server/middleware/accessLog.middleware.js index 8750ccb406..e10a7b34fe 100644 --- a/lib/server/middleware/accessLog.middleware.js +++ b/lib/server/middleware/accessLog.middleware.js @@ -61,8 +61,11 @@ exports.accessLogMiddleware = ({ logger = LogManager.getLogger('API'), silent = if (!response.writableFinished) { entry.aborted = true; } - if (query && Object.keys(query).length > 0) { - entry.query = query; + // The token is a credential and must never be logged + const loggableQuery = { ...query }; + delete loggableQuery.token; + if (Object.keys(loggableQuery).length > 0) { + entry.query = loggableQuery; } logger.infoMessage(JSON.stringify(entry)); diff --git a/test/lib/server/middleware/accessLog.middleware.test.js b/test/lib/server/middleware/accessLog.middleware.test.js index d289a4a198..1458c3dbfa 100644 --- a/test/lib/server/middleware/accessLog.middleware.test.js +++ b/test/lib/server/middleware/accessLog.middleware.test.js @@ -78,6 +78,34 @@ module.exports = () => { expect(entry).to.not.have.property('aborted'); }); + it('should never log the token query parameter', () => { + const { logger, response } = handle({ + method: 'GET', + baseUrl: '/api', + originalUrl: '/api/runs?token=secret&page[limit]=5', + route: { path: '/runs' }, + query: { token: 'secret', page: { limit: '5' } }, + }); + response.emit('finish'); + + const [message] = logger.infoMessage.firstCall.args; + expect(message).to.not.include('secret'); + expect(JSON.parse(message).query).to.eql({ page: { limit: '5' } }); + }); + + it('should not log a query containing only the token parameter', () => { + const { logger, response } = handle({ + method: 'GET', + baseUrl: '/api', + originalUrl: '/api/runs?token=secret', + route: { path: '/runs' }, + query: { token: 'secret' }, + }); + response.emit('finish'); + + expect(JSON.parse(logger.infoMessage.firstCall.args[0])).to.not.have.property('query'); + }); + it('should flag as aborted a request whose connection is closed before the response is sent', () => { const { logger, response } = handle({ method: 'GET', baseUrl: '/api', originalUrl: '/api/runs', route: { path: '/runs' } }, { writableFinished: false }); response.emit('close'); From b7ca3ac7e689fe413b3042f65e2b573bfc9188a1 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 26 Oct 2026 14:09:14 +0100 Subject: [PATCH 4/4] Register logging only to once router to avoid duplication --- lib/server/WebUiServer.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/server/WebUiServer.js b/lib/server/WebUiServer.js index 25751b710b..442d23972d 100644 --- a/lib/server/WebUiServer.js +++ b/lib/server/WebUiServer.js @@ -43,8 +43,11 @@ class WebUiServer { autoListen: false, }, JwtConfig, OpenIdConfig.isValid() ? OpenIdConfig : null); - // Logs every API request once handled (not the statics) and after the jwt verification has been done - [this.http.router, this.http.routerPublic].forEach((router) => router.use(accessLogMiddleware())); + /** + * Logs every API request once handled, not the statics + * (public router only: it is mounted first and registering both would log twice) + */ + this.http.routerPublic.use(accessLogMiddleware()); [this.http.router, this.http.routerPublic, this.http.routerStatics].forEach((router) => { router.use(async (request, response, next) => {