Merge pull request #30 from dbrain/master

hook.io appender
This commit is contained in:
Gareth Jones 2011-10-27 15:22:35 -07:00
commit b2827076da
7 changed files with 271 additions and 50 deletions

View File

@ -100,6 +100,65 @@ The options object that is passed to log4js.connectLogger supports a format stri
app.use(log4js.connectLogger(logger, { level: log4js.levels.INFO, format: ':method :url' })); app.use(log4js.connectLogger(logger, { level: log4js.levels.INFO, format: ':method :url' }));
}); });
## hook.io logger
A [hook.io](https://github.com/hookio) logger has been added to log4js by [dbrain](https://github.com/dbrain). This allows multiple worker processes to log through a single master process, avoiding issues with rolling etc. in a clustered environment.
This was mainly created for [cluster](https://github.com/LearnBoost/cluster), but you can adapt the example to match your needs, you know, if it fits them.
<pre>
#### log4js-master.json ####
{
"appenders": [{
"type": "logLevelFilter",
"level": "DEBUG",
"appender": {
"type": "hookio",
"name": "hookio-logger",
"mode": "master",
"debug": false,
"appender": {
"type": "file",
"filename": "muffin.log",
"maxLogSize": 104857600,
"backups": 10,
"pollInterval": 15
}
}
}]
}
#### log4js-worker.json ####
{
"appenders": [{
"type": "logLevelFilter",
"level": "DEBUG",
"appender": {
"type": "hookio",
"name": "hookio-logger",
"mode": "worker",
"debug": false
}
}]
}
#### ilikebeans.js ####
var cluster = require('cluster');
var hookCluster = cluster('./doyoulikebeans');
// Perform the once off configuration depending on type of cluster
if (hookCluster.isMaster) {
require('log4js').configure('log4js-master.json');
} else {
require('log4js').configure('log4js-worker.json');
}
// Standard cluster startup
hookCluster
.use(cluster.logger('run/logs'))
.use(cluster.pidfiles('run/pids'))
.listen(3000);
</pre>
*NOTE* hook.io appender will currently (and probably indefinitely) explode if you enable hook.io debug because of the way log4js overrides console.log
## author (of this node version) ## author (of this node version)
Gareth Jones (csausdev - gareth.jones@sensis.com.au) Gareth Jones (csausdev - gareth.jones@sensis.com.au)

56
lib/appenders/hookio.js Normal file
View File

@ -0,0 +1,56 @@
var log4js = require('../log4js');
var layouts = require('../layouts');
var Hook = require('hook.io').Hook;
var util = require('util');
var Logger = function createLogger(options) {
var self = this;
var actualAppender = options.actualAppender;
Hook.call(self, options);
self.on('hook::ready', function hookReady() {
self.on('*::' + options.name + '::log', function log(loggingEvent) {
deserializeLoggingEvent(loggingEvent);
actualAppender(loggingEvent);
});
});
}
util.inherits(Logger, Hook);
function deserializeLoggingEvent(loggingEvent) {
loggingEvent.startTime = new Date(loggingEvent.startTime);
loggingEvent.level.toString = function levelToString() {
return loggingEvent.level.levelStr;
};
}
function createAppender(hookioOptions, actualAppender) {
var loggerHook;
if (hookioOptions.mode === 'master') {
// Start the master hook, handling the actual logging
loggerHook = new Logger({ name: hookioOptions.name, debug: hookioOptions.debug, actualAppender: actualAppender });
} else {
// Start a worker, just emitting events for a master
loggerHook = new Hook({ name: hookioOptions.name, debug: hookioOptions.debug });
}
loggerHook.start();
var loggerEvent = hookioOptions.name + '::log';
return function log(loggingEvent) {
loggerHook.emit(loggerEvent, loggingEvent);
};
}
function configure(config) {
var actualAppender;
if (config.appender && config.mode === 'master') {
log4js.loadAppender(config.appender.type);
actualAppender = log4js.appenderMakers[config.appender.type](config.appender);
}
delete config.appender;
delete config.type;
return createAppender(config, actualAppender);
}
exports.name = 'hookio';
exports.appender = createAppender;
exports.configure = configure;

View File

@ -1,4 +1,5 @@
var levels = require('../levels'); var levels = require('../levels');
var log4js = require('../log4js');
function logLevelFilter (levelString, appender) { function logLevelFilter (levelString, appender) {
var level = levels.toLevel(levelString); var level = levels.toLevel(levelString);
@ -10,7 +11,8 @@ function logLevelFilter (levelString, appender) {
} }
function configure(config) { function configure(config) {
var appender = config.makers[config.appender.type](config.appender); log4js.loadAppender(config.appender.type);
var appender = log4js.appenderMakers[config.appender.type](config.appender);
return logLevelFilter(config.level, appender); return logLevelFilter(config.level, appender);
} }

View File

@ -136,6 +136,7 @@ function configureAppenders(appenderList) {
clearAppenders(); clearAppenders();
if (appenderList) { if (appenderList) {
appenderList.forEach(function(appenderConfig) { appenderList.forEach(function(appenderConfig) {
loadAppender(appenderConfig.type);
var appender; var appender;
appenderConfig.makers = appenderMakers; appenderConfig.makers = appenderMakers;
appender = appenderMakers[appenderConfig.type](appenderConfig); appender = appenderMakers[appenderConfig.type](appenderConfig);
@ -248,13 +249,13 @@ function getDefaultLogger () {
return getLogger(DEFAULT_CATEGORY); return getLogger(DEFAULT_CATEGORY);
} }
function findConfiguration() { function findConfiguration(filename) {
var path; var path;
try { try {
path = require.resolve('log4js.json'); path = require.resolve(filename || 'log4js.json');
} catch (e) { } catch (e) {
//file not found. default to the one in the log4js module. //file not found. default to the one in the log4js module.
path = __dirname + '/log4js.json'; path = filename || __dirname + '/log4js.json';
} }
return path; return path;
@ -263,7 +264,7 @@ function findConfiguration() {
var configState = {}; var configState = {};
function loadConfigurationFile(filename) { function loadConfigurationFile(filename) {
filename = filename || findConfiguration(); filename = findConfiguration(filename);
if (filename && (!configState.lastFilename || filename !== configState.lastFilename || if (filename && (!configState.lastFilename || filename !== configState.lastFilename ||
!configState.lastMTime || fs.statSync(filename).mtime !== configState.lastMTime)) { !configState.lastMTime || fs.statSync(filename).mtime !== configState.lastMTime)) {
configState.lastFilename = filename; configState.lastFilename = filename;
@ -285,7 +286,7 @@ function configureOnceOff(config) {
} }
function reloadConfiguration() { function reloadConfiguration() {
var filename = configState.filename || findConfiguration(), var filename = findConfiguration(configState.filename),
mtime; mtime;
if (!filename) { if (!filename) {
// can't find anything to reload // can't find anything to reload
@ -347,15 +348,10 @@ function replaceConsole(logger) {
}); });
} }
function loadAppenders() { function loadAppender(appender) {
var appenderList = fs.readdirSync(__dirname + '/appenders'); var appenderModule = require('./appenders/' + appender);
appenderList.forEach(function(file) {
if (/\.js$/.test(file)) {
var appenderModule = require('./appenders/' + file);
module.exports.appenders[appenderModule.name] = appenderModule.appender; module.exports.appenders[appenderModule.name] = appenderModule.appender;
appenderMakers[appenderModule.name] = appenderModule.configure; appenderMakers[appenderModule.name] = appenderModule.configure;
}
});
} }
module.exports = { module.exports = {
@ -363,6 +359,7 @@ module.exports = {
getDefaultLogger: getDefaultLogger, getDefaultLogger: getDefaultLogger,
addAppender: addAppender, addAppender: addAppender,
loadAppender: loadAppender,
clearAppenders: clearAppenders, clearAppenders: clearAppenders,
configure: configure, configure: configure,
@ -371,10 +368,15 @@ module.exports = {
layouts: layouts, layouts: layouts,
appenders: {}, appenders: {},
appenderMakers: appenderMakers,
connectLogger: require('./connect-logger').connectLogger connectLogger: require('./connect-logger').connectLogger
}; };
loadAppenders(); //load the old-style appenders
[ 'console', 'file', 'logLevelFilter' ].forEach(function(appender) {
loadAppender(appender);
});
//set ourselves up if we can find a default log4js.json //set ourselves up if we can find a default log4js.json
configure(findConfiguration()); configure(findConfiguration());
//replace console.log, etc with log4js versions //replace console.log, etc with log4js versions

View File

@ -21,8 +21,11 @@
"test": "test", "test": "test",
"lib": "lib" "lib": "lib"
}, },
"dependencies": {
},
"devDependencies": { "devDependencies": {
"vows": ">=0.5.2", "vows": ">=0.5.2",
"sandboxed-module": ">= 0.1.1" "sandboxed-module": ">= 0.1.1",
"hook.io": "0.7.7"
} }
} }

99
test/hookioAppender.js Normal file
View File

@ -0,0 +1,99 @@
var vows = require('vows');
var assert = require('assert');
var sandbox = require('sandboxed-module');
function fancyResultingHookioAppender(opts) {
var result = { ons: {}, emissions: {}, logged: [] };
var fakeLog4Js = {
appenderMakers: {}
};
fakeLog4Js.loadAppender = function (appender) {
fakeLog4Js.appenderMakers[appender] = function (config) {
result.actualLoggerConfig = config;
return function log(logEvent) {
result.logged.push(logEvent);
}
};
};
var fakeHookIo = { Hook: function() { } };
fakeHookIo.Hook.prototype.start = function () {
result.startCalled = true;
};
fakeHookIo.Hook.prototype.on = function (eventName, functionToExec) {
result.ons[eventName] = { functionToExec: functionToExec };
if (eventName === 'hook::ready') {
functionToExec();
}
};
fakeHookIo.Hook.prototype.emit = function (eventName, data) {
result.emissions[eventName] = result.emissions[eventName] || [];
result.emissions[eventName].push({data: data});
var on = '*::' + eventName;
if (eventName !== 'hook::ready' && result.ons[on]) {
result.ons[on].callingCount = result.ons[on].callingCount ? result.ons[on].callingCount += 1 : 1;
result.ons[on].functionToExec(data);
}
};
return { theResult: result,
theModule: sandbox.require('../lib/appenders/hookio', {
requires: {
'../log4js': fakeLog4Js,
'hook.io': fakeHookIo
}
})
};
}
vows.describe('log4js hookioAppender').addBatch({
'master': {
topic: function() {
var fancy = fancyResultingHookioAppender();
var logger = fancy.theModule.configure({ name: 'ohno', mode: 'master', appender: { type: 'file' } });
logger({ level: { levelStr: 'INFO' }, data: "ALRIGHTY THEN", startTime: '2011-10-27T03:53:16.031Z' });
logger({ level: { levelStr: 'DEBUG' }, data: "OH WOW", startTime: '2011-10-27T04:53:16.031Z'});
return fancy.theResult;
},
'should write to the actual appender': function (result) {
assert.isTrue(result.startCalled);
assert.equal(result.logged.length, 2);
assert.equal(result.emissions['ohno::log'].length, 2);
assert.equal(result.ons['*::ohno::log'].callingCount, 2);
},
'data written should be formatted correctly': function (result) {
assert.equal(result.logged[0].level.toString(), 'INFO');
assert.equal(result.logged[0].data, 'ALRIGHTY THEN');
assert.isTrue(typeof(result.logged[0].startTime) === 'object');
assert.equal(result.logged[1].level.toString(), 'DEBUG');
assert.equal(result.logged[1].data, 'OH WOW');
assert.isTrue(typeof(result.logged[1].startTime) === 'object');
},
'the actual logger should get the right config': function (result) {
assert.equal(result.actualLoggerConfig.type, 'file');
}
},
'worker': {
'should emit logging events to the master': {
topic: function() {
var fancy = fancyResultingHookioAppender();
var logger = fancy.theModule.configure({ name: 'ohno', mode: 'worker', appender: { type: 'file' } });
logger({ level: { levelStr: 'INFO' }, data: "ALRIGHTY THEN", startTime: '2011-10-27T03:53:16.031Z' });
logger({ level: { levelStr: 'DEBUG' }, data: "OH WOW", startTime: '2011-10-27T04:53:16.031Z'});
return fancy.theResult;
},
'should not write to the actual appender': function (result) {
assert.isTrue(result.startCalled);
assert.equal(result.logged.length, 0);
assert.equal(result.emissions['ohno::log'].length, 2);
assert.isUndefined(result.ons['*::ohno::log']);
}
}
}
}).exportTo(module);

View File

@ -69,7 +69,7 @@ vows.describe('log4js').addBatch({
, log4js = sandbox.require( , log4js = sandbox.require(
'../lib/log4js' '../lib/log4js'
, { requires: , { requires:
{ './appenders/file.js': { './appenders/file':
{ {
name: "file" name: "file"
, appender: function() {} , appender: function() {}
@ -123,10 +123,10 @@ vows.describe('log4js').addBatch({
}); });
}, },
readdirSync: function() { readdirSync: function() {
return ['file.js']; return ['file'];
} }
} }
, './appenders/file.js': , './appenders/file':
{ {
name: "file" name: "file"
, appender: function() {} , appender: function() {}
@ -168,7 +168,7 @@ vows.describe('log4js').addBatch({
'../lib/log4js' '../lib/log4js'
, { , {
requires: { requires: {
'./appenders/console.js': fakeConsoleAppender './appenders/console': fakeConsoleAppender
} }
} }
); );
@ -303,7 +303,7 @@ vows.describe('log4js').addBatch({
{ {
requires: { requires: {
'fs': fakeFS 'fs': fakeFS
, './appenders/console.js': fakeConsole , './appenders/console': fakeConsole
} }
} }
); );
@ -434,7 +434,7 @@ vows.describe('log4js').addBatch({
{ {
requires: { requires: {
'fs': fakeFS, 'fs': fakeFS,
'./appenders/console.js': fakeConsole './appenders/console': fakeConsole
}, },
globals: { globals: {
'console': fakeConsole, 'console': fakeConsole,
@ -511,7 +511,7 @@ vows.describe('log4js').addBatch({
{ {
requires: { requires: {
'fs': fakeFS, 'fs': fakeFS,
'./appenders/console.js': fakeConsole './appenders/console': fakeConsole
}, },
globals: { globals: {
'console': fakeConsole, 'console': fakeConsole,