diff --git a/index.js b/index.js index 7b33ff618..992c01d79 100755 --- a/index.js +++ b/index.js @@ -2,20 +2,17 @@ 'use strict'; -var dgram = require('dgram'), - fs = require('fs'), - path = require('path'), - mkdirp = require('mkdirp'), - parser = require('nsyslog-parser'); +var server = require('./server.js'); const argv = require('yargs') .default('port', 2514, 'The port to listen on') // syslog is 514 so we prefix with 2 .default('logdir', '/tmp/logs', 'The root log directory') .argv; -const LOG_FILE_FOLDER = argv.logdir; -const LOG_FILE_NAME = 'app.log'; -const PORT = argv.port; +const options = { + logFolder: argv.logdir, + port: argv.port +}; if (argv.version) { console.log('1.0.1'); @@ -27,34 +24,13 @@ console.log('=========================================='); console.log(' Cloudron Syslog Daemon '); console.log('=========================================='); console.log(); -console.log(' Log Folder: ', LOG_FILE_FOLDER); -console.log(' UDP Port: ', PORT); +console.log(' Log Folder: ', options.logFolder); +console.log(' UDP Port: ', options.port); console.log(); console.log('=========================================='); console.log(); -var server = dgram.createSocket('udp4'); - -server.on('error', function (error) { - console.error('Error:', error); -}).on('listening', function () { +server.start(options, function (error) { + if (error) return console.error(error); console.log('Listening...'); -}).on('message', function (msg, rinfo) { - var info = parser(msg.toString()); - - if (!info || !info.appName) return console.log('Ignore unknown app log:', msg.toString()); - - // remove line breaks to avoid holes in the log file - // we do not ignore empty log lines, to allow gaps for potential ease of readability - const message = info.message.replace(/\n/g, ''); - - const filePath = path.join(LOG_FILE_FOLDER, info.appName); - const fileName = path.join(filePath, LOG_FILE_NAME); - - try { - mkdirp.sync(filePath); - fs.appendFileSync(fileName, info.ts.toISOString() + ' ' + message + '\n'); - } catch (error) { - console.error(error); - } -}).bind(PORT); +}); diff --git a/package-lock.json b/package-lock.json index 2b72540f4..0490429d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,28 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", @@ -29,6 +51,18 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -39,11 +73,32 @@ "which": "1.3.1" } }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", @@ -58,6 +113,12 @@ "strip-eof": "1.0.0" } }, + "expect.js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", + "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", + "dev": true + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -66,6 +127,12 @@ "locate-path": "2.0.0" } }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, "get-caller-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", @@ -76,6 +143,60 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "grepit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grepit/-/grepit-1.0.0.tgz", + "integrity": "sha1-18Tdxd/IhS66mHZ0z+p8b1KLjZI=", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -135,6 +256,15 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", @@ -148,6 +278,31 @@ "minimist": "0.0.8" } }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -166,6 +321,15 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, "os-locale": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", @@ -207,6 +371,12 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -227,6 +397,15 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -272,6 +451,15 @@ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -327,6 +515,12 @@ } } }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", diff --git a/package.json b/package.json index 8e0a494e9..2f8b1148b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Cloudron Syslog Daemon listening on port 2514", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "./node_modules/.bin/mocha ./test.js" }, "bin": { "cloudron-syslog": "./index.js" @@ -19,5 +19,11 @@ "mkdirp": "^0.5.1", "nsyslog-parser": "^0.8.1", "yargs": "^11.0.0" + }, + "devDependencies": { + "expect.js": "^0.3.1", + "grepit": "^1.0.0", + "mocha": "^5.2.0", + "rimraf": "^2.6.2" } } diff --git a/server.js b/server.js new file mode 100644 index 000000000..60a265e46 --- /dev/null +++ b/server.js @@ -0,0 +1,62 @@ +'use strict'; + +exports = module.exports = { + start: start, + stop: stop +}; + +const LOG_FILENAME = 'app.log'; + +var assert = require('assert'), + dgram = require('dgram'), + fs = require('fs'), + path = require('path'), + mkdirp = require('mkdirp'), + parser = require('nsyslog-parser'); + +var server = null; + +function start(options, callback) { + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof options.port, 'number'); + assert.strictEqual(typeof options.logFolder, 'string'); + assert.strictEqual(typeof callback, 'function'); + + server = dgram.createSocket('udp4'); + + server.on('error', function (error) { + callback(error); + }).on('listening', function () { + callback(); + }).on('message', function (msg, rinfo) { + var info = parser(msg.toString()); + + if (!info || !info.appName) return console.log('Ignore unknown app log:', msg.toString()); + + // remove line breaks to avoid holes in the log file + // we do not ignore empty log lines, to allow gaps for potential ease of readability + const message = info.message.replace(/\n/g, ''); + + const filePath = path.join(options.logFolder, info.appName); + const fileName = path.join(filePath, LOG_FILENAME); + + try { + mkdirp.sync(filePath); + fs.appendFileSync(fileName, info.ts.toISOString() + ' ' + message + '\n'); + } catch (error) { + console.error(error); + } + }).bind(options.port); +} + +function stop(callback) { + assert.strictEqual(typeof callback, 'function'); + + if (!server) return callback(); + + server.close(); + + server = null; + + callback(); +} \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 000000000..51c38ba8f --- /dev/null +++ b/test.js @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +var expect = require('expect.js'), + dgram = require('dgram'), + grepit = require('grepit'), + path = require('path'), + os = require('os'), + rimraf = require('rimraf'), + server = require('./server.js'); + +const PORT = 5678; +const LOG_FOLDER = path.join(os.tmpdir(), '/cloudron-syslog-test/'); + +function sendMessage(message, callback) { + const client = dgram.createSocket('udp4'); + client.send(message, PORT, 'localhost', function (error) { + if (error) return callback(error); + + client.close(); + + callback(); + }); +} + +function verifyMessage(pattern, fileName, callback) { + // give the server some time to write to disk + setTimeout(function () { + const found = grepit(pattern, path.join(LOG_FOLDER, fileName)); + if (found.length === 0) return callback('not found'); + callback(); + }, 250); +} + +describe('Daemon', function () { + this.timeout(5000); + + after(function (done) { + rimraf.sync(LOG_FOLDER); + server.stop(done); + }); + + it('can start', function (done) { + server.start({ port: PORT, logFolder: LOG_FOLDER }, function (error) { + expect(error).to.not.be.ok(); + done(); + }); + }); + + it('handle good message', function (done) { + // IETF (RFC 5424) message, with structured data and chained hostnames + const ietfLine = '<110>1 2009-05-03T14:00:39.529966+02:00 host.example.org/relay.example.org testapp 2138 - [exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"][exampleSDID@32474 iut="4" eventSource="Application" eventID="1012"][ssign VER="0111" RSID="1" SG="0" SPRI="0" GBC="2" FMN="1" CNT="7" HB="K6wzcombEvKJ+UTMcn9bPryAeaU= zrkDcIeaDluypaPCY8WWzwHpPok= zgrWOdpx16ADc7UmckyIFY53icE= XfopJ+S8/hODapiBBCgVQaLqBKg= J67gKMFl/OauTC20ibbydwIlJC8= M5GziVgB6KPY3ERU1HXdSi2vtdw= Wxd/lU7uG/ipEYT9xeqnsfohyH0=" SIGN="AKBbX4J7QkrwuwdbV7Taujk2lvOf8gCgC62We1QYfnrNHz7FzAvdySuMyfM="] BOMAn application event log entry'; + + sendMessage(ietfLine, function (error) { + expect(error).to.not.be.ok(); + + verifyMessage(/An application event log entry/, 'testapp/app.log', done); + }); + }); + + it('ignores invalid message', function (done) { + const invalidLine = 'foobar'; + + sendMessage(invalidLine, function (error) { + expect(error).to.not.be.ok(); + + verifyMessage(/foobar/, 'testapp/app.log', function (error) { + expect(error).to.be.ok(); + done(); + }); + }); + }); + + xit('can handle message with :', function (done) { + const message = '<30>1 2018-06-24T22:22:53Z my.test.com testapp 26599 testapp - This: contains two : colons'; + + sendMessage(message, function (error) { + expect(error).to.not.be.ok(); + + verifyMessage(/This: contains two : colons/, 'testapp/app.log', done); + }); + }); +});