changed the way appenders are loaded, so that they don't need to include log4js as a direct dependency

This commit is contained in:
Gareth Jones
2013-09-15 14:37:01 +10:00
parent 245b2e3b1a
commit 491c2709e7
12 changed files with 350 additions and 185 deletions

View File

@@ -1,20 +1,21 @@
"use strict";
var layouts = require('../layouts')
, consoleLog = console.log.bind(console);
var consoleLog = console.log.bind(console);
function consoleAppender (layout) {
layout = layout || layouts.colouredLayout;
return function(loggingEvent) {
consoleLog(layout(loggingEvent));
};
}
function configure(config) {
var layout;
if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout);
module.exports = function(layouts, levels) {
function consoleAppender (layout) {
layout = layout || layouts.colouredLayout;
return function(loggingEvent) {
consoleLog(layout(loggingEvent));
};
}
return consoleAppender(layout);
}
exports.configure = configure;
return function configure(config) {
var layout;
if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout);
}
return consoleAppender(layout);
};
};

View File

@@ -1,6 +1,5 @@
"use strict";
var streams = require('streamroller')
, layouts = require('../layouts')
, path = require('path')
, os = require('os')
, eol = os.EOL || '\n'
@@ -13,42 +12,42 @@ process.on('exit', function() {
});
});
/**
* File appender that rolls files according to a date pattern.
* @filename base filename.
* @pattern the format that will be added to the end of filename when rolling,
* also used to check when to roll files - defaults to '.yyyy-MM-dd'
* @layout layout function for log messages - defaults to basicLayout
*/
function appender(filename, pattern, alwaysIncludePattern, layout) {
layout = layout || layouts.basicLayout;
module.exports = function(layouts, levels) {
/**
* File appender that rolls files according to a date pattern.
* @filename base filename.
* @pattern the format that will be added to the end of filename when rolling,
* also used to check when to roll files - defaults to '.yyyy-MM-dd'
* @layout layout function for log messages - defaults to basicLayout
*/
function appender(filename, pattern, alwaysIncludePattern, layout) {
layout = layout || layouts.basicLayout;
var logFile = new streams.DateRollingFileStream(
filename,
pattern,
{ alwaysIncludePattern: alwaysIncludePattern }
);
openFiles.push(logFile);
return function(logEvent) {
logFile.write(layout(logEvent) + eol, "utf8");
};
var logFile = new streams.DateRollingFileStream(
filename,
pattern,
{ alwaysIncludePattern: alwaysIncludePattern }
);
openFiles.push(logFile);
}
return function configure(config) {
var layout;
return function(logEvent) {
logFile.write(layout(logEvent) + eol, "utf8");
if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout);
}
if (!config.alwaysIncludePattern) {
config.alwaysIncludePattern = false;
}
return appender(config.filename, config.pattern, config.alwaysIncludePattern, layout);
};
}
function configure(config, options) {
var layout;
if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout);
}
if (!config.alwaysIncludePattern) {
config.alwaysIncludePattern = false;
}
return appender(config.filename, config.pattern, config.alwaysIncludePattern, layout);
}
exports.appender = appender;
exports.configure = configure;
};

View File

