Merge pull request #149 from mkielar/master

Clustered appender for log4js.
This commit is contained in:
Gareth Jones 2013-08-08 18:04:18 -07:00
commit 49892f35d3
2 changed files with 234 additions and 0 deletions

118
lib/appenders/clustered.js Executable file
View File

@ -0,0 +1,118 @@
"use strict";
var cluster = require('cluster');
var log4js = require('../log4js');
/**
* Takes a loggingEvent object, returns string representation of it.
*/
function serializeLoggingEvent(loggingEvent) {
return JSON.stringify(loggingEvent);
}
/**
* Takes a string, returns an object with
* the correct log properties.
*
* This method has been "borrowed" from the `multiprocess` appender
* by `nomiddlename` (https://github.com/nomiddlename/log4js-node/blob/master/lib/appenders/multiprocess.js)
*
* Apparently, node.js serializes everything to strings when using `process.send()`,
* so we need smart deserialization that will recreate log date and level for further processing by log4js internals.
*/
function deserializeLoggingEvent(loggingEventString) {
var loggingEvent;
try {
loggingEvent = JSON.parse(loggingEventString);
loggingEvent.startTime = new Date(loggingEvent.startTime);
loggingEvent.level = log4js.levels.toLevel(loggingEvent.level.levelStr);
} catch (e) {
// JSON.parse failed, just log the contents probably a naughty.
loggingEvent = {
startTime: new Date(),
categoryName: 'log4js',
level: log4js.levels.ERROR,
data: [ 'Unable to parse log:', loggingEventString ]
};
}
return loggingEvent;
}
/**
* Creates an appender.
*
* If the current process is a master (`cluster.isMaster`), then this will be a "master appender".
* Otherwise this will be a worker appender, that just sends loggingEvents to the master process.
*
* If you are using this method directly, make sure to provide it with `config.actualAppenders` array
* of actual appender instances.
*
* Or better use `configure(config, options)`
*/
function createAppender(config) {
if (cluster.isMaster) {
var masterAppender = function(loggingEvent) {
if (config.actualAppenders) {
var size = config.actualAppenders.length;
for(var i = 0; i < size; i++) {
config.actualAppenders[i](loggingEvent);
}
}
}
// Listen on new workers
cluster.on('fork', function(worker) {
worker.on('message', function(message) {
if (message.type && message.type === '::log-message') {
// console.log("master : " + cluster.isMaster + " received message: " + JSON.stringify(message.event));
var loggingEvent = deserializeLoggingEvent(message.event);
masterAppender(loggingEvent);
}
});
});
return masterAppender;
} else {
return function(loggingEvent) {
// If inside the worker process, then send the logger event to master.
if (cluster.isWorker) {
// console.log("worker " + cluster.worker.id + " is sending message");
process.send({ type: '::log-message', event: serializeLoggingEvent(loggingEvent)});
}
}
}
}
function configure(config, options) {
if (config.appenders && cluster.isMaster) {
var size = config.appenders.length;
config.actualAppenders = new Array(size);
for(var i = 0; i < size; i++) {
log4js.loadAppender(config.appenders[i].type);
config.actualAppenders[i] = log4js.appenderMakers[config.appenders[i].type](config.appenders[i], options);
}
}
return createAppender(config);
}
exports.appender = createAppender;
exports.configure = configure;

116
test/clusteredAppender-test.js Executable file
View File

@ -0,0 +1,116 @@
"use strict";
var assert = require('assert');
var vows = require('vows');
var layouts = require('../lib/layouts');
var sandbox = require('sandboxed-module');
var LoggingEvent = require('../lib/logger').LoggingEvent;
var cluster = require('cluster');
vows.describe('log4js cluster appender').addBatch({
'when in master mode': {
topic: function() {
var registeredClusterEvents = [];
var loggingEvents = [];
// Fake cluster module, so no cluster listeners be really added
var fakeCluster = {
on: function(event, callback) {
registeredClusterEvents.push(event);
},
isMaster: true,
isWorker: false,
};
var fakeActualAppender = function(loggingEvent) {
loggingEvents.push(loggingEvent);
}
// Load appender and fake modules in it
var appenderModule = sandbox.require('../lib/appenders/clustered', {
requires: {
'cluster': fakeCluster,
}
});
var masterAppender = appenderModule.appender({
actualAppenders: [ fakeActualAppender ]
});
// Actual test - log message using masterAppender
masterAppender(new LoggingEvent('wovs', 'Info', ['masterAppender test']));
var returnValue = {
registeredClusterEvents: registeredClusterEvents,
loggingEvents: loggingEvents,
};
return returnValue;
},
"should register 'fork' event listener on 'cluster'": function(topic) {
assert.equal(topic.registeredClusterEvents[0], 'fork');
},
"should log using actual appender": function(topic) {
assert.equal(topic.loggingEvents[0].data[0], 'masterAppender test');
},
},
'when in worker mode': {
topic: function() {
var registeredProcessEvents = [];
// Fake cluster module, to fake we're inside a worker process
var fakeCluster = {
isMaster: false,
isWorker: true,
};
var fakeProcess = {
send: function(data) {
registeredProcessEvents.push(data);
},
};
// Load appender and fake modules in it
var appenderModule = sandbox.require('../lib/appenders/clustered', {
requires: {
'cluster': fakeCluster,
},
globals: {
'process': fakeProcess,
}
});
var workerAppender = appenderModule.appender();
// Actual test - log message using masterAppender
workerAppender(new LoggingEvent('wovs', 'Info', ['workerAppender test']));
var returnValue = {
registeredProcessEvents: registeredProcessEvents,
};
return returnValue;
},
"worker appender should call process.send" : function(topic) {
assert.equal(topic.registeredProcessEvents[0].type, '::log-message');
assert.equal(JSON.parse(topic.registeredProcessEvents[0].event).data[0], "workerAppender test");
}
}
}).exportTo(module);