diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index b5fe3a1271..414a4333e7 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -72,7 +72,6 @@ "fs-extra": "^11.2.0", "helmet": "^6.0.0", "jose": "^5.0.0", - "json-stringify-safe": "^5.0.1", "knex": "^3.0.0", "lodash": "^4.17.21", "logform": "^2.3.2", @@ -84,6 +83,7 @@ "path-to-regexp": "^6.2.1", "selfsigned": "^2.0.0", "stoppable": "^1.1.0", + "triple-beam": "^1.4.1", "uuid": "^9.0.0", "winston": "^3.2.1", "winston-transport": "^4.5.0" @@ -94,7 +94,6 @@ "@types/compression": "^1.7.0", "@types/fs-extra": "^11.0.0", "@types/http-errors": "^2.0.0", - "@types/json-stringify-safe": "^5.0.3", "@types/minimist": "^1.2.0", "@types/morgan": "^1.9.0", "@types/node-forge": "^1.3.0", diff --git a/packages/backend-app-api/src/logging/WinstonLogger.test.ts b/packages/backend-app-api/src/logging/WinstonLogger.test.ts index 8f60e8dae2..4b608f8e57 100644 --- a/packages/backend-app-api/src/logging/WinstonLogger.test.ts +++ b/packages/backend-app-api/src/logging/WinstonLogger.test.ts @@ -14,12 +14,10 @@ * limitations under the License. */ -import { TransformableInfo } from 'logform'; +import { format } from 'logform'; import { WinstonLogger } from './WinstonLogger'; - -function msg(info: TransformableInfo): TransformableInfo { - return { message: info.message, level: info.level, stack: info.stack }; -} +import Transport from 'winston-transport'; +import { MESSAGE } from 'triple-beam'; describe('WinstonLogger', () => { it('creates a winston logger instance with default options', () => { @@ -33,57 +31,66 @@ describe('WinstonLogger', () => { expect(childLogger).toBeInstanceOf(WinstonLogger); }); - it('redacter should redact and escape regex', () => { - const redacter = WinstonLogger.redacter(); - const log = { - level: 'error', - message: 'hello (world)', - stack: 'hello (world) from this file', - }; - expect(redacter.format.transform(msg(log))).toEqual(msg(log)); - redacter.add(['hello\n']); - expect(redacter.format.transform(msg(log))).toEqual( - msg({ - ...log, - message: '[REDACTED] (world)', - stack: '[REDACTED] (world) from this file', - }), - ); - redacter.add(['(world']); - expect(redacter.format.transform(msg(log))).toEqual( - msg({ - ...log, - message: '[REDACTED] [REDACTED])', - stack: '[REDACTED] [REDACTED]) from this file', + it('should redact and escape regex', () => { + const mockTransport = new Transport({ + log: jest.fn(), + logv: jest.fn(), + }); + + const logger = WinstonLogger.create({ + format: format.json(), + transports: [mockTransport], + }); + + logger.addRedactions(['hello (world']); + + logger.error('hello (world) from this file'); + + expect(mockTransport.log).toHaveBeenCalledWith( + expect.objectContaining({ + [MESSAGE]: JSON.stringify({ + level: 'error', + message: '[REDACTED]) from this file', + }), }), + expect.any(Function), ); }); - it('redacter should redact nested object', () => { - const redacter = WinstonLogger.redacter(); - const log = { - level: 'error', - message: { - nested: 'hello (world) from nested object', - null: null, - nullProto: Object.create(null, { - foo: { value: 'hello foo', enumerable: true }, - }), - }, - }; + it('should redact nested object', () => { + const mockTransport = new Transport({ + log: jest.fn(), + logv: jest.fn(), + }); - redacter.add(['hello']); - expect(redacter.format.transform(msg(log))).toEqual( - msg({ - ...log, - message: { + const logger = WinstonLogger.create({ + format: format.json(), + transports: [mockTransport], + }); + + logger.addRedactions(['hello']); + + logger.error('something went wrong', { + null: null, + nested: 'hello (world) from nested object', + nullProto: Object.create(null, { + foo: { value: 'hello foo', enumerable: true }, + }), + }); + + expect(mockTransport.log).toHaveBeenCalledWith( + expect.objectContaining({ + [MESSAGE]: JSON.stringify({ + level: 'error', + message: 'something went wrong', nested: '[REDACTED] (world) from nested object', null: null, nullProto: { foo: '[REDACTED] foo', }, - }, + }), }), + expect.any(Function), ); }); }); diff --git a/packages/backend-app-api/src/logging/WinstonLogger.ts b/packages/backend-app-api/src/logging/WinstonLogger.ts index 42c181573c..3133fde949 100644 --- a/packages/backend-app-api/src/logging/WinstonLogger.ts +++ b/packages/backend-app-api/src/logging/WinstonLogger.ts @@ -27,7 +27,7 @@ import { transports, transport as Transport, } from 'winston'; -import stringify from 'json-stringify-safe'; +import { MESSAGE } from 'triple-beam'; import { escapeRegExp } from '../lib/escapeRegExp'; /** @@ -62,8 +62,8 @@ export class WinstonLogger implements RootLoggerService { let logger = createLogger({ level: process.env.LOG_LEVEL || options.level || 'info', format: format.combine( - redacter.format, options.format ?? defaultFormatter, + redacter.format, ), transports: options.transports ?? new transports.Console(), }); @@ -86,26 +86,17 @@ export class WinstonLogger implements RootLoggerService { let redactionPattern: RegExp | undefined = undefined; - const replace = (obj: TransformableInfo) => { - if (!redactionPattern) { - return obj; - } - - const stringifiedFields = stringify(obj); - const redacted = JSON.parse( - stringifiedFields.replace(redactionPattern, '[REDACTED]'), - ); - - for (const key in redacted) { - if (obj && Object.hasOwn(obj, key)) { - obj[key] = redacted[key]; - } - } - - return obj; - }; return { - format: format(replace)(), + format: format((obj: TransformableInfo) => { + if (!redactionPattern || !obj) { + return obj; + } + + obj[MESSAGE] = obj[MESSAGE]?.replace?.(redactionPattern, '[REDACTED]'); + obj.message = obj.message.replace?.(redactionPattern, '[REDACTED]'); + + return obj; + })(), add(newRedactions) { let added = 0; for (const redactionToTrim of newRedactions) {