@@ -1,6 +1,5 @@
"use strict";
var layouts = require('../layouts')
, path = require('path')
var path = require('path')
, fs = require('fs')
, streams = require('streamroller')
, os = require('os')
@@ -14,65 +13,66 @@ process.on('exit', function() {
});
});
/**
* File Appender writing the logs to a text file. Supports rolling of logs by size.
*
* @param file file log messages will be written to
* @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 numBackups - the number of log files to keep after logSize
* has been reached (default 5)
*/
function fileAppender (file, layout, logSize, numBackups) {
var bytesWritten = 0;
file = path.normalize(file);
layout = layout || layouts.basicLayout;
numBackups = numBackups === undefined ? 5 : numBackups;
//there has to be at least one backup if logSize has been specified
numBackups = numBackups === 0 ? 1 : numBackups;
module.exports = function(layouts, levels) {
function openTheStream(file, fileSize, numFiles) {
var stream;
if (fileSize) {
stream = new streams.RollingFileStream(
file,
fileSize,
numFiles
);
} else {
stream = fs.createWriteStream(
file,
{ encoding: "utf8",
mode: parseInt('0644', 8),
flags: 'a' }
);
/**
* File Appender writing the logs to a text file. Supports rolling of logs by size.
*
* @param file file log messages will be written to
* @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 numBackups - the number of log files to keep after logSize
* has been reached (default 5)
*/
function fileAppender (file, layout, logSize, numBackups) {
var bytesWritten = 0;
file = path.normalize(file);
layout = layout || layouts.basicLayout;
numBackups = numBackups === undefined ? 5 : numBackups;
//there has to be at least one backup if logSize has been specified
numBackups = numBackups === 0 ? 1 : numBackups;
function openTheStream(file, fileSize, numFiles) {
var stream;
if (fileSize) {
stream = new streams.RollingFileStream(
file,
fileSize,
numFiles
);
} else {
stream = fs.createWriteStream(
file,
{ encoding: "utf8",
mode: parseInt('0644', 8),
flags: 'a' }
);
}
stream.on("error", function (err) {
console.error("log4js.fileAppender - Writing to file %s, error happened ", file, err);
});
return stream;
}
stream.on("error", function (err) {
console.error("log4js.fileAppender - Writing to file %s, error happened ", file, err);
});
return stream;
var logFile = openTheStream(file, logSize, numBackups);
// push file to the stack of open handlers
openFiles.push(logFile);
return function(loggingEvent) {
logFile.write(layout(loggingEvent) + eol, "utf8");
};
}
var logFile = openTheStream(file, logSize, numBackups);
// push file to the stack of open handlers
openFiles.push(logFile);
return function(loggingEvent) {
logFile.write(layout(loggingEvent) + eol, "utf8");
return function configure(config) {
var layout;
if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout);
}
return fileAppender(config.filename, layout, config.maxLogSize, config.backups);
};
}
function configure(config) {
var layout;
if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout);
}
return fileAppender(config.filename, layout, config.maxLogSize, config.backups);
}
exports.appender = fileAppender;
exports.configure = configure;
};

View File

