const process = require('process'); const { test } = require('tap'); const debug = require('debug'); const fs = require('fs'); const sandbox = require('@log4js-node/sandboxed-module'); const log4js = require('../../lib/log4js'); const osDelay = process.platform === 'win32' ? 400 : 200; const removeFiles = async (filenames) => { if (!Array.isArray(filenames)) filenames = [filenames]; const promises = filenames.map((filename) => fs.promises.unlink(filename)); await Promise.allSettled(promises); }; test('multiFile appender', (batch) => { batch.test( 'should write to multiple files based on the loggingEvent property', (t) => { t.teardown(async () => { await removeFiles(['logs/A.log', 'logs/B.log']); }); log4js.configure({ appenders: { multi: { type: 'multiFile', base: 'logs/', property: 'categoryName', extension: '.log', }, }, categories: { default: { appenders: ['multi'], level: 'info' } }, }); const loggerA = log4js.getLogger('A'); const loggerB = log4js.getLogger('B'); loggerA.info('I am in logger A'); loggerB.info('I am in logger B'); log4js.shutdown(() => { t.match(fs.readFileSync('logs/A.log', 'utf-8'), 'I am in logger A'); t.match(fs.readFileSync('logs/B.log', 'utf-8'), 'I am in logger B'); t.end(); }); } ); batch.test( 'should write to multiple files based on loggingEvent.context properties', (t) => { t.teardown(async () => { await removeFiles(['logs/C.log', 'logs/D.log']); }); log4js.configure({ appenders: { multi: { type: 'multiFile', base: 'logs/', property: 'label', extension: '.log', }, }, categories: { default: { appenders: ['multi'], level: 'info' } }, }); const loggerC = log4js.getLogger('cheese'); const loggerD = log4js.getLogger('biscuits'); loggerC.addContext('label', 'C'); loggerD.addContext('label', 'D'); loggerC.info('I am in logger C'); loggerD.info('I am in logger D'); log4js.shutdown(() => { t.match(fs.readFileSync('logs/C.log', 'utf-8'), 'I am in logger C'); t.match(fs.readFileSync('logs/D.log', 'utf-8'), 'I am in logger D'); t.end(); }); } ); batch.test('should close file after timeout', (t) => { /* checking that the file is closed after a timeout is done by looking at the debug logs since detecting file locks with node.js is platform specific. */ const debugWasEnabled = debug.enabled('log4js:multiFile'); const debugLogs = []; const originalWrite = process.stderr.write; process.stderr.write = (string, encoding, fd) => { debugLogs.push(string); if (debugWasEnabled) { originalWrite.apply(process.stderr, [string, encoding, fd]); } }; const originalNamespace = debug.disable(); debug.enable(`${originalNamespace}, log4js:multiFile`); t.teardown(async () => { await new Promise((resolve) => { log4js.shutdown(resolve); }); await removeFiles('logs/C.log'); process.stderr.write = originalWrite; debug.enable(originalNamespace); }); const timeoutMs = 50; log4js.configure({ appenders: { multi: { type: 'multiFile', base: 'logs/', property: 'label', extension: '.log', timeout: timeoutMs, }, }, categories: { default: { appenders: ['multi'], level: 'info' } }, }); const loggerC = log4js.getLogger('cheese'); loggerC.addContext('label', 'C'); loggerC.info('I am in logger C'); setTimeout(() => { t.match( debugLogs[debugLogs.length - 1], `C not used for > ${timeoutMs} ms => close`, '(timeout1) should have closed' ); t.end(); }, timeoutMs * 1 + osDelay); }); batch.test('should close file safely after timeout', (t) => { const error = new Error('fileAppender shutdown error'); const sandboxedLog4js = sandbox.require('../../lib/log4js', { requires: { './appenders/file': { configure(config, layouts) { const fileAppender = require('../../lib/appenders/file').configure( config, layouts ); const originalShutdown = fileAppender.shutdown; fileAppender.shutdown = function (complete) { const onCallback = function () { complete(error); }; originalShutdown(onCallback); }; return fileAppender; }, }, debug, }, }); /* checking that the file is closed after a timeout is done by looking at the debug logs since detecting file locks with node.js is platform specific. */ const debugWasEnabled = debug.enabled('log4js:multiFile'); const debugLogs = []; const originalWrite = process.stderr.write; process.stderr.write = (string, encoding, fd) => { debugLogs.push(string); if (debugWasEnabled) { originalWrite.apply(process.stderr, [string, encoding, fd]); } }; const originalNamespace = debug.disable(); debug.enable(`${originalNamespace}, log4js:multiFile`); t.teardown(async () => { await new Promise((resolve) => { sandboxedLog4js.shutdown(resolve); }); await removeFiles('logs/C.log'); process.stderr.write = originalWrite; debug.enable(originalNamespace); }); const timeoutMs = 50; sandboxedLog4js.configure({ appenders: { multi: { type: 'multiFile', base: 'logs/', property: 'label', extension: '.log', timeout: timeoutMs, }, }, categories: { default: { appenders: ['multi'], level: 'info' } }, }); const loggerC = sandboxedLog4js.getLogger('cheese'); loggerC.addContext('label', 'C'); loggerC.info('I am in logger C'); setTimeout(() => { t.match( debugLogs[debugLogs.length - 2], `C not used for > ${timeoutMs} ms => close`, '(timeout1) should have closed' ); t.match( debugLogs[debugLogs.length - 1], `ignore error on file shutdown: ${error.message}`, 'safely shutdown' ); t.end(); }, timeoutMs * 1 + osDelay); }); batch.test('should close file after extended timeout', (t) => { /* checking that the file is closed after a timeout is done by looking at the debug logs since detecting file locks with node.js is platform specific. */ const debugWasEnabled = debug.enabled('log4js:multiFile'); const debugLogs = []; const originalWrite = process.stderr.write; process.stderr.write = (string, encoding, fd) => { debugLogs.push(string); if (debugWasEnabled) { originalWrite.apply(process.stderr, [string, encoding, fd]); } }; const originalNamespace = debug.disable(); debug.enable(`${originalNamespace}, log4js:multiFile`); t.teardown(async () => { await new Promise((resolve) => { log4js.shutdown(resolve); }); await removeFiles('logs/D.log'); process.stderr.write = originalWrite; debug.enable(originalNamespace); }); const timeoutMs = 1000; log4js.configure({ appenders: { multi: { type: 'multiFile', base: 'logs/', property: 'label', extension: '.log', timeout: timeoutMs, }, }, categories: { default: { appenders: ['multi'], level: 'info' } }, }); const loggerD = log4js.getLogger('cheese'); loggerD.addContext('label', 'D'); loggerD.info('I am in logger D'); setTimeout(() => { loggerD.info('extending activity!'); t.match( debugLogs[debugLogs.length - 1], 'D extending activity', 'should have extended' ); }, timeoutMs / 2); setTimeout(() => { t.notOk( debugLogs.some( (s) => s.indexOf(`D not used for > ${timeoutMs} ms => close`) !== -1 ), '(timeout1) should not have closed' ); }, timeoutMs * 1 + osDelay); setTimeout(() => { t.match( debugLogs[debugLogs.length - 1], `D not used for > ${timeoutMs} ms => close`, '(timeout2) should have closed' ); t.end(); }, timeoutMs * 2 + osDelay); }); batch.test('should clear interval for active timers on shutdown', (t) => { /* checking that the file is closed after a timeout is done by looking at the debug logs since detecting file locks with node.js is platform specific. */ const debugWasEnabled = debug.enabled('log4js:multiFile'); const debugLogs = []; const originalWrite = process.stderr.write; process.stderr.write = (string, encoding, fd) => { debugLogs.push(string); if (debugWasEnabled) { originalWrite.apply(process.stderr, [string, encoding, fd]); } }; const originalNamespace = debug.disable(); debug.enable(`${originalNamespace}, log4js:multiFile`); t.teardown(async () => { await removeFiles('logs/D.log'); process.stderr.write = originalWrite; debug.enable(originalNamespace); }); const timeoutMs = 100; log4js.configure({ appenders: { multi: { type: 'multiFile', base: 'logs/', property: 'label', extension: '.log', timeout: timeoutMs, }, }, categories: { default: { appenders: ['multi'], level: 'info' } }, }); const loggerD = log4js.getLogger('cheese'); loggerD.addContext('label', 'D'); loggerD.info('I am in logger D'); log4js.shutdown(() => { t.notOk( debugLogs.some( (s) => s.indexOf(`D not used for > ${timeoutMs} ms => close`) !== -1 ), 'should not have closed' ); t.ok( debugLogs.some((s) => s.indexOf('clearing timer for D') !== -1), 'should have cleared timers' ); t.match( debugLogs[debugLogs.length - 1], 'calling shutdown for D', 'should have called shutdown' ); t.end(); }); }); batch.test( 'should fail silently if loggingEvent property has no value', (t) => { t.teardown(async () => { await removeFiles('logs/E.log'); }); log4js.configure({ appenders: { multi: { type: 'multiFile', base: 'logs/', property: 'label', extension: '.log', }, }, categories: { default: { appenders: ['multi'], level: 'info' } }, }); const loggerE = log4js.getLogger(); loggerE.addContext('label', 'E'); loggerE.info('I am in logger E'); loggerE.removeContext('label'); loggerE.info('I am not in logger E'); loggerE.addContext('label', null); loggerE.info('I am also not in logger E'); log4js.shutdown(() => { const contents = fs.readFileSync('logs/E.log', 'utf-8'); t.match(contents, 'I am in logger E'); t.notMatch(contents, 'I am not in logger E'); t.notMatch(contents, 'I am also not in logger E'); t.end(); }); } ); batch.test('should pass options to rolling file stream', (t) => { t.teardown(async () => { await removeFiles(['logs/F.log', 'logs/F.log.1', 'logs/F.log.2']); }); log4js.configure({ appenders: { multi: { type: 'multiFile', base: 'logs/', property: 'label', extension: '.log', maxLogSize: 30, backups: 2, layout: { type: 'messagePassThrough' }, }, }, categories: { default: { appenders: ['multi'], level: 'info' } }, }); const loggerF = log4js.getLogger(); loggerF.addContext('label', 'F'); loggerF.info('Being in logger F is the best.'); loggerF.info('I am also in logger F, awesome'); loggerF.info('I am in logger F'); log4js.shutdown(() => { let contents = fs.readFileSync('logs/F.log', 'utf-8'); t.match(contents, 'I am in logger F'); contents = fs.readFileSync('logs/F.log.1', 'utf-8'); t.match(contents, 'I am also in logger F'); contents = fs.readFileSync('logs/F.log.2', 'utf-8'); t.match(contents, 'Being in logger F is the best'); t.end(); }); }); batch.test('should inherit config from category hierarchy', (t) => { t.teardown(async () => { await removeFiles('logs/test.someTest.log'); }); log4js.configure({ appenders: { out: { type: 'stdout' }, test: { type: 'multiFile', base: 'logs/', property: 'categoryName', extension: '.log', }, }, categories: { default: { appenders: ['out'], level: 'info' }, test: { appenders: ['test'], level: 'debug' }, }, }); const testLogger = log4js.getLogger('test.someTest'); testLogger.debug('This should go to the file'); log4js.shutdown(() => { const contents = fs.readFileSync('logs/test.someTest.log', 'utf-8'); t.match(contents, 'This should go to the file'); t.end(); }); }); batch.test('should shutdown safely even if it is not used', (t) => { log4js.configure({ appenders: { out: { type: 'stdout' }, test: { type: 'multiFile', base: 'logs/', property: 'categoryName', extension: '.log', }, }, categories: { default: { appenders: ['out'], level: 'info' }, test: { appenders: ['test'], level: 'debug' }, }, }); log4js.shutdown(() => { t.ok('callback is called'); t.end(); }); }); batch.teardown(async () => { try { const files = fs.readdirSync('logs'); await removeFiles(files.map((filename) => `logs/${filename}`)); fs.rmdirSync('logs'); } catch (e) { // doesn't matter } }); batch.end(); });