Rewrote file appender, fixing issue #16 and issue #31

This commit is contained in:
Gareth Jones 2011-11-21 15:03:51 +11:00
parent 40c5f5ee70
commit 7d50c45801
3 changed files with 77 additions and 98 deletions

View File

@ -1,4 +1,5 @@
var layouts = require('../layouts') var layouts = require('../layouts')
, path = require('path')
, fs = require('fs'); , fs = require('fs');
/** /**
@ -8,46 +9,69 @@ var layouts = require('../layouts')
* @param layout a function that takes a logevent and returns a string (defaults to basicLayout). * @param layout a function that takes a logevent and returns a string (defaults to basicLayout).
* @param logSize - the maximum size (in bytes) for a log file, if not provided then logs won't be rotated. * @param logSize - the maximum size (in bytes) for a log file, if not provided then logs won't be rotated.
* @param numBackups - the number of log files to keep after logSize has been reached (default 5) * @param numBackups - the number of log files to keep after logSize has been reached (default 5)
* @param filePollInterval - the time in seconds between file size checks (default 30s)
*/ */
function fileAppender (file, layout, logSize, numBackups, filePollInterval) { function fileAppender (file, layout, logSize, numBackups) {
var bytesWritten = 0;
file = path.normalize(file);
layout = layout || layouts.basicLayout; layout = layout || layouts.basicLayout;
numBackups = numBackups === undefined ? 5 : numBackups; numBackups = numBackups === undefined ? 5 : numBackups;
//there has to be at least one backup if logSize has been specified //there has to be at least one backup if logSize has been specified
numBackups = numBackups === 0 ? 1 : numBackups; numBackups = numBackups === 0 ? 1 : numBackups;
filePollInterval = filePollInterval * 1000 || 30000;
function setupLogRolling () { function setupLogRolling () {
fs.watchFile( try {
file, var stat = fs.statSync(file);
{ bytesWritten = stat.size;
persistent: false, if (bytesWritten >= logSize) {
interval: filePollInterval rollThatLog();
},
function (curr, prev) {
if (curr.size >= logSize) {
rollThatLog();
}
} }
); } catch (e) {
//file does not exist
bytesWritten = 0;
}
} }
function rollThatLog () { function rollThatLog () {
//roll the backups (rename file.n-1 to file.n, where n <= numBackups) function index(filename) {
for (var i=numBackups; i > 0; i--) { return parseInt(filename.substring((path.basename(file) + '.').length), 10) || 0;
if (i > 1) { }
if (fileExists(file + '.' + (i-1))) {
fs.renameSync(file+'.'+(i-1), file+'.'+i); var nameMatcher = new RegExp('^' + path.basename(file));
} function justTheLogFiles (item) {
return nameMatcher.test(item);
}
function byIndex(a, b) {
if (index(a) > index(b)) {
return 1;
} else if (index(a) < index(b) ) {
return -1;
} else { } else {
fs.renameSync(file, file+'.1'); return 0;
} }
} }
function increaseFileIndex (fileToRename) {
var idx = index(fileToRename);
if (idx < numBackups) {
fs.renameSync(path.join(path.dirname(file), fileToRename), file + '.' + (idx + 1));
}
}
//roll the backups (rename file.n to file.n+1, where n <= numBackups)
fs.readdirSync(path.dirname(file))
.filter(justTheLogFiles)
.sort(byIndex)
.reverse()
.forEach(increaseFileIndex);
//let's make a new file //let's make a new file
var newLogFileFD = fs.openSync(file, 'a', 0644) var newLogFileFD = fs.openSync(file, 'a', 0644)
, oldLogFileFD = logFile.fd; , oldLogFileFD = logFile.fd;
logFile.fd = newLogFileFD; logFile.fd = newLogFileFD;
fs.close(oldLogFileFD); fs.close(oldLogFileFD);
//reset the counter
bytesWritten = 0;
} }
function fileExists (filename) { function fileExists (filename) {
@ -62,18 +86,16 @@ function fileAppender (file, layout, logSize, numBackups, filePollInterval) {
function openTheStream() { function openTheStream() {
var stream = fs.createWriteStream(file, { flags: 'a', mode: 0644, encoding: 'utf8' }); var stream = fs.createWriteStream(file, { flags: 'a', mode: 0644, encoding: 'utf8' });
stream.on("open", function() { stream.on("open", function() {
canWrite = true; if (logEventBuffer.length > 0) {
while (logEventBuffer.length > 0 && canWrite) { writeToLog(logEventBuffer.shift());
canWrite = writeToLog(logEventBuffer.shift());
} }
}); });
stream.on("error", function (err) { stream.on("error", function (err) {
console.error("log4js.fileAppender - Error happened ", err); console.error("log4js.fileAppender - Writing to file %s, error happened ", file, err);
}); });
stream.on("drain", function() { stream.on("drain", function() {
canWrite = true; if (logEventBuffer.length > 0) {
while (logEventBuffer.length > 0 && canWrite) { writeToLog(logEventBuffer.shift());
canWrite = writeToLog(logEventBuffer.shift());
} }
}); });
return stream; return stream;
@ -81,7 +103,6 @@ function fileAppender (file, layout, logSize, numBackups, filePollInterval) {
var logEventBuffer = [] var logEventBuffer = []
, canWrite = false
, logFile = openTheStream(); , logFile = openTheStream();
if (logSize > 0) { if (logSize > 0) {
@ -95,17 +116,17 @@ function fileAppender (file, layout, logSize, numBackups, filePollInterval) {
}); });
function writeToLog(loggingEvent) { function writeToLog(loggingEvent) {
return logFile.write(layout(loggingEvent)+'\n', "utf8"); var logMessage = layout(loggingEvent)+'\n';
//not entirely accurate, but it'll do.
bytesWritten += logMessage.length;
logFile.write(logMessage, "utf8");
if (bytesWritten >= logSize) {
rollThatLog();
}
} }
return function(loggingEvent) { return function(loggingEvent) {
//because the log stream is opened asynchronously, we don't want to write logEventBuffer.push(loggingEvent);
//until it is ready.
if (canWrite) {
canWrite = writeToLog(loggingEvent);
} else {
logEventBuffer.push(loggingEvent);
}
}; };
} }
@ -114,7 +135,7 @@ function configure(config) {
if (config.layout) { if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout); layout = layouts.layout(config.layout.type, config.layout);
} }
return fileAppender(config.filename, layout, config.maxLogSize, config.backups, config.pollInterval); return fileAppender(config.filename, layout, config.maxLogSize, config.backups);
} }
exports.name = "file"; exports.name = "file";

View File

@ -3,17 +3,16 @@ var log4js = require('./lib/log4js')
, i = 0; , i = 0;
log4js.configure({ log4js.configure({
"appenders": [ "appenders": [
{
type: "console"
, category: "console"
},
{ {
"type": "file", "type": "file",
"filename": "tmp-test.log", "filename": "tmp-test.log",
"maxLogSize": 1024, "maxLogSize": 1024,
"backups": 3, "backups": 3,
"pollInterval": 0.1,
"category": "test" "category": "test"
},
{
type: "console"
, category: "console"
} }
] ]
}); });
@ -23,6 +22,6 @@ function doTheLogging(x) {
log.info("Logging something %d", x); log.info("Logging something %d", x);
} }
for ( ; i < 100000; i++) { for ( ; i < 5000; i++) {
doTheLogging(i); doTheLogging(i);
} }

