From 59f7e0af3c0611655e4b20091c6d290fa3f9f289 Mon Sep 17 00:00:00 2001 From: Daniel Bell Date: Thu, 7 Apr 2011 10:19:18 +1000 Subject: [PATCH] Added connect/express logger. --- README.md | 29 +++++++- example-connect-logger.js | 16 +++++ lib/connect-logger.js | 139 ++++++++++++++++++++++++++++++++++++ lib/log4js.js | 7 +- package.json | 4 +- test/test-connect-logger.js | 138 +++++++++++++++++++++++++++++++++++ 6 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 example-connect-logger.js create mode 100644 lib/connect-logger.js create mode 100644 test/test-connect-logger.js diff --git a/README.md b/README.md index 29a7e59c..1e46ffd8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ npm install log4js ## tests -Tests now use [vows](http://vowsjs.org), run with `vows test/logging.js`. +Tests now use [vows](http://vowsjs.org), run with `vows test/*.js`. ## usage @@ -60,6 +60,33 @@ patternLayout has no tests. This is mainly because I haven't found a use for it and am not entirely sure what it was supposed to do. It is more-or-less intact from the original log4js. +## connect/express logger + +A connect/express logger has been added to log4js. This allows connect/express servers to log using log4js. See example-connect-logger.js. + + var log4js = require('./lib/log4js')(); + log4js.addAppender(log4js.consoleAppender()); + log4js.addAppender(log4js.fileAppender('cheese.log'), 'cheese'); + + var logger = log4js.getLogger('cheese'); + + logger.setLevel('INFO'); + + var app = require('express').createServer(); + app.configure(function() { + app.use(log4js.connectLogger(logger, { level: log4js.levels.INFO })); + }); + app.get('/', function(req,res) { + res.send('hello world'); + }); + app.listen(5000); + +The options object that is passed to log4js.connectLogger supports a format string the same as the connect/express logger. For example: + + app.configure(function() { + app.use(log4js.connectLogger(logger, { level: log4js.levels.INFO, format: ':method :url' })); + }); + ## author (of this node version) Gareth Jones (csausdev - gareth.jones@sensis.com.au) diff --git a/example-connect-logger.js b/example-connect-logger.js new file mode 100644 index 00000000..55b05723 --- /dev/null +++ b/example-connect-logger.js @@ -0,0 +1,16 @@ +var log4js = require('./lib/log4js')(); +log4js.addAppender(log4js.consoleAppender()); +log4js.addAppender(log4js.fileAppender('cheese.log'), 'cheese'); + +var logger = log4js.getLogger('cheese'); + +logger.setLevel('INFO'); + +var app = require('express').createServer(); +app.configure(function() { + app.use(log4js.connectLogger(logger, { level: log4js.levels.INFO })); +}); +app.get('/', function(req,res) { + res.send('hello world'); +}); +app.listen(5000); diff --git a/lib/connect-logger.js b/lib/connect-logger.js new file mode 100644 index 00000000..75946014 --- /dev/null +++ b/lib/connect-logger.js @@ -0,0 +1,139 @@ +/** + * Log requests with the given `options` or a `format` string. + * + * Options: + * + * - `format` Format string, see below for tokens + * - `level` A log4js levels instance. + * + * Tokens: + * + * - `:req[header]` ex: `:req[Accept]` + * - `:res[header]` ex: `:res[Content-Length]` + * - `:http-version` + * - `:response-time` + * - `:remote-addr` + * - `:date` + * - `:method` + * - `:url` + * - `:referrer` + * - `:user-agent` + * - `:status` + * + * @param {String|Function|Object} format or options + * @return {Function} + * @api public + */ + +module.exports = function(log4js_module) { + var log4js = log4js_module; + + function getLogger(logger4js, options) { + if ('object' == typeof options) { + options = options || {}; + } else if (options) { + options = { format: options }; + } else { + options = {}; + } + + var thislogger = logger4js; + var level = options.level || log4js.levels.TRACE; + var fmt = options.format; + + return function logger(req, res, next) { + + // mount safety + if (req._logging) return next(); + + if (thislogger.isLevelEnabled(level)) { + + var start = +new Date + , statusCode + , writeHead = res.writeHead + , end = res.end + , url = req.originalUrl; + + // flag as logging + req._logging = true; + + // proxy for statusCode. + res.writeHead = function(code, headers){ + res.writeHead = writeHead; + res.writeHead(code, headers); + res.__statusCode = statusCode = code; + res.__headers = headers || {}; + }; + + // proxy end to output a line to the provided logger. + if (fmt) { + res.end = function(chunk, encoding) { + res.end = end; + res.end(chunk, encoding); + res.responseTime = +new Date - start; + if ('function' == typeof fmt) { + var line = fmt(req, res, function(str){ return format(str, req, res); }); + if (line) thislogger.log(level, line); + } else { + thislogger.log(level, format(fmt, req, res)); + } + }; + } else { + res.end = function(chunk, encoding) { + var contentLength = (res._headers && res._headers['content-length']) + || (res.__headers && res.__headers['Content-Length']) + || '-'; + + res.end = end; + res.end(chunk, encoding); + + thislogger.log(level, + (req.socket && (req.socket.remoteAddress || (req.socket.socket && req.socket.socket.remoteAddress))) + + ' - - "' + req.method + ' ' + url + + ' HTTP/' + req.httpVersionMajor + '.' + req.httpVersionMinor + '" ' + + (statusCode || res.statusCode) + ' ' + contentLength + ' "' + + (req.headers['referer'] || req.headers['referrer'] || '') + '" "' + + (req.headers['user-agent'] || '') + '"'); + }; + } + + next(); + }; + }; + + /** + * Return formatted log line. + * + * @param {String} str + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @return {String} + * @api private + */ + + function format(str, req, res) { + return str + .replace(':url', req.originalUrl) + .replace(':method', req.method) + .replace(':status', res.__statusCode || res.statusCode) + .replace(':response-time', res.responseTime) + .replace(':date', new Date().toUTCString()) + .replace(':referrer', req.headers['referer'] || req.headers['referrer'] || '') + .replace(':http-version', req.httpVersionMajor + '.' + req.httpVersionMinor) + .replace(':remote-addr', req.socket && (req.socket.remoteAddress || (req.socket.socket && req.socket.socket.remoteAddress))) + .replace(':user-agent', req.headers['user-agent'] || '') + .replace(/:req\[([^\]]+)\]/g, function(_, field){ return req.headers[field.toLowerCase()]; }) + .replace(/:res\[([^\]]+)\]/g, function(_, field){ + return res._headers + ? (res._headers[field.toLowerCase()] || res.__headers[field]) + : (res.__headers && res.__headers[field]); + }); + } + + } + + return { + connectLogger: getLogger + }; + +} \ No newline at end of file diff --git a/lib/log4js.js b/lib/log4js.js index 3785b846..fa676d28 100644 --- a/lib/log4js.js +++ b/lib/log4js.js @@ -649,7 +649,7 @@ module.exports = function (fileSystem, standardOutput, configPaths) { replaceConsole(getLogger("console")); } - return { + var thismodule = { getLogger: getLogger, getDefaultLogger: getDefaultLogger, @@ -667,8 +667,11 @@ module.exports = function (fileSystem, standardOutput, configPaths) { messagePassThroughLayout: messagePassThroughLayout, patternLayout: patternLayout, colouredLayout: colouredLayout, - coloredLayout: colouredLayout + coloredLayout: colouredLayout, }; + thismodule.connectLogger = require('./connect-logger')(thismodule).connectLogger; + + return thismodule; } diff --git a/package.json b/package.json index 04df4dfd..91af8d8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "log4js", - "version": "0.2.4", + "version": "0.2.5", "description": "Port of Log4js to work with node.", "keywords": [ "logging", @@ -15,7 +15,7 @@ }, "engines": [ "node >=0.1.100" ], "scripts": { - "test": "vows test/logging.js" + "test": "vows test/*.js" }, "directories": { "test": "test", diff --git a/test/test-connect-logger.js b/test/test-connect-logger.js new file mode 100644 index 00000000..505ae19b --- /dev/null +++ b/test/test-connect-logger.js @@ -0,0 +1,138 @@ +var vows = require('vows'), +assert = require('assert'); + +var mockLog4js = { + levels: { + TRACE: 0, + DEBUG: 1, + INFO: 2, + WARN: 3, + ERROR: 4, + FATAL: 5 + } +} + +function MockLogger() { + + var that = this; + this.messages = []; + + this.log = function(level, message, exception) { + that.messages.push({ level: level, message: message }); + }; + + this.isLevelEnabled = function(level) { + return (level >= that.level); + }; + + this.level = mockLog4js.levels.TRACE; + +} + +function MockRequest(remoteAddr, method, originalUrl) { + + this.socket = { remoteAddress: remoteAddr }; + this.originalUrl = originalUrl; + this.method = method; + this.httpVersionMajor = '5'; + this.httpVersionMinor = '0'; + this.headers = {} + +} + +function MockResponse(statusCode) { + + this.statusCode = statusCode; + + this.end = function(chunk, encoding) { + + } + +} + +vows.describe('log4js connect logger').addBatch({ + 'getConnectLoggerModule': { + topic: function() { + var clm = require('../lib/connect-logger')(mockLog4js); + return clm; + }, + + 'should return a "connect logger" factory' : function(clm) { + assert.isObject(clm); + }, + + 'take a log4js logger and return a "connect logger"' : { + topic: function(clm) { + var ml = new MockLogger(); + var cl = clm.connectLogger(ml); + return cl; + }, + + 'should return a "connect logger"': function(cl) { + assert.isFunction(cl); + } + }, + + 'log events' : { + topic: function(clm) { + var ml = new MockLogger(); + var cl = clm.connectLogger(ml); + var req = new MockRequest('my.remote.addr', 'GET', 'http://url'); + var res = new MockResponse(200); + cl(req, res, function() { }); + res.end('chunk', 'encoding'); + return ml.messages; + }, + + 'check message': function(messages) { + assert.isArray(messages); + assert.length(messages, 1); + assert.equal(messages[0].level, mockLog4js.levels.TRACE); + assert.include(messages[0].message, 'GET'); + assert.include(messages[0].message, 'http://url'); + assert.include(messages[0].message, 'my.remote.addr'); + assert.include(messages[0].message, '200'); + } + }, + + 'log events with level below logging level' : { + topic: function(clm) { + var ml = new MockLogger(); + ml.level = mockLog4js.levels.FATAL; + var cl = clm.connectLogger(ml); + var req = new MockRequest('my.remote.addr', 'GET', 'http://url'); + var res = new MockResponse(200); + cl(req, res, function() { }); + res.end('chunk', 'encoding'); + return ml.messages; + }, + + 'check message': function(messages) { + assert.isArray(messages); + assert.isEmpty(messages); + } + }, + + 'log events with non-default level and custom format' : { + topic: function(clm) { + var ml = new MockLogger(); + ml.level = mockLog4js.levels.INFO; + var cl = clm.connectLogger(ml, { level: mockLog4js.levels.INFO, format: ':method :url' } ); + var req = new MockRequest('my.remote.addr', 'GET', 'http://url'); + var res = new MockResponse(200); + cl(req, res, function() { }); + res.end('chunk', 'encoding'); + return ml.messages; + }, + + 'check message': function(messages) { + assert.isArray(messages); + assert.length(messages, 1); + assert.equal(messages[0].level, mockLog4js.levels.INFO); + assert.equal(messages[0].message, 'GET http://url'); + } + } + + } + +}).export(module);