Commit adfda8e12384c9f984db9762dd852b7a810399e7
0 parents
Exists in
master
and in
1 other branch
initial commit from ais code
Showing
16 changed files
with
1051 additions
and
0 deletions
Show diff stats
1 | +++ a/READ.ME | |
... | ... | @@ -0,0 +1,182 @@ |
1 | +# LOG STAT ALARM | |
2 | + | |
3 | + ### Log | |
4 | + | |
5 | + * Append JSON object, array, or string to a log file | |
6 | + * All type will be converted to string automatically | |
7 | + * ISO string and unix time will be with each log by default | |
8 | + | |
9 | + ### Stat | |
10 | + | |
11 | + * Keep track of value within the applications | |
12 | + * Call 'increment' method of specific value whenever incrementation is needed | |
13 | + * Stat will automatically append all value into a file when it reaches its configured interval | |
14 | + * Value can be pre-configured for alarm (threshold, inverted threshold: read more on Alarm section) | |
15 | + * ISO string and unix time will be with each stat by default | |
16 | + | |
17 | + ### Alarm | |
18 | + | |
19 | + * If any stat value exceed its pre-configured threshold, it will be logged into alarm file | |
20 | + * If any stat value does not reach its pre-configured inverted threshold, it will also be logged into alarm file | |
21 | + * Before logging into alarm file, the module will call external functions that are being provided on init method (more on example) | |
22 | + * Stat module will consistently check the above rules in at the end of each interval | |
23 | + | |
24 | + ### File Management | |
25 | + | |
26 | + * Every module has option to configure file rotation frequency | |
27 | + * File rotation can be configured in two ways: | |
28 | + * Rotate by time (in millisecond) | |
29 | + * Rotate by file max size (in byte) | |
30 | + * Each module can be configured with both options ( read more on example section ) | |
31 | + | |
32 | + ### Example | |
33 | + | |
34 | + ##### Initialization | |
35 | + | |
36 | + Initialization should be called only once in each application | |
37 | + Use the response object for log stat alarm | |
38 | + | |
39 | + Options | |
40 | + * dirname: the parent folder for log, stat, and alarm ('./logstatalarm'). Log, stat and alarm files will be auto generated as child folders ( Type: string ) | |
41 | + * ./logstatalarm/log/ | |
42 | + * ./logstatalarm/stat/ | |
43 | + * ./logstatalarm/alarm/ | |
44 | + * log.rotation: seperate files into time frame ( Type: integer (ms) ) | |
45 | + * 1000 = 1 second | |
46 | + * 60 * 1000 = 60 seconds | |
47 | + * 15 * 60 * 1000 = 15 minutes | |
48 | + * 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) ) | |
49 | + * 1 = 1 byte | |
50 | + * 1000 = 1 Kb | |
51 | + * 1000000 = 1 Mb | |
52 | + * stat.rotation: seperate files into time frame ( Type: integer (ms) ) | |
53 | + * 1000 = 1 second | |
54 | + * 60 * 1000 = 60 seconds | |
55 | + * 15 * 60 * 1000 = 15 minutes | |
56 | + * 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) ) | |
57 | + * 1 = 1 byte | |
58 | + * 1000 = 1 Kb | |
59 | + * 1000000 = 1 Mb | |
60 | + * 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) ) | |
61 | + * 1000 = 1 second | |
62 | + * 2 * 1000 = 2 seconds | |
63 | + * 2 * 60 * 1000 = 2 minutes | |
64 | + * stat.data: Data can be pre-configured with threshold and inverted threshold ( Type: array of object ) | |
65 | + * Object data type: { key: string, threshold: integer (min 0), threshold_inv: integer (min 0) } | |
66 | + * Threshold and threshold_inv is not required | |
67 | + * 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 | |
68 | + * alarm.rotation: seperate files into time frame ( Type: integer (ms) ) | |
69 | + * 1000 = 1 second | |
70 | + * 60 * 1000 = 60 seconds | |
71 | + * 15 * 60 * 1000 = 15 minutes | |
72 | + * 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) ) | |
73 | + * 1 = 1 byte | |
74 | + * 1000 = 1 Kb | |
75 | + * 1000000 = 1 Mb | |
76 | + * alarm.external: ( Type: Array ) | |
77 | + * alarm.external[].fn: ( Type: Function) | |
78 | + * This function will be called right before the module log into alarm log file (Use cases: http request, snmp request, syslog file logging, etc.) | |
79 | + * All functions will be called asynchronously | |
80 | + * All callback function within these functions will be ignored | |
81 | + * All return data within these functions will also be ignored | |
82 | + * Make sure the first argument of this function is for data (This data is the same data that logged in to alarm file)) | |
83 | + * Error handling have to be carefully implemented because all return data will be ignored | |
84 | + * alarm.external[].args: ( Type: Array ) | |
85 | + * alarm.external[].args[]: arguments for function above ( Type: Any ) | |
86 | + * Args will be use for executing the alarm.external[].fn function (more on examples) | |
87 | + | |
88 | + | |
89 | + Note*: In this example, we will be using request module (npm install request) on alarm external function for demonstration purpose only. | |
90 | + ``` | |
91 | + var logstatalarm = require('logstatalarm'); | |
92 | + var request = require('request'); | |
93 | + var http_request = function(data, host, port, path, method) { // make sure your first argument is Data | |
94 | + var opts = { | |
95 | + host: host, | |
96 | + port: port, | |
97 | + path: path, | |
98 | + method: method, | |
99 | + body: data // Data will always be in type: String | |
100 | + request(opts, function(err, res) { // Any callback or return data will be ignore | |
101 | + if(err) { | |
102 | + console.log(err); | |
103 | + } | |
104 | + }; | |
105 | + | |
106 | + } | |
107 | + var opts = { | |
108 | + dirname: './logstatalarm/', // required | |
109 | + log: { | |
110 | + rotation: 15 * 60 * 1000, // not required, default: 15 minutes | |
111 | + maxsize: 2000000 // not required, default: 20 Mb | |
112 | + }, // required ({} if no configuration is needed) | |
113 | + stat: { | |
114 | + rotation: 30 * 60 * 1000, // not required, default: 15 minutes | |
115 | + maxsize: 2000000, // not required, default: 20 Mb | |
116 | + interval: 2 * 60 * 1000, // not required, default: 1 minute | |
117 | + data: [ | |
118 | + {key: 'userLogin', threshold_inv: 100}, | |
119 | + {key: 'Error 400', threshold: 1000, threshold_inv: 10}, | |
120 | + {key: 'Error 500', threshold: 1} | |
121 | + ] // not required, default: [] | |
122 | + }, // required ({} if no configuration is needed) | |
123 | + alarm: { | |
124 | + rotation: 60 * 60 * 1000, // not required, default: 15 minutes | |
125 | + maxsize: 2000000 // not required, default: 20 Mb | |
126 | + external: [ | |
127 | + { fn: http_request, args: ["http://localhost", "3000", "/alarm", "POST"] }, // required | |
128 | + { fn: http_request, args: ["http://someserver.com", "9999", "/alarm", "POST"] } // not required | |
129 | + ] // not required | |
130 | + }, // required ({} if no configuration is needed) | |
131 | + }; | |
132 | + | |
133 | + //Call init method only once | |
134 | + var logObj = logstatalarm.init(opts); | |
135 | + | |
136 | + // NOTE *: in case if there is any error, the module will throw error and crash the applicatio (Configuration error) | |
137 | + ``` | |
138 | + | |
139 | + ##### Log | |
140 | + We will be using variable logObj throughout the application | |
141 | + ``` | |
142 | + var exampleLogData = { | |
143 | + path: '/user', | |
144 | + method: 'GET', | |
145 | + resquest: {}, | |
146 | + response: {statusCode: 200} | |
147 | + } | |
148 | + logObj.log.append(exampleLogData); | |
149 | + ``` | |
150 | + | |
151 | + ##### Stat | |
152 | + After initialization of the module, start method have to be called once inorder to start the interval logging of stat values | |
153 | + Then freely use increment method any where any time needed | |
154 | + We will be using variable logObj throughout the application | |
155 | + ``` | |
156 | + logObj.stat.start(); // call this once | |
157 | + logObj.stat.increment('Error 500'); | |
158 | + logObj.stat.increment('Error 400'); | |
159 | + logObj.stat.increment(User + 'user login'); | |
160 | + ``` | |
161 | + At any point in time stop method can be called to stop logging interval | |
162 | + ``` | |
163 | + logObj.stat.stop(); | |
164 | + ``` | |
165 | + ##### Alarm | |
166 | + External functions will be executed before logging in to alarm file. | |
167 | + You don't need to do anything with the alarm module. It will be logged automatically from stat module. | |
168 | + | |
169 | + ##### File management | |
170 | + Example of log file with this configuration: | |
171 | + rotation: 1000 | |
172 | + maxsize: 20000 | |
173 | + | |
174 | + -rw-r--r-- test 20K Jun 20 15:00 2016-06-20T15:00:19_00001.txt | |
175 | + -rw-r--r-- test 11K Jun 20 15:00 2016-06-20T15:00:19_00002.txt | |
176 | + -rw-r--r-- test 20K Jun 20 15:00 2016-06-20T15:00:20_00001.txt | |
177 | + -rw-r--r-- test 11K Jun 20 15:00 2016-06-20T15:00:20_00002.txt | |
178 | + | |
179 | + As you can see, the file is set to maximum at 20Kb and is rotate every 1 second | |
180 | + If the file reaches maxsize before 1 second it will append log into the same time frame with incremental suffix | |
181 | + | |
182 | + | ... | ... |
1 | +++ a/index.js | |
... | ... | @@ -0,0 +1,60 @@ |
1 | +var _ = require('lodash'); | |
2 | +var moment = require('moment'); | |
3 | +var log = require('./model/log'); | |
4 | +var stat = require('./model/stat'); | |
5 | +var alarm = require('./model/alarm'); | |
6 | +var validate = require('./lib/validate'); | |
7 | +var joi = require('joi'); | |
8 | + | |
9 | +module.exports = { | |
10 | + | |
11 | + /* | |
12 | + * opts: { | |
13 | + * log: [dirname, rotation, maxsize] | |
14 | + * stat: [dirname, rotation, maxsize, interval, key] | |
15 | + * alarm: [dirname, rotation, maxsize] | |
16 | + * } | |
17 | + */ | |
18 | + init: function(opts) { | |
19 | + var logstatalarm = undefined; | |
20 | + validate.joi(opts, this.getSchema(), function(err, opts) { | |
21 | + if(err) { | |
22 | + throw new Error(err); | |
23 | + } | |
24 | + }); | |
25 | + return { log: new log(opts.dirname, opts.log), | |
26 | + stat: new stat(opts.dirname, opts.stat, opts.alarm) | |
27 | + }; | |
28 | + }, | |
29 | + | |
30 | + /* | |
31 | + * Pre-define schema | |
32 | + */ | |
33 | + getSchema: function() { | |
34 | + return joi.object().keys({ | |
35 | + dirname: joi.string().required(), | |
36 | + log: joi.object().keys({ | |
37 | + rotation: joi.number().min(0), | |
38 | + maxsize: joi.number().min(0) | |
39 | + }).required(), | |
40 | + stat: joi.object().keys({ | |
41 | + rotation: joi.number().min(0), | |
42 | + maxsize: joi.number().min(0), | |
43 | + interval: joi.number().min(0), | |
44 | + data: joi.array().items(joi.object().keys({ | |
45 | + key: joi.string().required(), | |
46 | + threshold: joi.number().min(0), | |
47 | + threshold_inv: joi.number().min(0) | |
48 | + })) | |
49 | + }).required(), | |
50 | + alarm: joi.object().keys({ | |
51 | + rotation: joi.number().min(0), | |
52 | + maxsize: joi.number().min(0), | |
53 | + external: joi.array().items(joi.object().keys({ | |
54 | + fn: joi.func().required(), | |
55 | + args: joi.array().items(joi.any()) | |
56 | + })) | |
57 | + }).required() | |
58 | + }); | |
59 | + } | |
60 | +}; | ... | ... |
1 | +++ a/lib/helper.js | |
... | ... | @@ -0,0 +1,40 @@ |
1 | +var fs = require('fs'); | |
2 | +var request = require('request-promise').defaults({jar: true}) | |
3 | + | |
4 | +module.exports = { | |
5 | + | |
6 | + isValidFileSize: function(dirname, maxsize) { | |
7 | + return fs.statSync(dirname)['size'] < maxsize; | |
8 | + }, | |
9 | + | |
10 | + getLengthOfContent: function(string) { | |
11 | + return Buffer.byteLength(string, 'utf8'); | |
12 | + }, | |
13 | + | |
14 | + mkdirIfNotExist: function(dirname) { | |
15 | + var _dirname = dirname.split('/'); | |
16 | + var parentdir = _dirname.slice(0, _dirname.length-2).join('/'); | |
17 | + if (!fs.existsSync(parentdir)){ | |
18 | + fs.mkdirSync(parentdir); | |
19 | + } | |
20 | + if (!fs.existsSync(dirname)){ | |
21 | + fs.mkdirSync(dirname); | |
22 | + } | |
23 | + }, | |
24 | + | |
25 | + deleteFolderRecursive: function(path) { | |
26 | + var self = this; | |
27 | + if( fs.existsSync(path) ) { | |
28 | + fs.readdirSync(path).forEach(function(file, index){ | |
29 | + var curPath = path + "/" + file; | |
30 | + if(fs.lstatSync(curPath).isDirectory()) { | |
31 | + self.deleteFolderRecursive(curPath); | |
32 | + } else { | |
33 | + fs.unlinkSync(curPath); | |
34 | + } | |
35 | + }); | |
36 | + fs.rmdirSync(path); | |
37 | + } | |
38 | + } | |
39 | + | |
40 | +}; | ... | ... |
1 | +++ a/lib/validate.js | |
... | ... | @@ -0,0 +1,39 @@ |
1 | +var joi = require('joi'); | |
2 | +var _ = require('lodash'); | |
3 | + | |
4 | +module.exports = { | |
5 | + | |
6 | + joi: function(data, schema, callback) { | |
7 | + opts = { | |
8 | + abortEarly: false, | |
9 | + convert: true, | |
10 | + allowUnknown: false, | |
11 | + stripUnknown: false | |
12 | + } | |
13 | + joi.validate(data, schema, opts, function(err, obj){ | |
14 | + if(err) { | |
15 | + var errs = _.map(err.details, function(e) { | |
16 | + var key = e.path; | |
17 | + var x = e.type; | |
18 | + var _postfix = undefined; | |
19 | + switch(_.last(x.split('.'))) { | |
20 | + case 'required': | |
21 | + _postfix = 'required'; | |
22 | + break; | |
23 | + case 'allowUnknown': | |
24 | + _postfix = 'not allowed'; | |
25 | + break; | |
26 | + default: | |
27 | + _postfix = 'invalid format' | |
28 | + break; | |
29 | + } | |
30 | + return `${key} is ${_postfix}`; | |
31 | + }); | |
32 | + callback(errs, null); | |
33 | + } else { | |
34 | + callback(null, obj); | |
35 | + } | |
36 | + | |
37 | + }) | |
38 | + } | |
39 | +}; | ... | ... |
1 | +++ a/model/alarm.js | |
... | ... | @@ -0,0 +1,102 @@ |
1 | +var fs = require('fs'); | |
2 | +var moment = require('moment'); | |
3 | +var helper = require('../lib/helper'); | |
4 | +var _ = require('lodash'); | |
5 | + | |
6 | +/* | |
7 | + * parameters: | |
8 | + * dirname: string | |
9 | + * rotation: interger (ms) | |
10 | + * maxsize: interger (byte) | |
11 | + */ | |
12 | +function alarm(dirname, opts) { | |
13 | + opts = opts || {}; | |
14 | + this.dirname = dirname; | |
15 | + this.rotation = opts.rotation || 15 * 60 * 1000; | |
16 | + this.maxsize = opts.maxsize || 20000; | |
17 | + this.external = opts.external || []; | |
18 | + this.currentsize = 0; | |
19 | + this.timestamp = 0; | |
20 | + this.foldername = 'alarm/'; | |
21 | + helper.mkdirIfNotExist(`${this.dirname}/${this.foldername}`); | |
22 | +}; | |
23 | + | |
24 | +/* | |
25 | + * parameters: | |
26 | + * data: any | |
27 | + */ | |
28 | +alarm.prototype.appendAlarm = function(data) { | |
29 | + data = this.formatData(data); | |
30 | + this.currentsize = this.currentsize + helper.getLengthOfContent(data); | |
31 | + this.request(data); | |
32 | + fs.appendFile(this.getDir(), data, function(err) {}); | |
33 | +}; | |
34 | + | |
35 | +/* | |
36 | + * parameters: | |
37 | + * none | |
38 | + */ | |
39 | +alarm.prototype.getDir = function() { | |
40 | + var time = moment(Math.floor((+moment()) / this.rotation) * this.rotation); | |
41 | + this.resetCurrentSize(time.unix()); | |
42 | + time = time.format('YYYY-MM-DDTHH-mm-ss'); | |
43 | + var count = this.getCount(); | |
44 | + return `${this.dirname}${this.foldername}${time}_${count}.txt`; | |
45 | +}; | |
46 | + | |
47 | +/* | |
48 | + * parameters: | |
49 | + * time_unix: string | |
50 | + */ | |
51 | +alarm.prototype.resetCurrentSize = function(time_unix) { | |
52 | + if(time_unix > this.timestamp) { | |
53 | + this.currentsize = 0 | |
54 | + this.timestamp = time_unix; | |
55 | + } | |
56 | +}; | |
57 | + | |
58 | +/* | |
59 | + * parameters: | |
60 | + * none | |
61 | + */ | |
62 | +alarm.prototype.getCount = function() { | |
63 | + var count = Math.floor((this.currentsize / this.maxsize) + 1); | |
64 | + return ((count * 1e-5).toFixed(5)).split('.')[1]; | |
65 | +}; | |
66 | + | |
67 | +/* | |
68 | + * parameters: | |
69 | + * data: any | |
70 | + */ | |
71 | +alarm.prototype.formatData = function(data) { | |
72 | + var date = moment().toISOString().trim(); | |
73 | + var timestamp = moment().unix(); | |
74 | + data = this._formatObject(data).trim(); | |
75 | + return `${date} ${timestamp} ${data}\r\n`; | |
76 | +}; | |
77 | + | |
78 | +/* | |
79 | + * parameters: | |
80 | + * data: any | |
81 | + */ | |
82 | +alarm.prototype._formatObject = function(data) { | |
83 | + if(_.isObject(data)) { | |
84 | + return JSON.stringify(data); | |
85 | + } | |
86 | + if(_.isNumber(data)) { | |
87 | + return toString(data); | |
88 | + } | |
89 | + return data; | |
90 | +}; | |
91 | + | |
92 | +/* | |
93 | + * parameters: | |
94 | + * data: any | |
95 | + */ | |
96 | +alarm.prototype.request = function(data) { | |
97 | + _.forEach(this.external, function(external) { | |
98 | + external.fn.apply(this, [data].concat(external.args)); | |
99 | + }); | |
100 | +}; | |
101 | + | |
102 | +module.exports = alarm; | ... | ... |
1 | +++ a/model/log.js | |
... | ... | @@ -0,0 +1,89 @@ |
1 | +var fs = require('fs'); | |
2 | +var moment = require('moment'); | |
3 | +var helper = require('../lib/helper'); | |
4 | +var _ = require('lodash'); | |
5 | + | |
6 | +/* | |
7 | + * parameters: | |
8 | + * dirname: string | |
9 | + * rotation: interger (ms) | |
10 | + */ | |
11 | +function log(dirname, opts) { | |
12 | + opts = opts || {}; | |
13 | + this.dirname = dirname; | |
14 | + this.rotation = opts.rotation || 15 * 60 * 1000; | |
15 | + this.maxsize = opts.maxsize || 20000; | |
16 | + this.currentsize = 0; | |
17 | + this.timestamp = 0; | |
18 | + this.foldername = 'log/'; | |
19 | + helper.mkdirIfNotExist(`${this.dirname}/${this.foldername}`); | |
20 | +}; | |
21 | + | |
22 | +/* | |
23 | + * parameters: | |
24 | + * data: any | |
25 | + */ | |
26 | +log.prototype.append = function(data) { | |
27 | + data = this.formatData(data); | |
28 | + this.currentsize = this.currentsize + helper.getLengthOfContent(data); | |
29 | + fs.appendFile(this.getDir(), data, function(err) {}); | |
30 | +}; | |
31 | + | |
32 | +/* | |
33 | + * parameters: | |
34 | + * none | |
35 | + */ | |
36 | +log.prototype.getDir = function() { | |
37 | + var time = moment(Math.floor((+moment()) / this.rotation) * this.rotation); | |
38 | + this.resetCurrentSize(time.unix()); | |
39 | + time = time.format('YYYY-MM-DDTHH-mm-ss'); | |
40 | + var count = this.getCount(); | |
41 | + return `${this.dirname}${this.foldername}${time}_${count}.txt`; | |
42 | +}; | |
43 | + | |
44 | +/* | |
45 | + * parameters: | |
46 | + * time_unix: string | |
47 | + */ | |
48 | +log.prototype.resetCurrentSize = function(time_unix) { | |
49 | + if(time_unix > this.timestamp) { | |
50 | + this.currentsize = 0 | |
51 | + this.timestamp = time_unix; | |
52 | + } | |
53 | +}; | |
54 | + | |
55 | +/* | |
56 | + * parameters: | |
57 | + * none | |
58 | + */ | |
59 | +log.prototype.getCount = function() { | |
60 | + var count = Math.floor((this.currentsize / this.maxsize) + 1); | |
61 | + return ((count * 1e-5).toFixed(5)).split('.')[1]; | |
62 | +}; | |
63 | + | |
64 | +/* | |
65 | + * parameters: | |
66 | + * data: any | |
67 | + */ | |
68 | +log.prototype.formatData = function(data) { | |
69 | + var date = moment().toISOString().trim(); | |
70 | + var timestamp = moment().unix(); | |
71 | + data = this._formatObject(data).trim(); | |
72 | + return `${date} ${timestamp} ${data}\r\n`; | |
73 | +}; | |
74 | + | |
75 | +/* | |
76 | + * parameters: | |
77 | + * data: any | |
78 | + */ | |
79 | +log.prototype._formatObject = function(data) { | |
80 | + if(_.isObject(data)) { | |
81 | + return JSON.stringify(data); | |
82 | + } | |
83 | + if(_.isNumber(data)) { | |
84 | + return toString(data); | |
85 | + } | |
86 | + return data; | |
87 | +}; | |
88 | + | |
89 | +module.exports = log; | ... | ... |
1 | +++ a/model/stat.js | |
... | ... | @@ -0,0 +1,179 @@ |
1 | +var fs = require('fs'); | |
2 | +var moment = require('moment'); | |
3 | +var helper = require('../lib/helper'); | |
4 | +var _ = require('lodash'); | |
5 | +var alarm = require('./alarm'); | |
6 | + | |
7 | +/* | |
8 | + * parameters: | |
9 | + * dirname: string | |
10 | + * rotation: interger (ms) | |
11 | + * maxsize: interger (byte) | |
12 | + * interval: interger (ms) | |
13 | + * data: array (array of object) | |
14 | + * alarm: object (alarm object) | |
15 | + */ | |
16 | +function stat(dirname, opts, alarmData) { | |
17 | + opts = opts || {}; | |
18 | + alarmData = alarmData || {}; | |
19 | + this.dirname = dirname; | |
20 | + this.rotation = opts.rotation || 15 * 60 * 1000; | |
21 | + this.maxsize = opts.maxsize || 20000; | |
22 | + this.currentsize = 0; | |
23 | + this.timestamp = 0; | |
24 | + this.foldername = 'stat/'; | |
25 | + this.intervalId = undefined; | |
26 | + this.interval = opts.interval || 60 * 1000; | |
27 | + this.data = transformKeys(opts.data); | |
28 | + this.rules = opts.data; | |
29 | + this.alarm = new alarm(dirname, alarmData); | |
30 | + helper.mkdirIfNotExist(`${this.dirname}/${this.foldername}`); | |
31 | +}; | |
32 | + | |
33 | +function transformKeys(data) { | |
34 | + keys = _.map(data, function(obj) { | |
35 | + var fakey = {}; | |
36 | + fakey[obj.key] = 0; | |
37 | + return fakey; | |
38 | + }); | |
39 | + return _.reduce(keys, function(new_obj, obj) { | |
40 | + return _.merge(new_obj, obj); | |
41 | + }, {}); | |
42 | + | |
43 | +}; | |
44 | + | |
45 | +/* | |
46 | + * parameters: | |
47 | + * data: obj | |
48 | + * rules: [obj] | |
49 | + */ | |
50 | +function alarmDataOutOfThreshold(data, rules) { | |
51 | + _alarm = []; | |
52 | + _.forEach(data, function(v, k) { | |
53 | + rule = _.find(rules, ['key', k]); | |
54 | + var alarmObj = {}; | |
55 | + if(v < rule.threshold_inv) { | |
56 | + _alarm.push({key: k, | |
57 | + count: v, | |
58 | + threshold_inv: rule.threshold_inv, | |
59 | + message: `${k} count is below inverted threshold`}); | |
60 | + } | |
61 | + if(v > rule.threshold) { | |
62 | + _alarm.push({key: k, | |
63 | + count: v, | |
64 | + threshold: | |
65 | + rule.threshold, | |
66 | + message: `${k} count is above threshold`}); | |
67 | + } | |
68 | + }); | |
69 | + return _alarm | |
70 | +}; | |
71 | + | |
72 | +/* | |
73 | + * parameters: | |
74 | + * data: any | |
75 | + */ | |
76 | +stat.prototype.appendStat = function(data) { | |
77 | + data = this.formatData(data); | |
78 | + this.currentsize = this.currentsize + helper.getLengthOfContent(data); | |
79 | + fs.appendFile(this.getDir(), data, function(err) {}); | |
80 | +}; | |
81 | + | |
82 | +/* | |
83 | + * parameters: | |
84 | + * none | |
85 | + */ | |
86 | +stat.prototype.start = function() { | |
87 | + var self = this; | |
88 | + this.intervalId = setInterval(function(){ | |
89 | + var alarmData = alarmDataOutOfThreshold(self.data, self.rules); | |
90 | + if(!_.isEmpty(alarmData)) { | |
91 | + self.alarm.appendAlarm(alarmData); | |
92 | + } | |
93 | + self.appendStat(self.data); | |
94 | + self.reset(); | |
95 | + }, self.interval); | |
96 | +}; | |
97 | + | |
98 | +/* | |
99 | + * parameters: | |
100 | + * none | |
101 | + */ | |
102 | +stat.prototype.stop = function() { | |
103 | + clearInterval(this.intervalId); | |
104 | +}; | |
105 | + | |
106 | +/* | |
107 | + * parameters: | |
108 | + * data: string | |
109 | + */ | |
110 | +stat.prototype.increment = function(data) { | |
111 | + if(_.has(this.data, data)) { | |
112 | + this.data[data]++; | |
113 | + } | |
114 | + else this.data[data] = 1; | |
115 | +}; | |
116 | + | |
117 | +/* | |
118 | + * parameters: | |
119 | + * none | |
120 | + */ | |
121 | +stat.prototype.reset = function() { | |
122 | + var self = this; | |
123 | + _.forEach(this.data, function(v, k) { | |
124 | + self.data[k] = 0; | |
125 | + }); | |
126 | +}; | |
127 | + | |
128 | +/* | |
129 | + * parameters: | |
130 | + * none | |
131 | + */ | |
132 | +stat.prototype.getDir = function() { | |
133 | + var time = moment(Math.floor((+moment()) / this.rotation) * this.rotation); | |
134 | + this.resetCurrentSize(time.unix()); | |
135 | + time = time.format('YYYY-MM-DDTHH-mm-ss'); | |
136 | + var count = this.getCount(); | |
137 | + return `${this.dirname}${this.foldername}${time}_${count}.txt`; | |
138 | +}; | |
139 | + | |
140 | +/* | |
141 | + * parameters: | |
142 | + * time_unix: string | |
143 | + */ | |
144 | +stat.prototype.resetCurrentSize = function(time_unix) { | |
145 | + if(time_unix > this.timestamp) { | |
146 | + this.currentsize = 0 | |
147 | + this.timestamp = time_unix; | |
148 | + } | |
149 | +}; | |
150 | + | |
151 | +/* | |
152 | + * parameters: | |
153 | + * none | |
154 | + */ | |
155 | +stat.prototype.getCount = function() { | |
156 | + var count = Math.floor((this.currentsize / this.maxsize) + 1); | |
157 | + return ((count * 1e-5).toFixed(5)).split('.')[1]; | |
158 | +}; | |
159 | + | |
160 | +/* | |
161 | + * parameters: | |
162 | + * data: any | |
163 | + */ | |
164 | +stat.prototype.formatData = function(data) { | |
165 | + var date = moment().toISOString().trim(); | |
166 | + var timestamp = moment().unix(); | |
167 | + data = this._formatObject(data).trim(); | |
168 | + return `${date} ${timestamp} ${data}\r\n`; | |
169 | +}; | |
170 | + | |
171 | +/* | |
172 | + * parameters: | |
173 | + * data: any | |
174 | + */ | |
175 | +stat.prototype._formatObject = function(data) { | |
176 | + return JSON.stringify(data); | |
177 | +}; | |
178 | + | |
179 | +module.exports = stat; | ... | ... |
1 | +++ a/package.json | |
... | ... | @@ -0,0 +1,19 @@ |
1 | +{ | |
2 | + "name": "logstatalarm", | |
3 | + "version": "1.0.0", | |
4 | + "description": "", | |
5 | + "main": "index.js", | |
6 | + "scripts": { | |
7 | + "test": "mocha" | |
8 | + }, | |
9 | + "author": "pupuupup", | |
10 | + "license": "ISC", | |
11 | + "dependencies": { | |
12 | + "bluebird": "^3.4.0", | |
13 | + "chai": "^3.5.0", | |
14 | + "joi": "^8.4.2", | |
15 | + "lodash": "^4.13.1", | |
16 | + "moment": "^2.13.0", | |
17 | + "request-promise": "^3.0.0" | |
18 | + } | |
19 | +} | ... | ... |
1 | +++ a/test/alarm.js | |
... | ... | @@ -0,0 +1,54 @@ |
1 | +require('./setup'); | |
2 | +var alarm = require('../model/alarm'); | |
3 | +var _ = require('lodash'); | |
4 | +var Promise = require('bluebird'); | |
5 | +var fs = require('fs'); | |
6 | + | |
7 | +describe('Alarm Model', function() { | |
8 | + | |
9 | + var testData1, testData2; | |
10 | + var testAString = "helloworld"; | |
11 | + var aRequestFunction1 = function(data, aString){ | |
12 | + testData1 = data + "," + aString; | |
13 | + }; | |
14 | + var aRequestFunction2 = function(data, aString1, aString2){ | |
15 | + testData2 = aString1 + "," + data + "," + aString2; | |
16 | + }; | |
17 | + var validData = {} | |
18 | + data = { | |
19 | + rotation: 5000, | |
20 | + external: [ | |
21 | + { fn: aRequestFunction1 ,args: [testAString] }, | |
22 | + { fn: aRequestFunction2 ,args: [testAString, testAString] } | |
23 | + ] | |
24 | + }; | |
25 | + var alarm_object = new alarm('./log_test/', data); | |
26 | + | |
27 | + before(function(done) { | |
28 | + validData = ['lksdjfjklsdlfjll']; | |
29 | + done(); | |
30 | + }); | |
31 | + | |
32 | + it('should appendAlarm asynchronously correctly', function(done) { | |
33 | + Promise.all(_.map(_.times(200, String), function(n) { | |
34 | + return Promise.resolve(alarm_object.appendAlarm(validData)); | |
35 | + })) | |
36 | + .then(function() { | |
37 | + return Promise.delay(1100); | |
38 | + }) | |
39 | + .then(function() { | |
40 | + Promise.all(_.map(_.times(200, String), function(n) { | |
41 | + return Promise.resolve(alarm_object.appendAlarm(validData)); | |
42 | + })) | |
43 | + }) | |
44 | + .then(function() { | |
45 | + expect(testData1.split(",")[1]).eql(testAString) | |
46 | + expect(testData2.split(",")[0]).eql(testAString) | |
47 | + expect(testData2.split(",")[2]).eql(testAString) | |
48 | + }) | |
49 | + .then(function() { | |
50 | + done(); | |
51 | + }); | |
52 | + }); | |
53 | + | |
54 | +}); | ... | ... |
1 | +++ a/test/index.js | |
... | ... | @@ -0,0 +1,134 @@ |
1 | +require('./setup'); | |
2 | +var index = require('../index'); | |
3 | +var Promise = require('bluebird'); | |
4 | + | |
5 | +describe('Main index.js', function() { | |
6 | + | |
7 | + var validData = {} | |
8 | + before(function(done) { | |
9 | + validData = { | |
10 | + dirname: './log_test/', | |
11 | + log: {}, | |
12 | + stat: { | |
13 | + interval: 2, | |
14 | + data: [ | |
15 | + {key: 'a', threshold: 2, threshold_inv: 1}, | |
16 | + {key: 'b', threshold: 2 } | |
17 | + ] | |
18 | + }, | |
19 | + alarm: {} | |
20 | + } | |
21 | + done(); | |
22 | + }); | |
23 | + | |
24 | + it('init function should return true on cb properly', function(done) { | |
25 | + var object = index.init(validData); | |
26 | + expect(object).to.be.an('object'); | |
27 | + expect(object).to.have.property('log'); | |
28 | + expect(object).to.have.property('stat'); | |
29 | + done(); | |
30 | + }); | |
31 | + | |
32 | + it('init function should return err on cb properly (case 1)', function(done) { | |
33 | + try { | |
34 | + var object = index.init({log: 'hi'}); | |
35 | + } | |
36 | + catch (err) { | |
37 | + var my_err = err.message.split(','); | |
38 | + expect(my_err).to.include.members(['log is invalid format', 'dirname is required', 'alarm is required']); | |
39 | + done(); | |
40 | + } | |
41 | + }); | |
42 | + | |
43 | + it('init function should return err on cb properly (case 2)', function(done) { | |
44 | + try { | |
45 | + var object = index.init({pupu: 'hi'}); | |
46 | + } | |
47 | + catch (err) { | |
48 | + var my_err = err.message.split(','); | |
49 | + expect(my_err).to.include.members(['pupu is not allowed']); | |
50 | + done(); | |
51 | + } | |
52 | + }); | |
53 | + | |
54 | + it('init function should return err on cb properly (case 3)', function(done) { | |
55 | + try { | |
56 | + var object = index.init({pupu: 'hi'}); | |
57 | + } | |
58 | + catch (err) { | |
59 | + var my_err = err.message.split(','); | |
60 | + expect(my_err).to.include.members(['log is required', 'stat is required']); | |
61 | + done(); | |
62 | + } | |
63 | + }); | |
64 | + | |
65 | + it('should fail to create stat object if validdata object contain no key with threshold', function(done) { | |
66 | + var testData = { | |
67 | + dirname: './log_test/', | |
68 | + log: {}, | |
69 | + stat: { | |
70 | + interval: 2, | |
71 | + data: [ | |
72 | + {key: 'a', threshold: 2, threshold_inv: 1}, | |
73 | + {key: 'b', threshold: 2}, | |
74 | + {key: 'c', threshold_inv: 1}, | |
75 | + {key: 'd', threshold: 2}, | |
76 | + {threshold: 2}, | |
77 | + ] | |
78 | + } | |
79 | + } | |
80 | + try { | |
81 | + var object = index.init(testData); | |
82 | + } | |
83 | + catch (err) { | |
84 | + var my_err = err.message.split(','); | |
85 | + expect(my_err).to.include.members(['stat.data.4.key is required']); | |
86 | + done(); | |
87 | + } | |
88 | + }); | |
89 | + | |
90 | + it('should run the whole flow correctly', function(done) { | |
91 | + var initData = { | |
92 | + dirname: './whole_flow/', | |
93 | + log: { | |
94 | + rotation: 500, | |
95 | + maxsize: 500 | |
96 | + }, | |
97 | + stat: { | |
98 | + rotation: 500, | |
99 | + maxsize: 500, | |
100 | + interval: 5, | |
101 | + data: [ | |
102 | + {key: 'testIncrement1', threshold: 3, threshold_inv: 1}, | |
103 | + {key: 'testIncrement2', threshold: 2 } | |
104 | + ] | |
105 | + }, | |
106 | + alarm: { | |
107 | + rotation: 500, | |
108 | + maxsize: 500 | |
109 | + } | |
110 | + }; | |
111 | + var logObj = index.init(initData); | |
112 | + Promise.try(function() {}) | |
113 | + .then(function() { | |
114 | + logObj.stat.start(); | |
115 | + logObj.log.append({hi: 'test'}); | |
116 | + logObj.stat.increment('testIncrement1'); | |
117 | + return Promise.delay(100); | |
118 | + }) | |
119 | + .then(function() { | |
120 | + logObj.log.append({hi: 'test2'}); | |
121 | + logObj.stat.increment('testIncrement2'); | |
122 | + logObj.log.append({hi: 'test2'}); | |
123 | + logObj.stat.increment('testIncrement2'); | |
124 | + }) | |
125 | + .then(function() { | |
126 | + logObj.stat.stop(); | |
127 | + done(); | |
128 | + }) | |
129 | + | |
130 | + | |
131 | + }); | |
132 | + | |
133 | + | |
134 | +}); | ... | ... |
1 | +++ a/test/lib/helper.js | |
... | ... | @@ -0,0 +1,26 @@ |
1 | +require('../setup'); | |
2 | +var helper = require('../../lib/helper'); | |
3 | + | |
4 | +describe('Lib helper.js', function() { | |
5 | + | |
6 | + var _dirname = './test/mocha.opts'; | |
7 | + before(function(done) { | |
8 | + done(); | |
9 | + }); | |
10 | + | |
11 | + it('isValidFileSize should return true when file size is less than maxsize', function(done) { | |
12 | + var _maxsize = 200; | |
13 | + bool = helper.isValidFileSize(_dirname, _maxsize); | |
14 | + expect(bool).to.be.true; | |
15 | + done(); | |
16 | + }); | |
17 | + | |
18 | + it('isValidFileSize should return false when file size exceed max size', function(done) { | |
19 | + var _maxsize = 10; | |
20 | + bool = helper.isValidFileSize(_dirname, _maxsize); | |
21 | + expect(bool).to.be.false; | |
22 | + done(); | |
23 | + }); | |
24 | + | |
25 | + | |
26 | +}); | ... | ... |
1 | +++ a/test/log.js | |
... | ... | @@ -0,0 +1,62 @@ |
1 | +require('./setup'); | |
2 | +var log = require('../model/log'); | |
3 | +var _ = require('lodash'); | |
4 | +var Promise = require('bluebird'); | |
5 | +var fs = require('fs'); | |
6 | + | |
7 | +describe('Log Model', function() { | |
8 | + | |
9 | + var validData = {} | |
10 | + var log_object = new log('./log_test/', {rotation: 1000, maxsize: 20000}); | |
11 | + | |
12 | + before(function(done) { | |
13 | + validData = { | |
14 | + path: "/test", | |
15 | + method: "GET", | |
16 | + request: { | |
17 | + body: {test: true} | |
18 | + }, | |
19 | + response: { | |
20 | + statusCode: 200, | |
21 | + body: { success: true } | |
22 | + } | |
23 | + }; | |
24 | + done(); | |
25 | + }); | |
26 | + | |
27 | + it('should formatObject correctly', function(done) { | |
28 | + var data = log_object._formatObject(validData); | |
29 | + expect(data).eql(JSON.stringify(validData)); | |
30 | + done(); | |
31 | + }); | |
32 | + | |
33 | + it('should formatData correctly', function(done) { | |
34 | + var data = log_object.formatData(validData).split(' '); | |
35 | + expect(_.last(data).slice(0, _.last(data).length-1).trim().split('\r\n').join('')) | |
36 | + .eql(JSON.stringify(validData).trim().split('\r\n').join('')); | |
37 | + done(); | |
38 | + }); | |
39 | + | |
40 | + it('should appendLog correctly', function(done) { | |
41 | + log_object.append(validData); | |
42 | + done(); | |
43 | + }); | |
44 | + | |
45 | + it('should appendLog asynchronously correctly', function(done) { | |
46 | + Promise.all(_.map(_.times(200, String), function(n) { | |
47 | + return Promise.resolve(log_object.append(validData)); | |
48 | + })) | |
49 | + .then(function() { | |
50 | + return Promise.delay(1100); | |
51 | + }) | |
52 | + .then(function() { | |
53 | + Promise.all(_.map(_.times(200, String), function(n) { | |
54 | + return Promise.resolve(log_object.append(validData)); | |
55 | + })) | |
56 | + }) | |
57 | + .then(function() { | |
58 | + done(); | |
59 | + }); | |
60 | + }); | |
61 | + | |
62 | +}); | ... | ... |
1 | +++ a/test/stat.js | |
... | ... | @@ -0,0 +1,49 @@ |
1 | +require('./setup'); | |
2 | +var stat = require('../model/stat'); | |
3 | +var _ = require('lodash'); | |
4 | +var Promise = require('bluebird'); | |
5 | +var fs = require('fs'); | |
6 | + | |
7 | +describe('Stat Model', function() { | |
8 | + | |
9 | + var validData = [ | |
10 | + {key: 'a', threshold: 2, threshold_inv: 1}, | |
11 | + {key: 'b', threshold: 2}, | |
12 | + {key: 'c', threshold_inv: 1}, | |
13 | + {key: 'd', threshold: 2} | |
14 | + ] | |
15 | + | |
16 | + var stat_object = new stat('./log_test/', {rotation:5000, maxsize:5000, interval:1, data:validData}) | |
17 | + | |
18 | + before(function(done) { | |
19 | + validData = ['a', 'b']; | |
20 | + done(); | |
21 | + }); | |
22 | + | |
23 | + it('should stat correctly (whole process)', function(done) { | |
24 | + stat_object.start(); | |
25 | + return Promise.delay(100) | |
26 | + .then(function() { | |
27 | + Promise.all(_.map(_.times(100, String), function(){ | |
28 | + stat_object.increment(validData[0]); | |
29 | + })) | |
30 | + }) | |
31 | + .then(function() { | |
32 | + return Promise.delay(100) | |
33 | + }) | |
34 | + .then(function() { | |
35 | + stat_object.increment(validData[0]); | |
36 | + stat_object.increment(validData[1]); | |
37 | + }) | |
38 | + .then(function() { | |
39 | + return Promise.delay(100) | |
40 | + }) | |
41 | + .then(function() { | |
42 | + stat_object.stop(); | |
43 | + done(); | |
44 | + }) | |
45 | + | |
46 | + }); | |
47 | + | |
48 | + | |
49 | +}); | ... | ... |