diff --git a/README.md b/README.md index c0ce34a..737010f 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,23 @@ Output: You can either configure the appenders and log levels manually (as above), or provide a configuration file (`log4js.configure('path/to/file.json')`) explicitly, or just let log4js look for a file called `log4js.json` (it looks in the current directory first, then the require paths, and finally looks for the default config included in the same directory as the `log4js.js` file). -An example file can be found in `test/log4js.json`. An example config file with log rolling is in `test/with-log-rolling.json` +An example file can be found in `test/log4js.json`. An example config file with log rolling is in `test/with-log-rolling.json`. +By default, the configuration file is checked for changes every 60 seconds, and if changed, reloaded. This allows changes to logging levels +to occur without restarting the application. + +To turn off configuration file change checking, configure with: + + var log4js = require('log4js'); + log4js.configure(undefined, {}); // load 'log4js.json' from NODE_PATH + +Or: + + log4js.configure('my_log4js_configuration.json', {}); + +To specify a different period: + + log4js.configure(undefined, { reloadSecs: 300 }); // load 'log4js.json' from NODE_PATH + You can also pass an object to the configure function, which has the same properties as the json versions. ## connect/express logger diff --git a/lib/log4js.js b/lib/log4js.js index b42580b..2623639 100644 --- a/lib/log4js.js +++ b/lib/log4js.js @@ -67,25 +67,25 @@ function getLogger (categoryName) { // Use default logger if categoryName is not specified or invalid if (!(typeof categoryName == "string")) { - categoryName = DEFAULT_CATEGORY; + categoryName = DEFAULT_CATEGORY; } var appenderList; if (!loggers[categoryName]) { - // Create the logger for this name if it doesn't already exist - loggers[categoryName] = new Logger(categoryName); - if (appenders[categoryName]) { - appenderList = appenders[categoryName]; - appenderList.forEach(function(appender) { - loggers[categoryName].addListener("log", appender); - }); - } - if (appenders[ALL_CATEGORIES]) { - appenderList = appenders[ALL_CATEGORIES]; - appenderList.forEach(function(appender) { - loggers[categoryName].addListener("log", appender); - }); - } + // Create the logger for this name if it doesn't already exist + loggers[categoryName] = new Logger(categoryName); + if (appenders[categoryName]) { + appenderList = appenders[categoryName]; + appenderList.forEach(function(appender) { + loggers[categoryName].addListener("log", appender); + }); + } + if (appenders[ALL_CATEGORIES]) { + appenderList = appenders[ALL_CATEGORIES]; + appenderList.forEach(function(appender) { + loggers[categoryName].addListener("log", appender); + }); + } } return loggers[categoryName]; @@ -98,65 +98,69 @@ function addAppender () { var args = Array.prototype.slice.call(arguments); var appender = args.shift(); if (args.length == 0 || args[0] === undefined) { - args = [ ALL_CATEGORIES ]; + args = [ ALL_CATEGORIES ]; } //argument may already be an array if (Array.isArray(args[0])) { - args = args[0]; + args = args[0]; } args.forEach(function(category) { - if (!appenders[category]) { - appenders[category] = []; - } - appenders[category].push(appender); + if (!appenders[category]) { + appenders[category] = []; + } + appenders[category].push(appender); - if (category === ALL_CATEGORIES) { - for (var logger in loggers) { - if (loggers.hasOwnProperty(logger)) { - loggers[logger].addListener("log", appender); - } - } - } else if (loggers[category]) { - loggers[category].addListener("log", appender); - } + if (category === ALL_CATEGORIES) { + for (var logger in loggers) { + if (loggers.hasOwnProperty(logger)) { + loggers[logger].addListener("log", appender); + } + } + } else if (loggers[category]) { + loggers[category].addListener("log", appender); + } }); } function clearAppenders () { appenders = {}; for (var logger in loggers) { - if (loggers.hasOwnProperty(logger)) { - loggers[logger].removeAllListeners("log"); - } + if (loggers.hasOwnProperty(logger)) { + loggers[logger].removeAllListeners("log"); + } } } function configureAppenders(appenderList) { clearAppenders(); if (appenderList) { - appenderList.forEach(function(appenderConfig) { + appenderList.forEach(function(appenderConfig) { var appender; appenderConfig.makers = appenderMakers; - appender = appenderMakers[appenderConfig.type](appenderConfig); - if (appender) { - addAppender(appender, appenderConfig.category); - } else { - throw new Error("log4js configuration problem for "+sys.inspect(appenderConfig)); - } - }); + appender = appenderMakers[appenderConfig.type](appenderConfig); + if (appender) { + addAppender(appender, appenderConfig.category); + } else { + throw new Error("log4js configuration problem for "+sys.inspect(appenderConfig)); + } + }); } else { - addAppender(consoleAppender()); + addAppender(consoleAppender()); } } function configureLevels(levels) { if (levels) { - for (var category in levels) { - if (levels.hasOwnProperty(category)) { - getLogger(category).setLevel(levels[category]); - } - } + for (var category in levels) { + if (levels.hasOwnProperty(category)) { + getLogger(category).setLevel(levels[category]); + } + } + } else { + for (l in loggers) { + loggers[l].setLevel(); + } } } @@ -214,18 +218,18 @@ Logger.prototype.isLevelEnabled = function(otherLevel) { ['Trace','Debug','Info','Warn','Error','Fatal'].forEach( function(levelString) { - var level = levels.toLevel(levelString); - Logger.prototype['is'+levelString+'Enabled'] = function() { - return this.isLevelEnabled(level); - }; + var level = levels.toLevel(levelString); + Logger.prototype['is'+levelString+'Enabled'] = function() { + return this.isLevelEnabled(level); + }; - Logger.prototype[levelString.toLowerCase()] = function () { - if (this.isLevelEnabled(level)) { + Logger.prototype[levelString.toLowerCase()] = function () { + if (this.isLevelEnabled(level)) { var args = Array.prototype.slice.call(arguments); args.unshift(level); Logger.prototype.log.apply(this, args); - } - }; + } + }; } ); @@ -242,21 +246,6 @@ function getDefaultLogger () { return getLogger(DEFAULT_CATEGORY); } -function configure (configurationFileOrObject) { - var config = configurationFileOrObject; - if (typeof(config) === "string") { - config = JSON.parse(fs.readFileSync(config, "utf8")); - } - if (config) { - try { - configureAppenders(config.appenders); - configureLevels(config.levels); - } catch (e) { - throw new Error("Problem reading log4js config " + sys.inspect(config) + ". Error was \"" + e.message + "\" ("+e.stack+")"); - } - } -} - function findConfiguration() { //add current directory onto the list of configPaths var paths = ['.'].concat(require.paths); @@ -276,6 +265,79 @@ function findConfiguration() { return undefined; } +function loadConfigurationFile(filename) { + filename = filename || findConfiguration(); + if (filename) { + return JSON.parse(fs.readFileSync(filename, "utf8")); + } + return undefined; +} + +function configureOnceOff(config) { + if (config) { + try { + configureAppenders(config.appenders); + configureLevels(config.levels); + } catch (e) { + throw new Error("Problem reading log4js config " + sys.inspect(config) + ". Error was \"" + e.message + "\" ("+e.stack+")"); + } + } +} + +var configState = {}; + +function reloadConfiguration() { + var filename = configState.filename || findConfiguration(), + mtime; + if (!filename) { + // can't find anything to reload + return; + } + try { + mtime = fs.statSync(filename).mtime; + } catch (e) { + getLogger('log4js').warn('Failed to load configuration file ' + filename); + return; + } + if (configState.lastFilename && configState.lastFilename === filename) { + if (mtime.getTime() > configState.lastMTime.getTime()) { + configState.lastMTime = mtime; + } else { + filename = null; + } + } else { + configState.lastFilename = filename; + configState.lastMTime = mtime; + } + configureOnceOff(loadConfigurationFile(filename)); +} + +function initReloadConfiguration(filename, options) { + if (configState.timerId) { + clearInterval(configState.timerId); + delete configState.timerId; + } + configState.filename = filename; + configState.timerId = setInterval(reloadConfiguration, options.reloadSecs*1000); +} + +function configure (configurationFileOrObject, options) { + var config = configurationFileOrObject; + if (config === undefined || config === null || typeof(config) === 'string') { + options = options || { reloadSecs: 60 }; + if (options.reloadSecs) { + initReloadConfiguration(config, options); + } + configureOnceOff(loadConfigurationFile(config)); + } else { + options = options || {}; + if (options.reloadSecs) { + getLogger('log4js').warn('Ignoring configuration reload parameter for "object" configuration.'); + } + configureOnceOff(config); + } +} + function replaceConsole(logger) { function replaceWith(fn) { return function() { diff --git a/test/logging.js b/test/logging.js index b08fd13..b9821b3 100644 --- a/test/logging.js +++ b/test/logging.js @@ -4,54 +4,54 @@ var vows = require('vows') vows.describe('log4js').addBatch({ 'getLogger': { - topic: function() { - var log4js = require('../lib/log4js'); - log4js.clearAppenders(); - var logger = log4js.getLogger('tests'); - logger.setLevel("DEBUG"); - return logger; - }, + topic: function() { + var log4js = require('../lib/log4js'); + log4js.clearAppenders(); + var logger = log4js.getLogger('tests'); + logger.setLevel("DEBUG"); + return logger; + }, - 'should take a category and return a logger': function(logger) { - assert.equal(logger.category, 'tests'); - assert.equal(logger.level.toString(), "DEBUG"); - assert.isFunction(logger.debug); - assert.isFunction(logger.info); - assert.isFunction(logger.warn); - assert.isFunction(logger.error); - assert.isFunction(logger.fatal); - }, + 'should take a category and return a logger': function(logger) { + assert.equal(logger.category, 'tests'); + assert.equal(logger.level.toString(), "DEBUG"); + assert.isFunction(logger.debug); + assert.isFunction(logger.info); + assert.isFunction(logger.warn); + assert.isFunction(logger.error); + assert.isFunction(logger.fatal); + }, - 'log events' : { - topic: function(logger) { - var events = []; - logger.addListener("log", function (logEvent) { events.push(logEvent); }); - logger.debug("Debug event"); - logger.trace("Trace event 1"); - logger.trace("Trace event 2"); - logger.warn("Warning event"); + 'log events' : { + topic: function(logger) { + var events = []; + logger.addListener("log", function (logEvent) { events.push(logEvent); }); + logger.debug("Debug event"); + logger.trace("Trace event 1"); + logger.trace("Trace event 2"); + logger.warn("Warning event"); logger.error("Aargh!", new Error("Pants are on fire!")); logger.error("Simulated CouchDB problem", { err: 127, cause: "incendiary underwear" }); - return events; - }, + return events; + }, - 'should emit log events': function(events) { - assert.equal(events[0].level.toString(), 'DEBUG'); - assert.equal(events[0].data[0], 'Debug event'); - assert.instanceOf(events[0].startTime, Date); - }, + 'should emit log events': function(events) { + assert.equal(events[0].level.toString(), 'DEBUG'); + assert.equal(events[0].data[0], 'Debug event'); + assert.instanceOf(events[0].startTime, Date); + }, - 'should not emit events of a lower level': function(events) { - assert.length(events, 4); - assert.equal(events[1].level.toString(), 'WARN'); - }, + 'should not emit events of a lower level': function(events) { + assert.length(events, 4); + assert.equal(events[1].level.toString(), 'WARN'); + }, '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!'); } - }, + }, }, @@ -65,7 +65,7 @@ vows.describe('log4js').addBatch({ 'configuration when passed as object': { topic: function() { - var appenderConfig + var appenderConfig , log4js = sandbox.require( '../lib/log4js' , { requires: @@ -82,22 +82,22 @@ vows.describe('log4js').addBatch({ } ) , config = { - "appenders": [ - { - "type" : "file", - "filename" : "cheesy-wotsits.log", - "maxLogSize" : 1024, - "backups" : 3, - "pollInterval" : 15 - } - ] - }; - log4js.configure(config); + "appenders": [ + { + "type" : "file", + "filename" : "cheesy-wotsits.log", + "maxLogSize" : 1024, + "backups" : 3, + "pollInterval" : 15 + } + ] + }; + log4js.configure(config); return appenderConfig; }, 'should be passed to appender config': function(configuration) { - assert.equal(configuration.filename, 'cheesy-wotsits.log'); - } + assert.equal(configuration.filename, 'cheesy-wotsits.log'); + } }, 'configuration when passed as filename': { @@ -169,7 +169,7 @@ vows.describe('log4js').addBatch({ } } ); - logger = log4js.getLogger("some-logger"); + logger = log4js.getLogger("some-logger"); logger.debug("This is a test"); }, 'should default to the console appender': function(evt) { @@ -295,7 +295,7 @@ vows.describe('log4js').addBatch({ return fakeConsole.appender(); } }, - log4js = sandbox.require( + log4js = sandbox.require( '../lib/log4js', { requires: { @@ -305,7 +305,7 @@ vows.describe('log4js').addBatch({ } ); - logger = log4js.getLogger('a-test'); + logger = log4js.getLogger('a-test'); logger.debug("this is a test"); return [ pathsChecked, appenderEvent, modulePath ]; }, @@ -380,16 +380,87 @@ vows.describe('log4js').addBatch({ } }, 'configuration persistence' : { - 'should maintain appenders between requires': function () { - var logEvent, firstLog4js = require('../lib/log4js'), secondLog4js; - firstLog4js.clearAppenders(); - firstLog4js.addAppender(function(evt) { logEvent = evt; }); + 'should maintain appenders between requires': function () { + var logEvent, firstLog4js = require('../lib/log4js'), secondLog4js; + firstLog4js.clearAppenders(); + firstLog4js.addAppender(function(evt) { logEvent = evt; }); - secondLog4js = require('../lib/log4js'); - secondLog4js.getLogger().info("This should go to the appender defined in firstLog4js"); + secondLog4js = require('../lib/log4js'); + secondLog4js.getLogger().info("This should go to the appender defined in firstLog4js"); - assert.equal(logEvent.data[0], "This should go to the appender defined in firstLog4js"); - } + assert.equal(logEvent.data[0], "This should go to the appender defined in firstLog4js"); + } + }, + 'configuration reload' : { + topic: function() { + var pathsChecked = [], + logEvents = [], + logger, + modulePath = require('path').normalize(__dirname + '/../lib/log4js.json'), + fakeFS = { + config: { appenders: [ { type: 'console', layout: { type: 'messagePassThrough' } } ], + levels: { 'a-test' : 'INFO' } }, + readdirSync: function(dir) { + return require('fs').readdirSync(dir); + }, + readFileSync: function (file, encoding) { + assert.equal(file, modulePath); + assert.equal(encoding, 'utf8'); + return JSON.stringify(fakeFS.config); + }, + statSync: function (path) { + pathsChecked.push(path); + if (path === modulePath) { + return { mtime: new Date() }; + } else { + throw new Error("no such file"); + } + } + }, + fakeConsole = { + 'name': 'console', + 'appender': function () { + return function(evt) { logEvents.push(evt); }; + }, + 'configure': function (config) { + return fakeConsole.appender(); + } + }, + setIntervalCallback, + fakeSetInterval = function(cb, timeout) { + setIntervalCallback = cb; + }, + log4js = sandbox.require( + '../lib/log4js', + { + requires: { + 'fs': fakeFS, + './appenders/console.js': fakeConsole + }, + globals: { + 'console': fakeConsole, + 'setInterval' : fakeSetInterval, + } + } + ); + + logger = log4js.getLogger('a-test'); + logger.info("info1"); + logger.debug("debug2 - should be ignored"); + delete fakeFS.config.levels; + setIntervalCallback(); + logger.info("info3"); + logger.debug("debug4"); + + return [ pathsChecked, logEvents, modulePath ]; + }, + 'should configure log4js from first log4js.json found': function(args) { + var logEvents = args[1]; + assert.length(logEvents, 3); + assert.equal(logEvents[0].data[0], 'info1'); + assert.equal(logEvents[1].data[0], 'info3'); + assert.equal(logEvents[2].data[0], 'debug4'); + } } }).export(module);