diff --git a/docs/appenders.md b/docs/appenders.md index 839f616..40fcaaa 100644 --- a/docs/appenders.md +++ b/docs/appenders.md @@ -40,6 +40,7 @@ The following appenders are included with log4js. Some require extra dependencie * [smtp](smtp.md) * [stderr](stderr.md) * [stdout](stdout.md) +* [rabbitmq](rabbitmq.md) ## Other Appenders diff --git a/docs/rabbitmq.md b/docs/rabbitmq.md new file mode 100644 index 0000000..259a0a1 --- /dev/null +++ b/docs/rabbitmq.md @@ -0,0 +1,41 @@ +# Rabbitmq Appender + +Push log events to a [Rabbitmq](https://www.rabbitmq.com/) MQ. You will need to include the [amqplib](https://www.npmjs.com/package/amqplib) package in your application's dependencies to use this appender. + +## Configuration + +* `type` - `rabbitmq` +* `host` - `string` (optional, defaults to `127.0.0.1`) - the location of the rabbitmq server +* `port` - `integer` (optional, defaults to `5672`) - the port the rabbitmq server is listening on +* `username` - `string` (optional, defaults to `guest`) - username to use when authenticating connection to rabbitmq +* `password` - `string` (optional, defaults to `guest`) - password to use when authenticating connection to rabbitmq +* `routing_key` - `string` (optional, defaults to `logstash`) - rabbitmq message's routing_key +* `durable` - `string` (optional, defaults to false) - will that RabbitMQ lose our queue. +* `exchange` - `string` - rabbitmq send message's exchange +* `mq_type` - `string` - rabbitmq message's mq_type +* `layout` - `object` (optional, defaults to `messagePassThroughLayout`) - the layout to use for log events (see [layouts](layouts.md)). + +The appender will use the Rabbitmq Routing model command to send the log event messages to the channel. + +## Example + +```javascript +log4js.configure({ + appenders: { + mq: { + type: 'rabbitmq', + host: '127.0.0.1', + port: 5672, + username: 'guest', + password: 'guest', + routing_key: 'logstash', + exchange: 'exchange_logs', + mq_type: 'direct', + durable: true + } + }, + categories: { default: { appenders: ['mq'], level: 'info' } } +}); +``` + +This configuration will push log messages to the rabbitmq on `127.0.0.1:5672`. diff --git a/examples/rabbitmq-appender.js b/examples/rabbitmq-appender.js new file mode 100755 index 0000000..ba2a6e1 --- /dev/null +++ b/examples/rabbitmq-appender.js @@ -0,0 +1,49 @@ +// Note that rabbitmq appender needs install amqplib to work. + +const log4js = require('../lib/log4js'); + +log4js.configure({ + appenders: { + out: { + type: 'console' + }, + file: { + type: 'dateFile', + filename: 'logs/log.txt', + pattern: 'yyyyMMdd', + alwaysIncludePattern: false + }, + mq: { + type: 'rabbitmq', + host: '127.0.0.1', + port: 5672, + username: 'guest', + password: 'guest', + routing_key: 'logstash', + exchange: 'exchange_logs', + mq_type: 'direct', + durable: true, + layout: { + type: 'pattern', + pattern: '%d{yyyy-MM-dd hh:mm:ss:SSS}#%p#%m' + } + } + }, + categories: { + default: { appenders: ['out'], level: 'info' }, + dateFile: { appenders: ['file'], level: 'info' }, + rabbitmq: { appenders: ['mq'], level: 'info' } + } +}); + +const log = log4js.getLogger('console'); +const logRabbitmq = log4js.getLogger('rabbitmq'); + +function doTheLogging(x) { + log.info('Logging something %d', x); + logRabbitmq.info('Logging something %d', x); +} + +for (let i = 0; i < 500; i += 1) { + doTheLogging(i); +} diff --git a/lib/appenders/rabbitmq.js b/lib/appenders/rabbitmq.js new file mode 100644 index 0000000..e174727 --- /dev/null +++ b/lib/appenders/rabbitmq.js @@ -0,0 +1,61 @@ +'use strict'; + +const amqplib = require('amqplib'); + +function rabbitmqAppender(config, layout) { + const host = config.host || '127.0.0.1'; + const port = config.port || 5672; + const username = config.username || 'guest'; + const password = config.password || 'guest'; + const exchange = config.exchange || ''; + const type = config.mq_type || ''; + const durable = config.durable || false; + const routingKey = config.routing_key || 'logstash'; + const con = { + protocol: 'amqp', + hostname: host, + port: port, + username: username, + password: password, + locale: 'en_US', + frameMax: 0, + heartbeat: 0, + vhost: '/', + routing_key: routingKey, + exchange: exchange, + mq_type: type, + durable: durable, + }; + const clientconn = amqplib.connect(con); + clientconn.publish = amqplib.connect(con).publish ? amqplib.connect(con).publish : (client, message) => { + client.then((conn) => { + const rn = conn.createChannel().then((ch) => { + const ok = ch.assertExchange(exchange, type, { durable: durable }); + return ok.then(() => { + ch.publish(exchange, routingKey, Buffer.from(message)); + return ch.close(); + }); + }); + return rn; + }).catch(console.error); + }; + function log(loggingEvent) { + const message = layout(loggingEvent); + clientconn.publish(clientconn, message); + } + log.shutdown = function () { + clientconn.close(); + }; + return log; +} + +function configure(config, layouts) { + let layout = layouts.messagePassThroughLayout; + if (config.layout) { + layout = layouts.layout(config.layout.type, config.layout); + } + + return rabbitmqAppender(config, layout); +} + +module.exports.configure = configure; diff --git a/package.json b/package.json index 9f49f53..27759cc 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,8 @@ "nodemailer": "^2.5.0", "redis": "^2.7.1", "slack-node": "~0.2.0", - "axios": "^0.15.3" + "axios": "^0.15.3", + "amqplib": "^0.5.2" }, "browser": { "os": false diff --git a/test/tap/rabbitmqAppender-test.js b/test/tap/rabbitmqAppender-test.js new file mode 100644 index 0000000..85b97b4 --- /dev/null +++ b/test/tap/rabbitmqAppender-test.js @@ -0,0 +1,119 @@ +'use strict'; + +const test = require('tap').test; +const sandbox = require('sandboxed-module'); + +function setupLogging(category, options) { + const fakeRabbitmq = { + msgs: [], + connect: function (conn) { + this.port = conn.port; + this.host = conn.hostname; + this.username = conn.username; + this.password = conn.password; + this.routing_key = conn.routing_key; + this.exchange = conn.exchange; + this.mq_type = conn.mq_type; + this.durable = conn.durable; + return { + publish: function (client, message) { + fakeRabbitmq.msgs.push(message); + } + }; + } + }; + + const fakeConsole = { + errors: [], + error: function (msg) { + this.errors.push(msg); + } + }; + + const log4js = sandbox.require('../../lib/log4js', { + requires: { + amqplib: fakeRabbitmq, + }, + globals: { + console: fakeConsole + } + }); + log4js.configure({ + appenders: { rabbitmq: options }, + categories: { default: { appenders: ['rabbitmq'], level: 'trace' } } + }); + + return { + logger: log4js.getLogger(category), + fakeRabbitmq: fakeRabbitmq, + fakeConsole: fakeConsole + }; +} + +test('log4js rabbitmqAppender', (batch) => { + batch.test('rabbitmq setup', (t) => { + const result = setupLogging('rabbitmq setup', { + host: '123.123.123.123', + port: 5672, + username: 'guest', + password: 'guest', + routing_key: 'logstash', + exchange: 'exchange_logs', + mq_type: 'direct', + durable: true, + type: 'rabbitmq', + layout: { + type: 'pattern', + pattern: 'cheese %m' + } + }); + + result.logger.info('Log event #1'); + + t.test('rabbitmq credentials should match', (assert) => { + assert.equal(result.fakeRabbitmq.host, '123.123.123.123'); + assert.equal(result.fakeRabbitmq.port, 5672); + assert.equal(result.fakeRabbitmq.username, 'guest'); + assert.equal(result.fakeRabbitmq.password, 'guest'); + assert.equal(result.fakeRabbitmq.routing_key, 'logstash'); + assert.equal(result.fakeRabbitmq.exchange, 'exchange_logs'); + assert.equal(result.fakeRabbitmq.mq_type, 'direct'); + assert.equal(result.fakeRabbitmq.durable, true); + assert.equal(result.fakeRabbitmq.msgs.length, 1, 'should be one message only'); + assert.equal(result.fakeRabbitmq.msgs[0], 'cheese Log event #1'); + assert.end(); + }); + + t.end(); + }); + + batch.test('default values', (t) => { + const setup = setupLogging('defaults', { + type: 'rabbitmq' + }); + + setup.logger.info('just testing'); + + t.test('should use localhost', (assert) => { + assert.equal(setup.fakeRabbitmq.host, '127.0.0.1'); + assert.equal(setup.fakeRabbitmq.port, 5672); + assert.equal(setup.fakeRabbitmq.username, 'guest'); + assert.equal(setup.fakeRabbitmq.password, 'guest'); + assert.equal(setup.fakeRabbitmq.exchange, ''); + assert.equal(setup.fakeRabbitmq.mq_type, ''); + assert.equal(setup.fakeRabbitmq.durable, false); + assert.equal(setup.fakeRabbitmq.routing_key, 'logstash'); + assert.end(); + }); + + t.test('should use message pass through layout', (assert) => { + assert.equal(setup.fakeRabbitmq.msgs.length, 1); + assert.equal(setup.fakeRabbitmq.msgs[0], 'just testing'); + assert.end(); + }); + + t.end(); + }); + + batch.end(); +});