log4js-node/test/tap/multi-file-appender-test.js

462 lines
14 KiB
JavaScript

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();
});