@@ -1,40 +1,40 @@
"use strict";
var levels = require('../levels')
, debug = require('debug')('log4js:logLevelFilter')
, log4js = require('../log4js');
var debug = require('debug')('log4js:logLevelFilter');
function logLevelFilter(allowedLevels, appender) {
return function(logEvent) {
debug("Checking ", logEvent.level, " against ", allowedLevels);
if (allowedLevels.some(function(item) { return item.level === logEvent.level.level; })) {
debug("Sending ", logEvent, " to appender ", appender);
appender(logEvent);
module.exports = function(layouts, levels) {
function logLevelFilter(allowedLevels, appender) {
return function(logEvent) {
debug("Checking ", logEvent.level, " against ", allowedLevels);
if (allowedLevels.some(function(item) { return item.level === logEvent.level.level; })) {
debug("Sending ", logEvent, " to appender ", appender);
appender(logEvent);
}
};
}
return function configure(config, appenderByName) {
if (!Array.isArray(config.allow)) {
throw new Error("No allowed log levels specified.");
}
var allowedLevels = config.allow.map(function(allowed) {
var level = levels.toLevel(allowed);
if (!level) {
throw new Error("Unrecognised log level '" + allowed + "'.");
}
return level;
});
if (allowedLevels.length === 0) {
throw new Error("No allowed log levels specified.");
}
if (!config.appender) {
throw new Error("Missing an appender.");
}
return logLevelFilter(allowedLevels, appenderByName(config.appender));
};
}
function configure(config, appenderByName) {
if (!Array.isArray(config.allow)) {
throw new Error("No allowed log levels specified.");
}
var allowedLevels = config.allow.map(function(allowed) {
var level = levels.toLevel(allowed);
if (!level) {
throw new Error("Unrecognised log level '" + allowed + "'.");
}
return level;
});
if (allowedLevels.length === 0) {
throw new Error("No allowed log levels specified.");
}
if (!config.appender) {
throw new Error("Missing an appender.");
}
return logLevelFilter(allowedLevels, appenderByName(config.appender));
}
exports.configure = configure;
};

View File

@@ -213,8 +213,9 @@ function validateCategories(cats) {
}
function clearAppenders () {
debug("clearing appenders");
debug("clearing appenders and appender makers");
appenders = {};
appenderMakers = {};
}
function appenderByName(name) {
@@ -230,7 +231,6 @@ function configureAppenders(appenderMap) {
Object.keys(appenderMap).forEach(function(appenderName) {
var appender, appenderConfig = appenderMap[appenderName];
loadAppender(appenderConfig.type);
appenderConfig.makers = appenderMakers;
try {
appenders[appenderName] = appenderMakers[appenderConfig.type](
appenderConfig,
@@ -247,21 +247,26 @@ function configureAppenders(appenderMap) {
function loadAppender(appender) {
var appenderModule;
try {
appenderModule = require('./appenders/' + appender);
} catch (e) {
if (!appenderMakers[appender]) {
debug("Loading appender ", appender);
try {
appenderModule = require(appender);
} catch (err) {
throw new Error("Could not load appender of type '" + appender + "'.");
appenderModule = require('./appenders/' + appender);
} catch (e) {
try {
debug("Appender ", appender, " is not a core log4js appender.");
appenderModule = require(appender);
} catch (err) {
throw new Error("Could not load appender of type '" + appender + "'.");
}
}
}
appenderMakers[appender] = appenderModule.configure.bind(appenderModule);
appenderMakers[appender] = appenderModule(layouts, levels);
}
}
module.exports = {
getLogger: getLogger,
configure: configure,
configure: configure
};
//set ourselves up

View File

@@ -30,12 +30,12 @@ describe('log4js in a cluster', function() {
cb(worker);
}
},
'./appenders/console': {
configure: function() {
'./appenders/console': function() {
return function() {
return function(event) {
events.push(event);
};
}
};
}
}
}

View File

@@ -17,7 +17,7 @@ describe('../lib/appenders/console', function() {
}
}
)
, appender = appenderModule.configure(
, appender = appenderModule(require('../lib/layouts'))(
{ layout: { type: "messagePassThrough" } }
);

View File

@@ -17,7 +17,7 @@ describe('../lib/appenders/dateFile', function() {
var files = [], initialListeners;
before(function() {
var dateFileAppender = require('../lib/appenders/dateFile'),
var dateFileAppender = require('../lib/appenders/dateFile')({ basicLayout: function() {} }),
count = 5,
logfile;
@@ -25,7 +25,7 @@ describe('../lib/appenders/dateFile', function() {
while (count--) {
logfile = path.join(__dirname, 'datefa-default-test' + count + '.log');
dateFileAppender.configure({
dateFileAppender({
filename: logfile
});
files.push(logfile);
@@ -69,10 +69,10 @@ describe('../lib/appenders/dateFile', function() {
}
}
}
);
)({ basicLayout: function() {} });
for (var i=0; i < 5; i += 1) {
dateFileAppender.configure({
dateFileAppender({
filename: 'test' + i
});
}

View File

@@ -75,9 +75,9 @@ describe('log4js fileAppender', function() {
}
}
}
);
)(require('../lib/layouts'));
for (var i=0; i < 5; i += 1) {
fileAppender.appender('test' + i, null, 100);
fileAppender({ filename: 'test' + i, maxLogSize: 100 });
}
openedFiles.should.not.be.empty;
exitListener();
@@ -317,8 +317,8 @@ describe('log4js fileAppender', function() {
}
}
}
);
fileAppender.configure({
)(require('../lib/layouts'));
fileAppender({
filename: 'test1.log',
maxLogSize: 100
});

View File

@@ -178,10 +178,10 @@ describe('../lib/log4js', function() {
'../lib/log4js',
{
requires: {
'cheese': {
configure: function() {
'cheese': function() {
return function() {
return function(evt) { events.push(evt); };
}
};
}
}
}
@@ -201,6 +201,102 @@ describe('../lib/log4js', function() {
});
it('should only load third-party appenders once', function() {
var moduleCalled = 0
, log4js_sandbox = sandbox.require(
'../lib/log4js',
{
requires: {
'cheese': function() {
moduleCalled += 1;
return function() {
return function() {};
};
}
}
}
);
log4js_sandbox.configure({
appenders: {
"thing1": { type: "cheese" },
"thing2": { type: "cheese" }
},
categories: {
default: { level: "DEBUG", appenders: [ "thing1", "thing2" ] }
}
});
moduleCalled.should.eql(1);
});
it('should pass layouts and levels to appender modules', function() {
var layouts
, levels
, log4js_sandbox = sandbox.require(
'../lib/log4js',
{
requires: {
'cheese': function(arg1, arg2) {
layouts = arg1;
levels = arg2;
return function() {
return function() {};
};
}
}
}
);
log4js_sandbox.configure({
appenders: {
"thing": { type: "cheese" }
},
categories: {
"default": { level: "debug", appenders: [ "thing" ] }
}
});
layouts.should.have.property("basicLayout");
levels.should.have.property("toLevel");
});
it('should pass config and appenderByName to appender makers', function() {
var otherAppender = function() { /* I do nothing */ }
, config
, other
, log4js_sandbox = sandbox.require(
'../lib/log4js',
{
requires: {
'other': function() {
return function() {
return otherAppender;
};
},
'cheese': function() {
return function(arg1, arg2) {
config = arg1;
other = arg2("other");
return function() {};
};
}
}
}
);
log4js_sandbox.configure({
appenders: {
"other": { type: "other" },
"thing": { type: "cheese", something: "something" }
},
categories: {
default: { level: "debug", appenders: [ "other", "thing" ] }
}
});
other.should.equal(otherAppender);
config.should.have.property("something", "something");
});
it('should complain about unknown appenders', function() {
(function() {
log4js.configure({
@@ -221,10 +317,10 @@ describe('../lib/log4js', function() {
'../lib/log4js',
{
requires: {
'cheese': {
configure: function() {
'cheese': function() {
return function() {
return function(event) { events.push(event); };
}
};
}
}
}
@@ -244,10 +340,10 @@ describe('../lib/log4js', function() {
'../lib/log4js',
{
requires: {
'cheese': {
configure: function() {
'cheese': function() {
return function() {
return function(event) { events.push(event); };
}
};
}
}
}
@@ -273,10 +369,10 @@ describe('../lib/log4js', function() {
'../lib/log4js',
{
requires: {
'cheese': {
configure: function() {
'cheese': function() {
return function() {
return function(event) { events.push(event); };
}
};
}
}
}
@@ -301,10 +397,10 @@ describe('../lib/log4js', function() {
'../lib/log4js',
{
requires: {
'./appenders/console': {
configure: function() {
'./appenders/console': function() {
return function() {
return function(event) { events.push(event); };
}
};
}
}
}

View File

@@ -11,8 +11,8 @@ describe('log level filter', function() {
var log4js_sandboxed = sandbox.require(
'../lib/log4js',
{ requires:
{ './appenders/console':
{ configure: function() { return function(evt) { events.push(evt); }; } }
{ './appenders/console': function() {
return function() { return function(evt) { events.push(evt); }; }; }
}
}
);

64
writing-appenders.md Normal file
View File

@@ -0,0 +1,64 @@
Writing Appenders For log4js
============================
Loading appenders
-----------------
log4js supports loading appender modules from outside its own code. The [log4js-gelf](http://github.com/nomiddlename/log4js-gelf), [log4js-smtp](http://github.com/nomiddlename/log4js-smtp), and [log4js-hookio](http://github.com/nomiddlename/log4js-hookio) appenders are examples of this. In the configuration for an appender, log4js will first attempt to `require` the module from `./lib/appenders/ + type` within log4js - if that fails, it will `require` just using the type. e.g.
log4js.configure({
appenders: {
"custom": { type: "log4js-gelf", hostname: "blah", port: 1234 }
},
categories: {
"default": { level: "debug", appenders: ["custom"] }
}
});
log4js will first attempt to `require('./appenders/' + log4js-gelf)`, this will fail. It will then attempt `require('log4js-gelf')`, which (assuming you have previously run `npm install log4js-gelf`) will pick up the gelf appender.
Writing your own custom appender
--------------------------------
This is easiest to explain with an example. Let's assume you want to write a [CouchDB](http://couchdb.apache.org) appender. CouchDB is a document database that you talk to via HTTP and JSON. Our log4js configuration is going to look something like this:
log4js.configure({
appenders: {
"couch": {
type: "log4js-couchdb",
url: "http://mycouchhost:5984",
db: "logs",
layout: {
type: "messagePassThrough"
}
}
},
categories: {
"default": { level: "debug", appenders: ["couch"] }
}
});
When processing this configuration, the first thing log4js will do is `require('log4js-couchdb')`. It expects this module to return a function that accepts two arguments
module.exports = function(layouts, levels) {
...
};
log4js will then call that function, passing in the `layouts` and `levels` sub-modules in case your appender might need to use them. Layouts contains functions which will format a log event as a string in various different ways. Levels contains the definitions of the log levels used by log4js - you might need this for mapping log4js levels to external definitions (the GELF appender does this). These are passed in so that appenders do not need to include a hard dependency on log4js (see below), and so that log4js does not need to expose these modules to the public API. The module function will only be called once per call to `log4js.configure`, even if there are multiple appenders of that type defined.
The module function should return another function, a configuration function, which will be called for each appender of that type defined in the config. That function should return an appender instance. For our CouchDB example, the calling process is roughly like this:
couchDbModule = require('log4js-couchdb');
appenderMaker = couchDbModule(layouts, levels);
appender = appenderMaker({
type: "log4js-couchdb",
url: "http://mycouchhost:5984",
db: "logs",
layout: {
type: "messagePassThrough"
}
}, appenderByName)
Note that in addition to our couchdb appender config, the appenderMaker function gets an extra argument: `appenderByName`, a function which returns an appender when passed its name. This is used by appenders that wrap other appenders. The `logLevelFilter` is an example of this use.
The `layout` portion of the config can be passed directly to `layouts.layout(config.layout)` to generate a layout function.
The appender function returned after processing your config should just take one argument: a log event. This function will be called for every log event that should be handled by your appender. In our case, with the config above, every log event of DEBUG level and above will be sent to our appender.