diff --git a/.gitignore b/.gitignore index 279bc30..268c88d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build node_modules .bob/ test/streams/test-rolling-file-stream* +test/streams/test-rolling-stream-with-existing-files* diff --git a/examples/flush-on-exit.js b/examples/flush-on-exit.js new file mode 100644 index 0000000..19c661c --- /dev/null +++ b/examples/flush-on-exit.js @@ -0,0 +1,27 @@ +/** + * run this, then "ab -c 10 -n 100 localhost:4444/" to test (in + * another shell) + */ +var log4js = require('../lib/log4js'); +log4js.configure({ + appenders: [ + { type: 'file', filename: 'cheese.log', category: 'cheese' }, + { type: 'console'} + ] +}); + +var logger = log4js.getLogger('cheese'); +logger.setLevel('INFO'); + +var http=require('http'); + +var server = http.createServer(function(request, response){ + response.writeHead(200, {'Content-Type': 'text/plain'}); + var rd = Math.random() * 50; + logger.info("hello " + rd); + response.write('hello '); + if (Math.floor(rd) == 30){ + log4js.shutdown(function() { process.exit(1); }); + } + response.end(); +}).listen(4444); diff --git a/lib/appenders/file.js b/lib/appenders/file.js index da0e80b..a0c2869 100644 --- a/lib/appenders/file.js +++ b/lib/appenders/file.js @@ -1,5 +1,6 @@ "use strict"; var layouts = require('../layouts') +, async = require('async') , path = require('path') , fs = require('fs') , streams = require('../streams') @@ -78,5 +79,16 @@ function configure(config, options) { return fileAppender(config.filename, layout, config.maxLogSize, config.backups); } +function shutdown(cb) { + async.forEach(openFiles, function(file, done) { + if (!file.write(eol, "utf-8")) { + file.once('drain', function() { + file.end(done); + }); + } + }, cb); +} + exports.appender = fileAppender; exports.configure = configure; +exports.shutdown = shutdown; diff --git a/lib/log4js.js b/lib/log4js.js index db2c230..84a7d03 100644 --- a/lib/log4js.js +++ b/lib/log4js.js @@ -44,17 +44,20 @@ * Website: http://log4js.berlios.de */ var events = require('events') +, async = require('async') , fs = require('fs') , path = require('path') , util = require('util') , layouts = require('./layouts') , levels = require('./levels') -, LoggingEvent = require('./logger').LoggingEvent -, Logger = require('./logger').Logger +, loggerModule = require('./logger') +, LoggingEvent = loggerModule.LoggingEvent +, Logger = loggerModule.Logger , ALL_CATEGORIES = '[all]' , appenders = {} , loggers = {} , appenderMakers = {} +, appenderShutdowns = {} , defaultConfig = { appenders: [ { type: "console" } @@ -303,9 +306,42 @@ function loadAppender(appender) { appenderModule = require(appender); } module.exports.appenders[appender] = appenderModule.appender.bind(appenderModule); + if (appenderModule.shutdown) { + appenderShutdowns[appender] = appenderModule.shutdown.bind(appenderModule); + } appenderMakers[appender] = appenderModule.configure.bind(appenderModule); } +/** + * Shutdown all log appenders. This will first disable all writing to appenders + * and then call the shutdown function each appender. + * + * @params {Function} cb - The callback to be invoked once all appenders have + * shutdown. If an error occurs, the callback will be given the error object + * as the first argument. + * @returns {void} + */ +function shutdown(cb) { + // First, disable all writing to appenders. This prevents appenders from + // not being able to be drained because of run-away log writes. + loggerModule.disableAllLogWrites(); + + // Next, get all the shutdown functions for appenders as an array. + var shutdownFunctions = Object.keys(appenderShutdowns).reduce( + function(accum, category) { + return accum.concat(appenderShutdowns[category]); + }, []); + + // Call each of the shutdown functions. + async.forEach( + shutdownFunctions, + function(shutdownFn, done) { + shutdownFn(done); + }, + cb + ); +} + module.exports = { getLogger: getLogger, getDefaultLogger: getDefaultLogger, @@ -315,6 +351,7 @@ module.exports = { loadAppender: loadAppender, clearAppenders: clearAppenders, configure: configure, + shutdown: shutdown, replaceConsole: replaceConsole, restoreConsole: restoreConsole, diff --git a/lib/logger.js b/lib/logger.js index 1c6ff50..3615a18 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -4,6 +4,8 @@ var levels = require('./levels') , events = require('events') , DEFAULT_CATEGORY = '[default]'; +var logWritesEnabled = true; + /** * Models a logging event. * @constructor @@ -69,7 +71,7 @@ Logger.prototype.isLevelEnabled = function(otherLevel) { }; Logger.prototype[levelString.toLowerCase()] = function () { - if (this.isLevelEnabled(level)) { + if (logWritesEnabled && this.isLevelEnabled(level)) { var args = Array.prototype.slice.call(arguments); args.unshift(level); Logger.prototype.log.apply(this, args); @@ -78,6 +80,23 @@ Logger.prototype.isLevelEnabled = function(otherLevel) { } ); +/** + * Disable all log writes. + * @returns {void} + */ +function disableAllLogWrites() { + logWritesEnabled = false; +} + +/** + * Enable log writes. + * @returns {void} + */ +function enableAllLogWrites() { + logWritesEnabled = true; +} exports.LoggingEvent = LoggingEvent; exports.Logger = Logger; +exports.disableAllLogWrites = disableAllLogWrites; +exports.enableAllLogWrites = enableAllLogWrites; diff --git a/test/dateFileAppender-test.js b/test/dateFileAppender-test.js index 59355e2..e64619d 100644 --- a/test/dateFileAppender-test.js +++ b/test/dateFileAppender-test.js @@ -4,7 +4,8 @@ var vows = require('vows') , path = require('path') , fs = require('fs') , sandbox = require('sandboxed-module') -, log4js = require('../lib/log4js'); +, log4js = require('../lib/log4js') +, EOL = require('os').EOL || '\n'; function removeFile(filename) { return function() { @@ -134,7 +135,10 @@ vows.describe('../lib/appenders/dateFile').addBatch({ teardown: removeFile('date-file-test.log'), 'should load appender configuration from a json file': function(err, contents) { - assert.include(contents, 'this should be written to the file' + require('os').EOL); + if (err) { + throw err; + } + assert.include(contents, 'this should be written to the file' + EOL); assert.equal(contents.indexOf('this should not be written to the file'), -1); } }, @@ -161,7 +165,7 @@ vows.describe('../lib/appenders/dateFile').addBatch({ , thisTime = format.asString(options.appenders[0].pattern, new Date()); fs.writeFileSync( path.join(__dirname, 'date-file-test' + thisTime), - "this is existing data" + require('os').EOL, + "this is existing data" + EOL, 'utf8' ); log4js.clearAppenders(); diff --git a/test/logger-test.js b/test/logger-test.js index 55899f2..0bd29e1 100644 --- a/test/logger-test.js +++ b/test/logger-test.js @@ -2,7 +2,8 @@ var vows = require('vows') , assert = require('assert') , levels = require('../lib/levels') -, Logger = require('../lib/logger').Logger; +, loggerModule = require('../lib/logger') +, Logger = loggerModule.Logger; vows.describe('../lib/logger').addBatch({ 'constructor with no parameters': { @@ -53,5 +54,28 @@ vows.describe('../lib/logger').addBatch({ assert.isTrue(logger.isErrorEnabled()); assert.isTrue(logger.isFatalEnabled()); } + }, + + 'should emit log events': { + topic: function() { + var events = [], + logger = new Logger(); + logger.addListener('log', function (logEvent) { events.push(logEvent); }); + logger.debug('Event 1'); + loggerModule.disableAllLogWrites(); + logger.debug('Event 2'); + loggerModule.enableAllLogWrites(); + logger.debug('Event 3'); + return events; + }, + + 'when log writes are enabled': function(events) { + assert.equal(events[0].data[0], 'Event 1'); + }, + + 'but not when log writes are disabled': function(events) { + assert.equal(events.length, 2); + assert.equal(events[1].data[0], 'Event 3'); + } } }).exportTo(module); diff --git a/test/logging-test.js b/test/logging-test.js index 32ff099..a62de31 100644 --- a/test/logging-test.js +++ b/test/logging-test.js @@ -75,13 +75,65 @@ vows.describe('log4js').addBatch({ assert.equal(events[1].level.toString(), 'WARN'); }, - 'should include the error if passed in': function (events) { + 'should include the error if passed in': function(events) { assert.instanceOf(events[2].data[1], Error); assert.equal(events[2].data[1].message, 'Pants are on fire!'); } - + } + }, + + 'when shutdown is called': { + topic: function() { + var events = { + appenderShutdownCalled: false, + shutdownCallbackCalled: false + }, + log4js = sandbox.require( + '../lib/log4js', + { + requires: { + './appenders/file': + { + name: "file", + appender: function() {}, + configure: function(configuration) { + return function() {}; + }, + shutdown: function(cb) { + events.appenderShutdownCalled = true; + cb(); + } + } + } + } + ), + shutdownCallback = function() { + events.shutdownCallbackCalled = true; + }, + config = { appenders: + [ { "type" : "file", + "filename" : "cheesy-wotsits.log", + "maxLogSize" : 1024, + "backups" : 3 + } + ] + }; + + log4js.configure(config); + log4js.shutdown(shutdownCallback); + // Re-enable log writing so other tests that use logger are not + // affected. + require('../lib/logger').enableAllLogWrites(); + return events; }, - + + 'should invoke appender shutdowns': function(events) { + assert.ok(events.appenderShutdownCalled); + }, + + 'should call callback': function(events) { + assert.ok(events.shutdownCallbackCalled); + } }, 'invalid configuration': {