diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b8b49e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +log_test/ +whole_flow/ +node_modules/ +.DS_Store diff --git a/READ.ME b/READ.ME new file mode 100644 index 0000000..7a1a1c3 --- /dev/null +++ b/READ.ME @@ -0,0 +1,182 @@ +# LOG STAT ALARM + + ### Log + + * Append JSON object, array, or string to a log file + * All type will be converted to string automatically + * ISO string and unix time will be with each log by default + + ### Stat + + * Keep track of value within the applications + * Call 'increment' method of specific value whenever incrementation is needed + * Stat will automatically append all value into a file when it reaches its configured interval + * Value can be pre-configured for alarm (threshold, inverted threshold: read more on Alarm section) + * ISO string and unix time will be with each stat by default + + ### Alarm + + * If any stat value exceed its pre-configured threshold, it will be logged into alarm file + * If any stat value does not reach its pre-configured inverted threshold, it will also be logged into alarm file + * Before logging into alarm file, the module will call external functions that are being provided on init method (more on example) + * Stat module will consistently check the above rules in at the end of each interval + + ### File Management + + * Every module has option to configure file rotation frequency + * File rotation can be configured in two ways: + * Rotate by time (in millisecond) + * Rotate by file max size (in byte) + * Each module can be configured with both options ( read more on example section ) + + ### Example + + ##### Initialization + + Initialization should be called only once in each application + Use the response object for log stat alarm + + Options + * dirname: the parent folder for log, stat, and alarm ('./logstatalarm'). Log, stat and alarm files will be auto generated as child folders ( Type: string ) + * ./logstatalarm/log/ + * ./logstatalarm/stat/ + * ./logstatalarm/alarm/ + * log.rotation: seperate files into time frame ( Type: integer (ms) ) + * 1000 = 1 second + * 60 * 1000 = 60 seconds + * 15 * 60 * 1000 = 15 minutes + * log.maxsize: if the current time frame for the file exceed maxsize, it will auto generate file with same time frame with suffix to count ( Type: integer (byte) ) + * 1 = 1 byte + * 1000 = 1 Kb + * 1000000 = 1 Mb + * stat.rotation: seperate files into time frame ( Type: integer (ms) ) + * 1000 = 1 second + * 60 * 1000 = 60 seconds + * 15 * 60 * 1000 = 15 minutes + * stat.maxsize: if the current time frame for the file exceed maxsize, it will auto generate file with same time frame with suffix to count ( Type: integer (byte) ) + * 1 = 1 byte + * 1000 = 1 Kb + * 1000000 = 1 Mb + * stat.interval: interval defines how frequent stat should be append to file. All value counter will be reset to 0 and if any value is not within the configured threshold range, it will be log to alarm file. ( Type: interger (ms) ) + * 1000 = 1 second + * 2 * 1000 = 2 seconds + * 2 * 60 * 1000 = 2 minutes + * stat.data: Data can be pre-configured with threshold and inverted threshold ( Type: array of object ) + * Object data type: { key: string, threshold: integer (min 0), threshold_inv: integer (min 0) } + * Threshold and threshold_inv is not required + * At the end of each interval, stat value will be checked if it exceed threshold or haven't reach threshold_inv. If so, it will log to alarm file automatically + * alarm.rotation: seperate files into time frame ( Type: integer (ms) ) + * 1000 = 1 second + * 60 * 1000 = 60 seconds + * 15 * 60 * 1000 = 15 minutes + * alarm.maxsize: if the current time frame for the file exceed maxsize, it will auto generate file with same time frame with suffix to count ( Type: integer (byte) ) + * 1 = 1 byte + * 1000 = 1 Kb + * 1000000 = 1 Mb + * alarm.external: ( Type: Array ) + * alarm.external[].fn: ( Type: Function) + * This function will be called right before the module log into alarm log file (Use cases: http request, snmp request, syslog file logging, etc.) + * All functions will be called asynchronously + * All callback function within these functions will be ignored + * All return data within these functions will also be ignored + * Make sure the first argument of this function is for data (This data is the same data that logged in to alarm file)) + * Error handling have to be carefully implemented because all return data will be ignored + * alarm.external[].args: ( Type: Array ) + * alarm.external[].args[]: arguments for function above ( Type: Any ) + * Args will be use for executing the alarm.external[].fn function (more on examples) + + + Note*: In this example, we will be using request module (npm install request) on alarm external function for demonstration purpose only. + ``` + var logstatalarm = require('logstatalarm'); + var request = require('request'); + var http_request = function(data, host, port, path, method) { // make sure your first argument is Data + var opts = { + host: host, + port: port, + path: path, + method: method, + body: data // Data will always be in type: String + request(opts, function(err, res) { // Any callback or return data will be ignore + if(err) { + console.log(err); + } + }; + + } + var opts = { + dirname: './logstatalarm/', // required + log: { + rotation: 15 * 60 * 1000, // not required, default: 15 minutes + maxsize: 2000000 // not required, default: 20 Mb + }, // required ({} if no configuration is needed) + stat: { + rotation: 30 * 60 * 1000, // not required, default: 15 minutes + maxsize: 2000000, // not required, default: 20 Mb + interval: 2 * 60 * 1000, // not required, default: 1 minute + data: [ + {key: 'userLogin', threshold_inv: 100}, + {key: 'Error 400', threshold: 1000, threshold_inv: 10}, + {key: 'Error 500', threshold: 1} + ] // not required, default: [] + }, // required ({} if no configuration is needed) + alarm: { + rotation: 60 * 60 * 1000, // not required, default: 15 minutes + maxsize: 2000000 // not required, default: 20 Mb + external: [ + { fn: http_request, args: ["http://localhost", "3000", "/alarm", "POST"] }, // required + { fn: http_request, args: ["http://someserver.com", "9999", "/alarm", "POST"] } // not required + ] // not required + }, // required ({} if no configuration is needed) + }; + + //Call init method only once + var logObj = logstatalarm.init(opts); + + // NOTE *: in case if there is any error, the module will throw error and crash the applicatio (Configuration error) + ``` + + ##### Log + We will be using variable logObj throughout the application + ``` + var exampleLogData = { + path: '/user', + method: 'GET', + resquest: {}, + response: {statusCode: 200} + } + logObj.log.append(exampleLogData); + ``` + + ##### Stat + After initialization of the module, start method have to be called once inorder to start the interval logging of stat values + Then freely use increment method any where any time needed + We will be using variable logObj throughout the application + ``` + logObj.stat.start(); // call this once + logObj.stat.increment('Error 500'); + logObj.stat.increment('Error 400'); + logObj.stat.increment(User + 'user login'); + ``` + At any point in time stop method can be called to stop logging interval + ``` + logObj.stat.stop(); + ``` + ##### Alarm + External functions will be executed before logging in to alarm file. + You don't need to do anything with the alarm module. It will be logged automatically from stat module. + + ##### File management + Example of log file with this configuration: + rotation: 1000 + maxsize: 20000 + + -rw-r--r-- test 20K Jun 20 15:00 2016-06-20T15:00:19_00001.txt + -rw-r--r-- test 11K Jun 20 15:00 2016-06-20T15:00:19_00002.txt + -rw-r--r-- test 20K Jun 20 15:00 2016-06-20T15:00:20_00001.txt + -rw-r--r-- test 11K Jun 20 15:00 2016-06-20T15:00:20_00002.txt + + As you can see, the file is set to maximum at 20Kb and is rotate every 1 second + If the file reaches maxsize before 1 second it will append log into the same time frame with incremental suffix + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..cac80fb --- /dev/null +++ b/index.js @@ -0,0 +1,60 @@ +var _ = require('lodash'); +var moment = require('moment'); +var log = require('./model/log'); +var stat = require('./model/stat'); +var alarm = require('./model/alarm'); +var validate = require('./lib/validate'); +var joi = require('joi'); + +module.exports = { + + /* + * opts: { + * log: [dirname, rotation, maxsize] + * stat: [dirname, rotation, maxsize, interval, key] + * alarm: [dirname, rotation, maxsize] + * } + */ + init: function(opts) { + var logstatalarm = undefined; + validate.joi(opts, this.getSchema(), function(err, opts) { + if(err) { + throw new Error(err); + } + }); + return { log: new log(opts.dirname, opts.log), + stat: new stat(opts.dirname, opts.stat, opts.alarm) + }; + }, + + /* + * Pre-define schema + */ + getSchema: function() { + return joi.object().keys({ + dirname: joi.string().required(), + log: joi.object().keys({ + rotation: joi.number().min(0), + maxsize: joi.number().min(0) + }).required(), + stat: joi.object().keys({ + rotation: joi.number().min(0), + maxsize: joi.number().min(0), + interval: joi.number().min(0), + data: joi.array().items(joi.object().keys({ + key: joi.string().required(), + threshold: joi.number().min(0), + threshold_inv: joi.number().min(0) + })) + }).required(), + alarm: joi.object().keys({ + rotation: joi.number().min(0), + maxsize: joi.number().min(0), + external: joi.array().items(joi.object().keys({ + fn: joi.func().required(), + args: joi.array().items(joi.any()) + })) + }).required() + }); + } +}; diff --git a/lib/helper.js b/lib/helper.js new file mode 100644 index 0000000..1986822 --- /dev/null +++ b/lib/helper.js @@ -0,0 +1,40 @@ +var fs = require('fs'); +var request = require('request-promise').defaults({jar: true}) + +module.exports = { + + isValidFileSize: function(dirname, maxsize) { + return fs.statSync(dirname)['size'] < maxsize; + }, + + getLengthOfContent: function(string) { + return Buffer.byteLength(string, 'utf8'); + }, + + mkdirIfNotExist: function(dirname) { + var _dirname = dirname.split('/'); + var parentdir = _dirname.slice(0, _dirname.length-2).join('/'); + if (!fs.existsSync(parentdir)){ + fs.mkdirSync(parentdir); + } + if (!fs.existsSync(dirname)){ + fs.mkdirSync(dirname); + } + }, + + deleteFolderRecursive: function(path) { + var self = this; + if( fs.existsSync(path) ) { + fs.readdirSync(path).forEach(function(file, index){ + var curPath = path + "/" + file; + if(fs.lstatSync(curPath).isDirectory()) { + self.deleteFolderRecursive(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(path); + } + } + +}; diff --git a/lib/validate.js b/lib/validate.js new file mode 100644 index 0000000..09405eb --- /dev/null +++ b/lib/validate.js @@ -0,0 +1,39 @@ +var joi = require('joi'); +var _ = require('lodash'); + +module.exports = { + + joi: function(data, schema, callback) { + opts = { + abortEarly: false, + convert: true, + allowUnknown: false, + stripUnknown: false + } + joi.validate(data, schema, opts, function(err, obj){ + if(err) { + var errs = _.map(err.details, function(e) { + var key = e.path; + var x = e.type; + var _postfix = undefined; + switch(_.last(x.split('.'))) { + case 'required': + _postfix = 'required'; + break; + case 'allowUnknown': + _postfix = 'not allowed'; + break; + default: + _postfix = 'invalid format' + break; + } + return `${key} is ${_postfix}`; + }); + callback(errs, null); + } else { + callback(null, obj); + } + + }) + } +}; diff --git a/model/alarm.js b/model/alarm.js new file mode 100644 index 0000000..6cb16ce --- /dev/null +++ b/model/alarm.js @@ -0,0 +1,102 @@ +var fs = require('fs'); +var moment = require('moment'); +var helper = require('../lib/helper'); +var _ = require('lodash'); + +/* + * parameters: + * dirname: string + * rotation: interger (ms) + * maxsize: interger (byte) + */ +function alarm(dirname, opts) { + opts = opts || {}; + this.dirname = dirname; + this.rotation = opts.rotation || 15 * 60 * 1000; + this.maxsize = opts.maxsize || 20000; + this.external = opts.external || []; + this.currentsize = 0; + this.timestamp = 0; + this.foldername = 'alarm/'; + helper.mkdirIfNotExist(`${this.dirname}/${this.foldername}`); +}; + +/* + * parameters: + * data: any + */ +alarm.prototype.appendAlarm = function(data) { + data = this.formatData(data); + this.currentsize = this.currentsize + helper.getLengthOfContent(data); + this.request(data); + fs.appendFile(this.getDir(), data, function(err) {}); +}; + +/* + * parameters: + * none + */ +alarm.prototype.getDir = function() { + var time = moment(Math.floor((+moment()) / this.rotation) * this.rotation); + this.resetCurrentSize(time.unix()); + time = time.format('YYYY-MM-DDTHH-mm-ss'); + var count = this.getCount(); + return `${this.dirname}${this.foldername}${time}_${count}.txt`; +}; + +/* + * parameters: + * time_unix: string + */ +alarm.prototype.resetCurrentSize = function(time_unix) { + if(time_unix > this.timestamp) { + this.currentsize = 0 + this.timestamp = time_unix; + } +}; + +/* + * parameters: + * none + */ +alarm.prototype.getCount = function() { + var count = Math.floor((this.currentsize / this.maxsize) + 1); + return ((count * 1e-5).toFixed(5)).split('.')[1]; +}; + +/* + * parameters: + * data: any + */ +alarm.prototype.formatData = function(data) { + var date = moment().toISOString().trim(); + var timestamp = moment().unix(); + data = this._formatObject(data).trim(); + return `${date} ${timestamp} ${data}\r\n`; +}; + +/* + * parameters: + * data: any + */ +alarm.prototype._formatObject = function(data) { + if(_.isObject(data)) { + return JSON.stringify(data); + } + if(_.isNumber(data)) { + return toString(data); + } + return data; +}; + +/* + * parameters: + * data: any + */ +alarm.prototype.request = function(data) { + _.forEach(this.external, function(external) { + external.fn.apply(this, [data].concat(external.args)); + }); +}; + +module.exports = alarm; diff --git a/model/log.js b/model/log.js new file mode 100644 index 0000000..df9b946 --- /dev/null +++ b/model/log.js @@ -0,0 +1,89 @@ +var fs = require('fs'); +var moment = require('moment'); +var helper = require('../lib/helper'); +var _ = require('lodash'); + +/* + * parameters: + * dirname: string + * rotation: interger (ms) + */ +function log(dirname, opts) { + opts = opts || {}; + this.dirname = dirname; + this.rotation = opts.rotation || 15 * 60 * 1000; + this.maxsize = opts.maxsize || 20000; + this.currentsize = 0; + this.timestamp = 0; + this.foldername = 'log/'; + helper.mkdirIfNotExist(`${this.dirname}/${this.foldername}`); +}; + +/* + * parameters: + * data: any + */ +log.prototype.append = function(data) { + data = this.formatData(data); + this.currentsize = this.currentsize + helper.getLengthOfContent(data); + fs.appendFile(this.getDir(), data, function(err) {}); +}; + +/* + * parameters: + * none + */ +log.prototype.getDir = function() { + var time = moment(Math.floor((+moment()) / this.rotation) * this.rotation); + this.resetCurrentSize(time.unix()); + time = time.format('YYYY-MM-DDTHH-mm-ss'); + var count = this.getCount(); + return `${this.dirname}${this.foldername}${time}_${count}.txt`; +}; + +/* + * parameters: + * time_unix: string + */ +log.prototype.resetCurrentSize = function(time_unix) { + if(time_unix > this.timestamp) { + this.currentsize = 0 + this.timestamp = time_unix; + } +}; + +/* + * parameters: + * none + */ +log.prototype.getCount = function() { + var count = Math.floor((this.currentsize / this.maxsize) + 1); + return ((count * 1e-5).toFixed(5)).split('.')[1]; +}; + +/* + * parameters: + * data: any + */ +log.prototype.formatData = function(data) { + var date = moment().toISOString().trim(); + var timestamp = moment().unix(); + data = this._formatObject(data).trim(); + return `${date} ${timestamp} ${data}\r\n`; +}; + +/* + * parameters: + * data: any + */ +log.prototype._formatObject = function(data) { + if(_.isObject(data)) { + return JSON.stringify(data); + } + if(_.isNumber(data)) { + return toString(data); + } + return data; +}; + +module.exports = log; diff --git a/model/stat.js b/model/stat.js new file mode 100644 index 0000000..0d34854 --- /dev/null +++ b/model/stat.js @@ -0,0 +1,179 @@ +var fs = require('fs'); +var moment = require('moment'); +var helper = require('../lib/helper'); +var _ = require('lodash'); +var alarm = require('./alarm'); + +/* + * parameters: + * dirname: string + * rotation: interger (ms) + * maxsize: interger (byte) + * interval: interger (ms) + * data: array (array of object) + * alarm: object (alarm object) + */ +function stat(dirname, opts, alarmData) { + opts = opts || {}; + alarmData = alarmData || {}; + this.dirname = dirname; + this.rotation = opts.rotation || 15 * 60 * 1000; + this.maxsize = opts.maxsize || 20000; + this.currentsize = 0; + this.timestamp = 0; + this.foldername = 'stat/'; + this.intervalId = undefined; + this.interval = opts.interval || 60 * 1000; + this.data = transformKeys(opts.data); + this.rules = opts.data; + this.alarm = new alarm(dirname, alarmData); + helper.mkdirIfNotExist(`${this.dirname}/${this.foldername}`); +}; + +function transformKeys(data) { + keys = _.map(data, function(obj) { + var fakey = {}; + fakey[obj.key] = 0; + return fakey; + }); + return _.reduce(keys, function(new_obj, obj) { + return _.merge(new_obj, obj); + }, {}); + +}; + +/* + * parameters: + * data: obj + * rules: [obj] + */ +function alarmDataOutOfThreshold(data, rules) { + _alarm = []; + _.forEach(data, function(v, k) { + rule = _.find(rules, ['key', k]); + var alarmObj = {}; + if(v < rule.threshold_inv) { + _alarm.push({key: k, + count: v, + threshold_inv: rule.threshold_inv, + message: `${k} count is below inverted threshold`}); + } + if(v > rule.threshold) { + _alarm.push({key: k, + count: v, + threshold: + rule.threshold, + message: `${k} count is above threshold`}); + } + }); + return _alarm +}; + +/* + * parameters: + * data: any + */ +stat.prototype.appendStat = function(data) { + data = this.formatData(data); + this.currentsize = this.currentsize + helper.getLengthOfContent(data); + fs.appendFile(this.getDir(), data, function(err) {}); +}; + +/* + * parameters: + * none + */ +stat.prototype.start = function() { + var self = this; + this.intervalId = setInterval(function(){ + var alarmData = alarmDataOutOfThreshold(self.data, self.rules); + if(!_.isEmpty(alarmData)) { + self.alarm.appendAlarm(alarmData); + } + self.appendStat(self.data); + self.reset(); + }, self.interval); +}; + +/* + * parameters: + * none + */ +stat.prototype.stop = function() { + clearInterval(this.intervalId); +}; + +/* + * parameters: + * data: string + */ +stat.prototype.increment = function(data) { + if(_.has(this.data, data)) { + this.data[data]++; + } + else this.data[data] = 1; +}; + +/* + * parameters: + * none + */ +stat.prototype.reset = function() { + var self = this; + _.forEach(this.data, function(v, k) { + self.data[k] = 0; + }); +}; + +/* + * parameters: + * none + */ +stat.prototype.getDir = function() { + var time = moment(Math.floor((+moment()) / this.rotation) * this.rotation); + this.resetCurrentSize(time.unix()); + time = time.format('YYYY-MM-DDTHH-mm-ss'); + var count = this.getCount(); + return `${this.dirname}${this.foldername}${time}_${count}.txt`; +}; + +/* + * parameters: + * time_unix: string + */ +stat.prototype.resetCurrentSize = function(time_unix) { + if(time_unix > this.timestamp) { + this.currentsize = 0 + this.timestamp = time_unix; + } +}; + +/* + * parameters: + * none + */ +stat.prototype.getCount = function() { + var count = Math.floor((this.currentsize / this.maxsize) + 1); + return ((count * 1e-5).toFixed(5)).split('.')[1]; +}; + +/* + * parameters: + * data: any + */ +stat.prototype.formatData = function(data) { + var date = moment().toISOString().trim(); + var timestamp = moment().unix(); + data = this._formatObject(data).trim(); + return `${date} ${timestamp} ${data}\r\n`; +}; + +/* + * parameters: + * data: any + */ +stat.prototype._formatObject = function(data) { + return JSON.stringify(data); +}; + +module.exports = stat; diff --git a/package.json b/package.json new file mode 100644 index 0000000..918769d --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "logstatalarm", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "mocha" + }, + "author": "pupuupup", + "license": "ISC", + "dependencies": { + "bluebird": "^3.4.0", + "chai": "^3.5.0", + "joi": "^8.4.2", + "lodash": "^4.13.1", + "moment": "^2.13.0", + "request-promise": "^3.0.0" + } +} diff --git a/test/alarm.js b/test/alarm.js new file mode 100644 index 0000000..7d22e95 --- /dev/null +++ b/test/alarm.js @@ -0,0 +1,54 @@ +require('./setup'); +var alarm = require('../model/alarm'); +var _ = require('lodash'); +var Promise = require('bluebird'); +var fs = require('fs'); + +describe('Alarm Model', function() { + + var testData1, testData2; + var testAString = "helloworld"; + var aRequestFunction1 = function(data, aString){ + testData1 = data + "," + aString; + }; + var aRequestFunction2 = function(data, aString1, aString2){ + testData2 = aString1 + "," + data + "," + aString2; + }; + var validData = {} + data = { + rotation: 5000, + external: [ + { fn: aRequestFunction1 ,args: [testAString] }, + { fn: aRequestFunction2 ,args: [testAString, testAString] } + ] + }; + var alarm_object = new alarm('./log_test/', data); + + before(function(done) { + validData = ['lksdjfjklsdlfjll']; + done(); + }); + + it('should appendAlarm asynchronously correctly', function(done) { + Promise.all(_.map(_.times(200, String), function(n) { + return Promise.resolve(alarm_object.appendAlarm(validData)); + })) + .then(function() { + return Promise.delay(1100); + }) + .then(function() { + Promise.all(_.map(_.times(200, String), function(n) { + return Promise.resolve(alarm_object.appendAlarm(validData)); + })) + }) + .then(function() { + expect(testData1.split(",")[1]).eql(testAString) + expect(testData2.split(",")[0]).eql(testAString) + expect(testData2.split(",")[2]).eql(testAString) + }) + .then(function() { + done(); + }); + }); + +}); diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..c417a32 --- /dev/null +++ b/test/index.js @@ -0,0 +1,134 @@ +require('./setup'); +var index = require('../index'); +var Promise = require('bluebird'); + +describe('Main index.js', function() { + + var validData = {} + before(function(done) { + validData = { + dirname: './log_test/', + log: {}, + stat: { + interval: 2, + data: [ + {key: 'a', threshold: 2, threshold_inv: 1}, + {key: 'b', threshold: 2 } + ] + }, + alarm: {} + } + done(); + }); + + it('init function should return true on cb properly', function(done) { + var object = index.init(validData); + expect(object).to.be.an('object'); + expect(object).to.have.property('log'); + expect(object).to.have.property('stat'); + done(); + }); + + it('init function should return err on cb properly (case 1)', function(done) { + try { + var object = index.init({log: 'hi'}); + } + catch (err) { + var my_err = err.message.split(','); + expect(my_err).to.include.members(['log is invalid format', 'dirname is required', 'alarm is required']); + done(); + } + }); + + it('init function should return err on cb properly (case 2)', function(done) { + try { + var object = index.init({pupu: 'hi'}); + } + catch (err) { + var my_err = err.message.split(','); + expect(my_err).to.include.members(['pupu is not allowed']); + done(); + } + }); + + it('init function should return err on cb properly (case 3)', function(done) { + try { + var object = index.init({pupu: 'hi'}); + } + catch (err) { + var my_err = err.message.split(','); + expect(my_err).to.include.members(['log is required', 'stat is required']); + done(); + } + }); + + it('should fail to create stat object if validdata object contain no key with threshold', function(done) { + var testData = { + dirname: './log_test/', + log: {}, + stat: { + interval: 2, + data: [ + {key: 'a', threshold: 2, threshold_inv: 1}, + {key: 'b', threshold: 2}, + {key: 'c', threshold_inv: 1}, + {key: 'd', threshold: 2}, + {threshold: 2}, + ] + } + } + try { + var object = index.init(testData); + } + catch (err) { + var my_err = err.message.split(','); + expect(my_err).to.include.members(['stat.data.4.key is required']); + done(); + } + }); + + it('should run the whole flow correctly', function(done) { + var initData = { + dirname: './whole_flow/', + log: { + rotation: 500, + maxsize: 500 + }, + stat: { + rotation: 500, + maxsize: 500, + interval: 5, + data: [ + {key: 'testIncrement1', threshold: 3, threshold_inv: 1}, + {key: 'testIncrement2', threshold: 2 } + ] + }, + alarm: { + rotation: 500, + maxsize: 500 + } + }; + var logObj = index.init(initData); + Promise.try(function() {}) + .then(function() { + logObj.stat.start(); + logObj.log.append({hi: 'test'}); + logObj.stat.increment('testIncrement1'); + return Promise.delay(100); + }) + .then(function() { + logObj.log.append({hi: 'test2'}); + logObj.stat.increment('testIncrement2'); + logObj.log.append({hi: 'test2'}); + logObj.stat.increment('testIncrement2'); + }) + .then(function() { + logObj.stat.stop(); + done(); + }) + + + }); + + +}); diff --git a/test/lib/helper.js b/test/lib/helper.js new file mode 100644 index 0000000..7fb03f5 --- /dev/null +++ b/test/lib/helper.js @@ -0,0 +1,26 @@ +require('../setup'); +var helper = require('../../lib/helper'); + +describe('Lib helper.js', function() { + + var _dirname = './test/mocha.opts'; + before(function(done) { + done(); + }); + + it('isValidFileSize should return true when file size is less than maxsize', function(done) { + var _maxsize = 200; + bool = helper.isValidFileSize(_dirname, _maxsize); + expect(bool).to.be.true; + done(); + }); + + it('isValidFileSize should return false when file size exceed max size', function(done) { + var _maxsize = 10; + bool = helper.isValidFileSize(_dirname, _maxsize); + expect(bool).to.be.false; + done(); + }); + + +}); diff --git a/test/log.js b/test/log.js new file mode 100644 index 0000000..4be0261 --- /dev/null +++ b/test/log.js @@ -0,0 +1,62 @@ +require('./setup'); +var log = require('../model/log'); +var _ = require('lodash'); +var Promise = require('bluebird'); +var fs = require('fs'); + +describe('Log Model', function() { + + var validData = {} + var log_object = new log('./log_test/', {rotation: 1000, maxsize: 20000}); + + before(function(done) { + validData = { + path: "/test", + method: "GET", + request: { + body: {test: true} + }, + response: { + statusCode: 200, + body: { success: true } + } + }; + done(); + }); + + it('should formatObject correctly', function(done) { + var data = log_object._formatObject(validData); + expect(data).eql(JSON.stringify(validData)); + done(); + }); + + it('should formatData correctly', function(done) { + var data = log_object.formatData(validData).split(' '); + expect(_.last(data).slice(0, _.last(data).length-1).trim().split('\r\n').join('')) + .eql(JSON.stringify(validData).trim().split('\r\n').join('')); + done(); + }); + + it('should appendLog correctly', function(done) { + log_object.append(validData); + done(); + }); + + it('should appendLog asynchronously correctly', function(done) { + Promise.all(_.map(_.times(200, String), function(n) { + return Promise.resolve(log_object.append(validData)); + })) + .then(function() { + return Promise.delay(1100); + }) + .then(function() { + Promise.all(_.map(_.times(200, String), function(n) { + return Promise.resolve(log_object.append(validData)); + })) + }) + .then(function() { + done(); + }); + }); + +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..3d16389 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,4 @@ +--recursive +--reporter spec +--slow 500 +--timeout 3000 diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..8cf23d7 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,8 @@ +global.expect = require('chai').expect; +var fs = require('fs'); +var helper = require('../lib/helper'); + +before(function (done) { + helper.deleteFolderRecursive('./log_test/'); + done(); +}); diff --git a/test/stat.js b/test/stat.js new file mode 100644 index 0000000..1cf118c --- /dev/null +++ b/test/stat.js @@ -0,0 +1,49 @@ +require('./setup'); +var stat = require('../model/stat'); +var _ = require('lodash'); +var Promise = require('bluebird'); +var fs = require('fs'); + +describe('Stat Model', function() { + + var validData = [ + {key: 'a', threshold: 2, threshold_inv: 1}, + {key: 'b', threshold: 2}, + {key: 'c', threshold_inv: 1}, + {key: 'd', threshold: 2} + ] + + var stat_object = new stat('./log_test/', {rotation:5000, maxsize:5000, interval:1, data:validData}) + + before(function(done) { + validData = ['a', 'b']; + done(); + }); + + it('should stat correctly (whole process)', function(done) { + stat_object.start(); + return Promise.delay(100) + .then(function() { + Promise.all(_.map(_.times(100, String), function(){ + stat_object.increment(validData[0]); + })) + }) + .then(function() { + return Promise.delay(100) + }) + .then(function() { + stat_object.increment(validData[0]); + stat_object.increment(validData[1]); + }) + .then(function() { + return Promise.delay(100) + }) + .then(function() { + stat_object.stop(); + done(); + }) + + }); + + +}); -- libgit2 0.21.2