View File

@ -40,17 +40,15 @@ vows.describe('log4js fileAppender').addBatch({
, that = this; , that = this;
remove(testFile); remove(testFile);
remove(testFile + '.1'); remove(testFile + '.1');
//log file of 100 bytes maximum, no backups, check every 10ms for changes //log file of 100 bytes maximum, no backups
log4js.addAppender(log4js.fileAppender(testFile, log4js.layouts.basicLayout, 100, 0, 0.01), 'max-file-size'); log4js.addAppender(log4js.fileAppender(testFile, log4js.layouts.basicLayout, 100, 0), 'max-file-size');
logger.info("This is the first log message."); logger.info("This is the first log message.");
logger.info("This is an intermediate log message."); logger.info("This is an intermediate log message.");
//we have to wait before writing the second one, because node is too fast for the file system. logger.info("This is the second log message.");
setTimeout(function() { //wait for the file system to catch up
logger.info("This is the second log message.");
}, 200);
setTimeout(function() { setTimeout(function() {
fs.readFile(testFile, "utf8", that.callback); fs.readFile(testFile, "utf8", that.callback);
}, 400); }, 100);
}, },
'log file should only contain the second message': function(err, fileContents) { 'log file should only contain the second message': function(err, fileContents) {
assert.include(fileContents, "This is the second log message.\n"); assert.include(fileContents, "This is the second log message.\n");
@ -75,21 +73,17 @@ vows.describe('log4js fileAppender').addBatch({
remove(testFile+'.1'); remove(testFile+'.1');
remove(testFile+'.2'); remove(testFile+'.2');
//log file of 50 bytes maximum, 2 backups, check every 10ms for changes //log file of 50 bytes maximum, 2 backups
log4js.addAppender(log4js.fileAppender(testFile, log4js.layouts.basicLayout, 50, 2, 0.01), 'max-file-size-backups'); log4js.addAppender(log4js.fileAppender(testFile, log4js.layouts.basicLayout, 50, 2), 'max-file-size-backups');
logger.info("This is the first log message."); logger.info("This is the first log message.");
//we have to wait before writing the second one, because node is too fast for the file system. logger.info("This is the second log message.");
logger.info("This is the third log message.");
logger.info("This is the fourth log message.");
var that = this; var that = this;
//give the system a chance to open the stream
setTimeout(function() { setTimeout(function() {
logger.info("This is the second log message.");
}, 200);
setTimeout(function() {
logger.info("This is the third log message.");
}, 400);
setTimeout(function() {
logger.info("This is the fourth log message.");
fs.readdir(__dirname, that.callback); fs.readdir(__dirname, that.callback);
}, 600); }, 100);
}, },
'the log files': { 'the log files': {
topic: function(files) { topic: function(files) {
@ -146,42 +140,7 @@ vows.describe('log4js fileAppender').addBatch({
'should load appender configuration from a json file': function(err, contents) { 'should load appender configuration from a json file': function(err, contents) {
assert.include(contents, 'this should be written to the file\n'); assert.include(contents, 'this should be written to the file\n');
assert.equal(contents.indexOf('this should not be written to the file'), -1); assert.equal(contents.indexOf('this should not be written to the file'), -1);
}, }
'and log rolling': {
topic: function() {
var sandbox = require('sandboxed-module')
, that = this
, fileAppender = sandbox.require(
'../lib/appenders/file.js'
, { requires:
{ 'fs':
{
watchFile: function(filename, options, cb) {
that.callback(null, filename);
}
, createWriteStream: function() {
return {
on: function() {}
, end: function() {}
, destroy: function() {}
};
}
}
}
}
);
fileAppender.configure({
filename: "cheese.log"
, maxLogSize: 100
, backups: 5
, pollInterval: 30
});
},
'should watch the log file': function(watchedFile) {
assert.equal(watchedFile, 'cheese.log');
}
}
} }
} }