diff --git a/README.md b/README.md index b9b055c..71fc14b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Out of the box it supports the following features: * GELF appender * hook.io appender * Loggly appender +* Logstash UDP appender * multiprocess appender (useful when you've got worker processes) * a logger for connect/express servers * configurable log message layout/patterns diff --git a/examples/logstashUDP.js b/examples/logstashUDP.js new file mode 100644 index 0000000..871f157 --- /dev/null +++ b/examples/logstashUDP.js @@ -0,0 +1,39 @@ +var log4js = require('../lib/log4js'); + +/* + Sample logstash config: + udp { + codec => json + port => 10001 + queue_size => 2 + workers => 2 + type => myAppType + } +*/ + +log4js.configure({ + "appenders": [ + { + type: "console", + category: "myLogger" + }, + { + "host": "127.0.0.1", + "port": 10001, + "type": "logstashUDP", + "logType": "myAppType", // Optional, defaults to 'category' + "fields": { // Optional, will be added to the 'fields' object in logstash + "field1": "value1", + "field2": "value2" + }, + "layout": { + "type": "pattern", + "pattern": "%m" + }, + "category": "myLogger" + } + ] +}); + +var logger = log4js.getLogger("myLogger"); +logger.info("Test log message %s", "arg1", "arg2"); diff --git a/lib/appenders/logstashUDP.js b/lib/appenders/logstashUDP.js new file mode 100644 index 0000000..9ac5579 --- /dev/null +++ b/lib/appenders/logstashUDP.js @@ -0,0 +1,50 @@ +"use strict"; +var layouts = require('../layouts') +, dgram = require('dgram') +, util = require('util'); + +function logstashUDP (config, layout) { + var udp = dgram.createSocket('udp4'); + var type = config.logType ? config.logType : config.category; + layout = layout || layouts.colouredLayout; + if(!config.fields) { + config.fields = {}; + } + return function(loggingEvent) { + var logMessage = layout(loggingEvent); + var fields = {}; + for(var i in config.fields) { + fields[i] = config.fields[i]; + } + fields['level'] = loggingEvent.level.levelStr; + var logObject = { + '@timestamp': (new Date(loggingEvent.startTime)).toISOString(), + type: type, + message: logMessage, + fields: fields + }; + sendLog(udp, config.host, config.port, logObject); + }; +} + +function sendLog(udp, host, port, logObject) { + var buffer = new Buffer(JSON.stringify(logObject)); + udp.send(buffer, 0, buffer.length, port, host, function(err, bytes) { + if(err) { + console.error( + "log4js.logstashUDP - %s:%p Error: %s", host, port, util.inspect(err) + ); + } + }); +} + +function configure(config) { + var layout; + if (config.layout) { + layout = layouts.layout(config.layout.type, config.layout); + } + return logstashUDP(config, layout); +} + +exports.appender = logstashUDP; +exports.configure = configure; diff --git a/test/logstashUDP-test.js b/test/logstashUDP-test.js new file mode 100644 index 0000000..37f2b47 --- /dev/null +++ b/test/logstashUDP-test.js @@ -0,0 +1,106 @@ +"use strict"; +var sys = require("sys"); +var vows = require('vows') +, assert = require('assert') +, log4js = require('../lib/log4js') +, sandbox = require('sandboxed-module') +; + +function setupLogging(category, options) { + var udpSent = {}; + + var fakeDgram = { + createSocket: function (type) { + return { + send: function(buffer, offset, length, port, host, callback) { + udpSent.date = new Date(); + udpSent.host = host; + udpSent.port = port; + udpSent.length = length; + udpSent.offset = 0; + udpSent.buffer = buffer; + callback(undefined, length); + } + }; + } + }; + + var logstashModule = sandbox.require('../lib/appenders/logstashUDP', { + requires: { + 'dgram': fakeDgram + } + }); + log4js.clearAppenders(); + log4js.addAppender(logstashModule.configure(options), category); + + return { + logger: log4js.getLogger(category), + results: udpSent + }; +} + +vows.describe('logstashUDP appender').addBatch({ + 'when logging with logstash via UDP': { + topic: function() { + var setup = setupLogging('logstashUDP', { + "host": "127.0.0.1", + "port": 10001, + "type": "logstashUDP", + "logType": "myAppType", + "category": "myLogger", + "fields": { + "field1": "value1", + "field2": "value2" + }, + "layout": { + "type": "pattern", + "pattern": "%m" + } + }); + setup.logger.log('trace', 'Log event #1'); + return setup; + }, + 'an UDP packet should be sent': function (topic) { + assert.equal(topic.results.host, "127.0.0.1"); + assert.equal(topic.results.port, 10001); + assert.equal(topic.results.offset, 0); + var json = JSON.parse(topic.results.buffer.toString()); + assert.equal(json.type, 'myAppType'); + var fields = { + field1: 'value1', + field2: 'value2', + level: 'TRACE' + }; + assert.equal(JSON.stringify(json.fields), JSON.stringify(fields)); + assert.equal(json.message, 'Log event #1'); + // Assert timestamp, up to hours resolution. + var date = new Date(json['@timestamp']); + assert.equal( + date.toISOString().substring(0, 14), + topic.results.date.toISOString().substring(0, 14) + ); + } + }, + + 'when missing some options': { + topic: function() { + var setup = setupLogging('myLogger', { + "host": "127.0.0.1", + "port": 10001, + "type": "logstashUDP", + "category": "myLogger", + "layout": { + "type": "pattern", + "pattern": "%m" + } + }); + setup.logger.log('trace', 'Log event #1'); + return setup; + }, + 'it sets some defaults': function (topic) { + var json = JSON.parse(topic.results.buffer.toString()); + assert.equal(json.type, 'myLogger'); + assert.equal(JSON.stringify(json.fields), JSON.stringify({'level': 'TRACE'})); + } + } +}).export(module);