diff --git a/README.md b/README.md index 554a0af3..a1c738af 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,33 @@ var server = app.listen(3000); } ``` +* **option.logLevel**: string, ['debug', 'info', 'warn', 'error', 'silent']. Default: 'info' + +* **option.logProvider**: function, modify or replace log provider. Default: `console`. + ```javascript + // simple replace + function logProvider(provider) { + // replace the default console log provider. + return require('winston'); + } + ``` + + ```javascript + // verbose replacement + function logProvider(provider) { + var logger = new (require('winston').Logger)(); + + var myCustomProvider = { + log: logger.log, + debug: logger.debug, + info: logger.info, + warn: logger.warn, + error: logger.error + } + return myCustomProvider; + } + ``` + * **option.onError**: function, subscribe to http-proxy's error event for custom error handling. ```javascript function onError(err, req, res) { @@ -222,10 +249,10 @@ $ node examples/connect ``` Or just explore the proxy examples' sources: - * `examples/connect` - [connect proxy example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/connect/index.js) - * `examples/express` - [express proxy example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/express/index.js) - * `examples/browser-sync` - [browser-sync proxy example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/browser-sync/index.js) - * `examples/websocket` - [websocket proxy example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/websocket/index.js) with express +* `examples/connect` - [connect proxy example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/connect/index.js) +* `examples/express` - [express proxy example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/express/index.js) +* `examples/browser-sync` - [browser-sync proxy example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/browser-sync/index.js) +* `examples/websocket` - [websocket proxy example](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/websocket/index.js) with express ## Compatible servers diff --git a/index.js b/index.js index ccfabe34..c42a1206 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ var handlers = require('./lib/handlers'); var contextMatcher = require('./lib/context-matcher'); var PathRewriter = require('./lib/path-rewriter'); var ProxyTable = require('./lib/proxy-table'); +var logger = require('./lib/logger').getInstance(); var httpProxyMiddleware = function (context, opts) { var isWsUpgradeListened = false; @@ -13,7 +14,7 @@ var httpProxyMiddleware = function (context, opts) { // create proxy var proxy = httpProxy.createProxyServer(proxyOptions); - console.log('[HPM] Proxy created:', config.context, ' -> ', proxyOptions.target); + logger.info('[HPM] Proxy created:', config.context, ' -> ', proxyOptions.target); var pathRewriter = PathRewriter.create(proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided @@ -31,7 +32,7 @@ var httpProxyMiddleware = function (context, opts) { // Listen for the `close` event on `proxy`. proxy.on('close', function (req, socket, head) { // view disconnected websocket connections - console.log('[HPM] Client disconnected'); + logger.info('[HPM] Client disconnected'); }); // https://github.com/chimurai/http-proxy-middleware/issues/19 @@ -50,15 +51,19 @@ var httpProxyMiddleware = function (context, opts) { } if (contextMatcher.match(config.context, req.url)) { + logger.debug('[HPM] Context match: "%s" -> "%s"', config.context, req.url); + // handle option.pathRewrite if (pathRewriter) { req.url = pathRewriter(req.url); } - if (proxyOptions.proxyTable) { // change option.target when proxyTable present. - proxy.web(req, res, ProxyTable.createProxyOptions(req, proxyOptions)); + var altOpts = ProxyTable.createProxyOptions(req, proxyOptions); + logger.debug('[HPM] Proxying "%s": "%s" -> "%s"', req.url, req.hostname, altOpts.target); + proxy.web(req, res, altOpts); } else { + logger.debug('[HPM] Proxying "%s": "%s" -> "%s"', req.url, req.hostname, proxyOptions.target); proxy.web(req, res); } @@ -87,7 +92,7 @@ var httpProxyMiddleware = function (context, opts) { req.url = pathRewriter(req.url); } proxy.ws(req, socket, head); - console.log('[HPM] Upgrading to WebSocket'); + logger.info('[HPM] Upgrading to WebSocket'); } } @@ -101,7 +106,7 @@ var httpProxyMiddleware = function (context, opts) { function proxyErrorLogger (err, req, res) { var targetUri = proxyOptions.target.host + req.url; - console.log('[HPM] Proxy error:', err.code, targetUri); + logger.error('[HPM] Proxy error: %s. %s -> "%s"', err.code, req.hostname, targetUri); } }; diff --git a/lib/config-factory.js b/lib/config-factory.js index 4802d19f..a0d169e1 100644 --- a/lib/config-factory.js +++ b/lib/config-factory.js @@ -1,5 +1,6 @@ -var _ = require('lodash'); -var url = require('url'); +var _ = require('lodash'); +var url = require('url'); +var logger = require('./logger').getInstance(); module.exports = { createConfig : createConfig @@ -30,13 +31,15 @@ function createConfig (context, opts) { config.options = _.assign(config.options, opts); } - // Legacy option.proxyHost - config.options = mapLegacyProxyHostOption(config.options); + configureLogger(config.options); if (!config.options.target) { throw new Error('[HPM] Missing "target" option. Example: {target: "http://www.example.org"}'); } + // Legacy option.proxyHost + config.options = mapLegacyProxyHostOption(config.options); + return config; }; @@ -49,11 +52,11 @@ function isShortHand (context) { function mapLegacyProxyHostOption (options) { // set options.headers.host when option.proxyHost is provided if (options.proxyHost) { - console.log('*************************************'); - console.log('[HPM] Deprecated "option.proxyHost"'); - console.log(' Use "option.changeOrigin" or "option.headers.host" instead'); - console.log(' "option.proxyHost" will be removed in future release.'); - console.log('*************************************'); + logger.warn('*************************************'); + logger.warn('[HPM] Deprecated "option.proxyHost"'); + logger.warn(' Use "option.changeOrigin" or "option.headers.host" instead'); + logger.warn(' "option.proxyHost" will be removed in future release.'); + logger.warn('*************************************'); options.headers = options.headers || {}; options.headers.host = options.proxyHost; @@ -61,3 +64,13 @@ function mapLegacyProxyHostOption (options) { return options; } + +function configureLogger (options) { + if (options.logLevel) { + logger.setLevel(options.logLevel); + } + + if (options.logProvider) { + logger.setProvider(options.logProvider); + } +} diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 00000000..be5975c1 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,143 @@ +var util = require('util'); +var _ = require('lodash'); + +var loggerInstance; + +var defaultProvider = { + log : console.log, + debug : console.log, // use .log(); since console does not have .debug() + info : console.info, + warn : console.warn, + error : console.error +} + +// log level 'weight' +var LEVELS = { + debug : 10, + info : 20, + warn : 30, + error : 50, + silent : 80 +} + +module.exports = { + // singleton + getInstance : function () { + if (!loggerInstance) { + loggerInstance = new Logger(); + } + + return loggerInstance; + } +}; + +function Logger () { + var logLevel; + var provider; + + var api = { + log : log, + debug : debug, + info : info, + warn : warn, + error : error, + setLevel : function (v) { + if (isValidLevel(v)) { + logLevel = v; + } + }, + setProvider : function (fn) { + if (fn && isValidProvider(fn)) { + provider = fn(defaultProvider); + } + } + }; + + + init(); + + return api; + + function init () { + api.setLevel('info'); + api.setProvider(function () { + return defaultProvider; + }); + } + + // log will log messages, regardless of logLevels + function log () { + provider.log(_interpolate.apply(null, arguments)); + } + + function debug () { + if (_showLevel('debug')) { + provider.debug(_interpolate.apply(null, arguments)); + } + } + + function info () { + if (_showLevel('info')) { + provider.info(_interpolate.apply(null, arguments)); + } + } + + function warn () { + if (_showLevel('warn')) { + provider.warn(_interpolate.apply(null, arguments)); + } + } + + function error () { + if (_showLevel('error')) { + provider.error(_interpolate.apply(null, arguments)); + } + } + + /** + * Decide to log or not to log, based on the log levels 'weight' + * @param {String} showLevel [debug, info, warn, error, silent] + * @return {Boolean} + */ + function _showLevel (showLevel) { + var result = false; + var currentLogLevel = LEVELS[logLevel]; + + if (currentLogLevel && (currentLogLevel <= LEVELS[showLevel])) { + result = true; + } + + return result; + } + + // make sure logged messages and its data are return interpolated + // make it possible for additional log data, such date/time or custom prefix. + function _interpolate () { + var fn = _.spread(util.format); + var result = fn(_.slice(arguments)); + + return result; + } + + function isValidProvider (fnProvider) { + var result = true; + + if (fnProvider && !_.isFunction(fnProvider)) { + throw new Error('[HPM] Log provider config error. Expecting a function.'); + } + + return result; + } + + function isValidLevel (levelName) { + var validLevels = _.keys(LEVELS); + var isValid = _.includes(validLevels, levelName); + + if (!isValid) { + throw new Error('[HPM] Log level error. Invalid logLevel.'); + } + + return isValid; + } +} + diff --git a/lib/path-rewriter.js b/lib/path-rewriter.js index 4d382ca3..4b73db8f 100644 --- a/lib/path-rewriter.js +++ b/lib/path-rewriter.js @@ -1,4 +1,5 @@ -var _ = require('lodash'); +var _ = require('lodash'); +var logger = require('./logger').getInstance(); module.exports = { create : createPathRewriter @@ -23,6 +24,7 @@ function createPathRewriter (config) { _.forEach(rules,function (rule) { if (rule.regex.test(path)) { result = result.replace(rule.regex, rule.value); + logger.debug('[HPM] Rewriting path from "%s" to "%s"', path, result); return false; } }); @@ -43,7 +45,7 @@ function parsePathRewriteRules (config) { regex : new RegExp(key), value : config[key] }); - console.log('[HPM] Proxy rewrite rule created: "' + key + '" -> "' + config[key] + '"'); + logger.info('[HPM] Proxy rewrite rule created: "%s" -> "%s"', key, config[key]); }); } diff --git a/lib/proxy-table.js b/lib/proxy-table.js index 3d43e756..6fe78abd 100644 --- a/lib/proxy-table.js +++ b/lib/proxy-table.js @@ -1,5 +1,6 @@ -var url = require('url'); -var _ = require('lodash'); +var url = require('url'); +var _ = require('lodash'); +var logger = require('./logger.js').getInstance(); module.exports = { createProxyOptions : createProxyOptions @@ -12,6 +13,7 @@ function createProxyOptions (req, config) { if (proxyTable) { var newTarget = getTargetFromProxyTable(req, proxyTable); if (newTarget) { + logger.debug('[HPM] proxyTable new target: %s -> "%s"', config.target, newTarget); result = _.assign(result, {target : newTarget}); // override option.target } } @@ -31,12 +33,14 @@ function getTargetFromProxyTable (req, proxyTable) { if (hostAndPath.indexOf(key) > -1) { // match 'localhost:3000/api' result = proxyTable[key]; + logger.debug('[HPM] proxyTable match: %s -> "%s"', hostAndPath, result); return false; } } else { if (key === host) { // match 'localhost:3000' result = proxyTable[key]; + logger.debug('[HPM] proxyTable match: %s -> "%s"', host, result); return false; } diff --git a/test/http-proxy-middleware.spec.js b/test/http-proxy-middleware.spec.js index 53ee8045..7fd0400e 100644 --- a/test/http-proxy-middleware.spec.js +++ b/test/http-proxy-middleware.spec.js @@ -527,6 +527,51 @@ describe('http-proxy-middleware in actual server', function () { }); + + describe('option.logLevel & option.logProvider', function () { + var proxyServer, targetServer; + var responseBody; + var logMessage; + + beforeEach(function (done) { + var customLogger = function (message) { + logMessage = message; + } + + var mw_proxy = proxyMiddleware('http://localhost:8000/api', { + logLevel: 'info', + logProvider: function (provider) { + provider.debug = customLogger; + provider.info = customLogger; + return provider; + } + }); + var mw_target = function (req, res, next) { + res.write(req.url); // respond with req.url + res.end(); + }; + + proxyServer = createServer(3000, mw_proxy); + targetServer = createServer(8000, mw_target); + + http.get('http://localhost:3000/api/foo/bar', function (res) { + res.on('data', function (chunk) { + responseBody = chunk.toString(); + done(); + }); + }); + }); + + afterEach(function () { + proxyServer.close(); + targetServer.close(); + }); + + it('should have logged messages', function () { + expect(logMessage).not.to.equal(undefined); + }); + }); + }); @@ -534,7 +579,6 @@ function createServer (portNumber, middleware, path) { var app = express(); if (middleware, path) { - console.log('pathpathpathpathpathpathpath: ', path); app.use(path, middleware); } else if (middleware) { diff --git a/test/logger.spec.js b/test/logger.spec.js new file mode 100644 index 00000000..147e3644 --- /dev/null +++ b/test/logger.spec.js @@ -0,0 +1,216 @@ +var expect = require('chai').expect; +var Logger = require('../lib/logger'); + +describe('Logger', function () { + var logger; + var logMessage, debugMessage, infoMessage, warnMessage, errorMessage; + + beforeEach(function () { + logMessage = undefined; + debugMessage = undefined; + infoMessage = undefined; + warnMessage = undefined; + errorMessage = undefined; + }); + + beforeEach(function () { + logger = Logger.getInstance(); + }); + + beforeEach(function () { + logger.setProvider(function (provider) { + provider.log = function (message) {logMessage = message}; + provider.debug = function (message) {debugMessage = message}; + provider.info = function (message) {infoMessage = message}; + provider.warn = function (message) {warnMessage = message}; + provider.error = function (message) {errorMessage = message}; + + return provider; + }); + }); + + describe('logging with different levels', function () { + beforeEach(function () { + logger.log('log'); + logger.debug('debug'); + logger.info('info'); + logger.warn('warn'); + logger.error('error'); + }); + + describe('level: debug', function () { + beforeEach(function () { + logger.setLevel('debug'); + }); + + it('should log .log() messages', function () { + expect(logMessage).to.equal('log'); + }); + it('should log .debug() messages', function () { + expect(debugMessage).to.equal('debug'); + }); + it('should log .info() messages', function () { + expect(infoMessage).to.equal('info'); + }); + it('should log .warn() messages', function () { + expect(warnMessage).to.equal('warn'); + }); + it('should log .error() messages', function () { + expect(errorMessage).to.equal('error'); + }); + }); + + describe('level: info', function () { + beforeEach(function () { + logger.setLevel('info'); + }); + + it('should log .log() messages', function () { + expect(logMessage).to.equal('log'); + }); + it('should not log .debug() messages', function () { + expect(debugMessage).to.equal(undefined); + }); + it('should log .info() messages', function () { + expect(infoMessage).to.equal('info'); + }); + it('should log .warn() messages', function () { + expect(warnMessage).to.equal('warn'); + }); + it('should log .error() messages', function () { + expect(errorMessage).to.equal('error'); + }); + }); + + describe('level: warn', function () { + beforeEach(function () { + logger.setLevel('warn'); + }); + + it('should log .log() messages', function () { + expect(logMessage).to.equal('log'); + }); + it('should not log .debug() messages', function () { + expect(debugMessage).to.equal(undefined); + }); + it('should not log .info() messages', function () { + expect(infoMessage).to.equal(undefined); + }); + it('should log .warn() messages', function () { + expect(warnMessage).to.equal('warn'); + }); + it('should log .error() messages', function () { + expect(errorMessage).to.equal('error'); + }); + }); + + describe('level: error', function () { + beforeEach(function () { + logger.setLevel('error'); + }); + + it('should log .log() messages', function () { + expect(logMessage).to.equal('log'); + }); + it('should not log .debug() messages', function () { + expect(debugMessage).to.equal(undefined); + }); + it('should not log .info() messages', function () { + expect(infoMessage).to.equal(undefined); + }); + it('should log .warn() messages', function () { + expect(warnMessage).to.equal(undefined); + }); + it('should log .error() messages', function () { + expect(errorMessage).to.equal('error'); + }); + }); + + describe('level: silent', function () { + beforeEach(function () { + logger.setLevel('silent'); + }); + + it('should log .log() messages', function () { + expect(logMessage).to.equal('log'); + }); + it('should not log .debug() messages', function () { + expect(debugMessage).to.equal(undefined); + }); + it('should not log .info() messages', function () { + expect(infoMessage).to.equal(undefined); + }); + it('should not log .warn() messages', function () { + expect(warnMessage).to.equal(undefined); + }); + it('should not log .error() messages', function () { + expect(errorMessage).to.equal(undefined); + }); + }); + + describe('Interpolation', function () { + // make sure all messages are logged + beforeEach(function () { + logger.setLevel('debug'); + }); + + beforeEach(function () { + logger.log('log %s %s', 123, 456); + logger.debug('debug %s %s', 123, 456); + logger.info('info %s %s', 123, 456); + logger.warn('warn %s %s', 123, 456); + logger.error('error %s %s', 123, 456); + }); + + it('should interpolate .log() messages', function () { + expect(logMessage).to.equal('log 123 456'); + }); + it('should interpolate .debug() messages', function () { + expect(debugMessage).to.equal('debug 123 456'); + }); + it('should interpolate .info() messages', function () { + expect(infoMessage).to.equal('info 123 456'); + }); + it('should interpolate .warn() messages', function () { + expect(warnMessage).to.equal('warn 123 456'); + }); + it('should interpolate .error() messages', function () { + expect(errorMessage).to.equal('error 123 456'); + }); + }); + }); + + describe('Erroneous usage.', function () { + var fn; + + describe('Log provider is not a function', function () { + beforeEach(function () { + fn = function () { + logger.setProvider({}); + }; + }); + + it('should throw an error', function () { + expect(fn).to.throw(Error); + }); + }); + + describe('Invalid logLevel', function () { + beforeEach(function () { + fn = function () { + logger.setLevel('foo'); + }; + }); + + it('should throw an error', function () { + expect(fn).to.throw(Error); + }); + }); + + }); + + + +}); + +