Skip to content

Commit 5430617

Browse files
idantoviidantovi
idantovi
authored and
idantovi
committedSep 12, 2016
first commit
0 parents  commit 5430617

8 files changed

+987
-0
lines changed
 

‎.gitignore

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/typings
2+
/coverage
3+
/mochawesome-reports
4+
5+
# VS Code
6+
launch.json
7+
8+
# Logs
9+
logs
10+
*.log
11+
npm-debug.log*
12+
13+
# Runtime data
14+
pids
15+
*.pid
16+
*.seed
17+
18+
# Directory for instrumented libs generated by jscoverage/JSCover
19+
lib-cov
20+
21+
# Coverage directory used by tools like istanbul
22+
coverage
23+
24+
# nyc test coverage
25+
.nyc_output
26+
27+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
28+
.grunt
29+
30+
# node-waf configuration
31+
.lock-wscript
32+
33+
# Compiled binary addons (http://nodejs.org/api/addons.html)
34+
build/Release
35+
36+
# Dependency directories
37+
node_modules
38+
jspm_packages
39+
40+
# Optional npm cache directory
41+
.npm
42+
43+
# Optional REPL history
44+
.node_repl_history

‎index.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var middleware = require('./metrics');
2+
var metrics = require('./metricsModel');

‎metrics.js

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
'use strict';
2+
var measured = require('measured');
3+
// var metricsCollection = measured.createCollection();
4+
var gc = (require('gc-stats'))();
5+
var eventLoopStats = require("event-loop-stats");
6+
var memwatch = require('memwatch-next');
7+
var schedule = require('node-schedule');
8+
var usage = require('pidusage');
9+
var metrics = {};
10+
var trackedMetrics = {};
11+
var interval = 1000; // how often to refresh our measurement
12+
var cpuUsage;
13+
14+
15+
var CATEGORIES = {
16+
all: 'global.all',
17+
statuses: 'statuses',
18+
methods: 'methods',
19+
endpoints: 'endpoints'
20+
};
21+
22+
var NAMESPACES = {
23+
process: 'process',
24+
internalMetrics: 'internalMetrics',
25+
apiMetrics: 'apiMetrics'
26+
}
27+
28+
var cpuUsageScheduleJob;
29+
30+
metrics.getAll = function (reset) {
31+
var metricsAsJson = JSON.stringify(trackedMetrics);
32+
if (reset)
33+
resetAll();
34+
return metricsAsJson;
35+
}
36+
37+
metrics.processMetrics = function (reset) {
38+
var metricsAsJson = JSON.stringify(trackedMetrics[NAMESPACES.process]);
39+
if (reset)
40+
resetProcessMetrics();
41+
return metricsAsJson;
42+
}
43+
44+
metrics.apiMetrics = function (reset) {
45+
var metricsAsJson = JSON.stringify(trackedMetrics[NAMESPACES.apiMetrics]);
46+
if (reset)
47+
resetMetric(NAMESPACES.apiMetrics);
48+
return metricsAsJson;
49+
}
50+
51+
metrics.internalMetrics = function (reset) {
52+
var metricsAsJson = JSON.stringify(trackedMetrics[NAMESPACES.internalMetrics]);
53+
if (reset)
54+
resetMetric(NAMESPACES.internalMetrics);
55+
return metricsAsJson;
56+
}
57+
58+
metrics.logInternalMetric = function (info, err) {
59+
var status = "success";
60+
61+
if (err) {
62+
status = "failure";
63+
}
64+
65+
addInnerIO({
66+
destenation: info.source,
67+
method: info.methodName,
68+
status: status,
69+
elapsedTime: Date.now() - info.startTime
70+
});
71+
}
72+
73+
metrics.addApiData = function (message) {
74+
var metricName = getMetricName(message.route, message.method);
75+
// var path = message.route ? message.route.path : undefined;
76+
77+
updateMetric(NAMESPACES.apiMetrics + '.' + CATEGORIES.all, message.time);
78+
updateMetric(NAMESPACES.apiMetrics + '.' + CATEGORIES.statuses + '.' + message.status, message.time);
79+
updateMetric(NAMESPACES.apiMetrics + '.' + CATEGORIES.methods + '.' + message.method, message.time);
80+
updateMetric(NAMESPACES.apiMetrics + '.' + CATEGORIES.endpoints + '.' + metricName, message.time);
81+
};
82+
83+
function getMetricName(route, methodName) {
84+
return route + '|' + methodName.toLowerCase();
85+
};
86+
87+
function addInnerIO(message) {
88+
updateMetric(NAMESPACES.internalMetrics + '.' + message.destenation + '.' + CATEGORIES.all, message.elapsedTime);
89+
updateMetric(NAMESPACES.internalMetrics + '.' + message.destenation + '.' + CATEGORIES.statuses + '.' + message.status, message.elapsedTime);
90+
updateMetric(NAMESPACES.internalMetrics + '.' + message.destenation + '.' + CATEGORIES.methods + '.' + message.method, message.elapsedTime)
91+
}
92+
93+
function _evtparse(eventName) {
94+
var namespaces = eventName.split('.');
95+
96+
var name1;
97+
var levels = namespaces.length;
98+
var name = namespaces.pop(),
99+
category = namespaces.pop(),
100+
namespace = namespaces.pop();
101+
102+
if (levels == 4) {
103+
name1 = name;
104+
name = category;
105+
category = namespace;
106+
namespace = namespaces.pop();
107+
}
108+
109+
return {
110+
ns: namespace,
111+
name: name,
112+
name1: name1,
113+
category: category
114+
}
115+
}
116+
117+
function addMetric(eventName, metric) {
118+
var parts = _evtparse(eventName);
119+
var metricsPath;
120+
121+
if (!trackedMetrics[parts.ns]) {
122+
trackedMetrics[parts.ns] = {};
123+
}
124+
if (!trackedMetrics[parts.ns][parts.category]) {
125+
trackedMetrics[parts.ns][parts.category] = {};
126+
}
127+
if (!trackedMetrics[parts.ns][parts.category][parts.name]) {
128+
if (parts.name1) {
129+
trackedMetrics[parts.ns][parts.category][parts.name] = {}
130+
}
131+
else {
132+
trackedMetrics[parts.ns][parts.category][parts.name] = metric;
133+
}
134+
}
135+
136+
if ((parts.name1) && (!trackedMetrics[parts.ns][parts.category][parts.name][parts.name1])) {
137+
trackedMetrics[parts.ns][parts.category][parts.name][parts.name1] = metric;
138+
}
139+
140+
if(parts.name1) {
141+
return trackedMetrics[parts.ns][parts.category][parts.name][parts.name1];
142+
}
143+
else {
144+
return trackedMetrics[parts.ns][parts.category][parts.name];
145+
}
146+
}
147+
148+
function updateMetric(name, elapsedTime) {
149+
var metric = addMetric(name, new measured.Timer());
150+
metric.update(elapsedTime);
151+
}
152+
153+
function addProcessMetrics() {
154+
memwatch.on('leak', function (info) {
155+
trackedMetrics[NAMESPACES.process]["memory"]["leak"] = info;
156+
});
157+
158+
gc.removeAllListeners('stats');
159+
gc.on('stats', function (stats) {
160+
updateMetric(NAMESPACES.process + ".gc.time", stats.pauseMS);
161+
//in bytes
162+
updateMetric(NAMESPACES.process + ".gc.releasedMem", stats.diff.usedHeapSize);
163+
});
164+
165+
addMetric(NAMESPACES.process + ".cpu.usage", new measured.Gauge(function () {
166+
return cpuUsage;
167+
}))
168+
169+
addMetric(NAMESPACES.process + ".memory.usage", new measured.Gauge(function () {
170+
//in bytes
171+
return process.memoryUsage();
172+
}));
173+
174+
addMetric(NAMESPACES.process + ".eventLoop.latency", new measured.Gauge(function () {
175+
return eventLoopStats.sense();
176+
}));
177+
178+
setCpuUsageScheduleJob();
179+
}
180+
181+
function setCpuUsageScheduleJob() {
182+
if (cpuUsageScheduleJob) {
183+
cpuUsageScheduleJob.cancel();
184+
}
185+
cpuUsageScheduleJob = schedule.scheduleJob('*/1 * * * *', function () {
186+
var pid = process.pid;
187+
usage.stat(pid, function (err, result) {
188+
cpuUsage = result.cpu;
189+
});
190+
});
191+
}
192+
193+
function resetAll() {
194+
resetProcessMetrics();
195+
resetMetric(NAMESPACES.apiMetrics);
196+
resetMetric(NAMESPACES.internalMetrics);
197+
}
198+
199+
function resetProcessMetrics() {
200+
resetMetric(NAMESPACES.process);
201+
addProcessMetrics();
202+
}
203+
204+
function resetMetric(namespaceToReset) {
205+
delete trackedMetrics[namespaceToReset];
206+
}
207+
208+
addProcessMetrics();
209+
210+
module.exports = metrics;

‎middleware.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
var metricsModel = require('./metrics');
3+
4+
/**
5+
* middleware for express in order to add start time and decorate the end method
6+
* at the end it will add the data to the metrics
7+
* @param {any} req
8+
* @param {any} res
9+
* @param {any} next
10+
*/
11+
module.exports = function (req, res, next) {
12+
req.startTime = new Date();
13+
// decorate response#end method from express
14+
var end = res.end;
15+
res.end = function () {
16+
var responseTime = new Date() - req.startTime
17+
18+
res.setHeader('X-Response-Time', responseTime + 'ms');
19+
20+
// call to original express#res.end()
21+
end.apply(res, arguments);
22+
23+
if (!req.originalUrl.includes('metrics')) {
24+
metricsModel.addApiData({
25+
route: req.baseUrl + req.route.path,
26+
method: req.method,
27+
status: res.statusCode,
28+
time: responseTime
29+
});
30+
}
31+
};
32+
33+
next();
34+
}

‎package.json

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "node-metrics",
3+
"version": "1.0.0",
4+
"description": "metrics for node+express application",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "./node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- --reporter mochawesome"
8+
},
9+
"keywords": [
10+
"metrics",
11+
"node",
12+
"express"
13+
],
14+
"author": "Idan Tovi",
15+
"license": "MIT",
16+
"devDependencies": {
17+
"chai": "^3.5.0",
18+
"istanbul": "^0.4.5",
19+
"measured": "^1.1.0",
20+
"mocha": "^3.0.2",
21+
"mochawesome": "^1.5.2",
22+
"node-mocks-http": "^1.5.3",
23+
"rewire": "^2.5.2",
24+
"sinon": "^1.17.5",
25+
"sleep": "^4.0.0"
26+
},
27+
"dependencies": {
28+
"event-loop-stats": "^1.0.0",
29+
"memwatch-next": "^0.3.0",
30+
"node-schedule": "^1.1.1",
31+
"pidusage": "^1.0.7"
32+
}
33+
}

‎test/metricsTest.js

+504
Large diffs are not rendered by default.

‎test/middlewareTests.js

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
'use strict';
2+
var sinon = require("sinon");
3+
var should = require('chai').should();
4+
var httpMocks = require('node-mocks-http');
5+
var metrics = require("../middleware");
6+
var clock;
7+
var metricsModel = require("../metrics");
8+
var sandbox;
9+
10+
describe('middleware tests', function () {
11+
12+
before(function (done) {
13+
clock = sinon.useFakeTimers();
14+
done();
15+
});
16+
17+
after(function (done) {
18+
clock.restore();
19+
done();
20+
});
21+
22+
describe('gets regular request', function () {
23+
var res = httpMocks.createResponse();
24+
var end = sinon.spy();
25+
res.end = end;
26+
var req = httpMocks.createRequest({
27+
method: 'GET',
28+
url: '/user/42',
29+
params: {
30+
id: 42
31+
}
32+
});
33+
req.baseUrl = '/user';
34+
req.route = {};
35+
req.route.path = ':id';
36+
var metricsModelSpy;
37+
38+
before(function () {
39+
sandbox = sinon.sandbox.create();
40+
metricsModelSpy = sandbox.spy(metricsModel, 'addApiData');
41+
});
42+
43+
after(function () {
44+
sandbox.restore();
45+
});
46+
47+
it('should return new end method to res', function (done) {
48+
metrics(req, res, function () {
49+
res.end.should.not.be.equal(end);
50+
done();
51+
})
52+
});
53+
54+
it('should set start time to req', function (done) {
55+
should.exist(req.startTime);
56+
req.startTime.should.deep.equal(new Date());
57+
done();
58+
});
59+
60+
describe('test the end method of res', function () {
61+
before(function (done) {
62+
clock.tick(10);
63+
res.end();
64+
done();
65+
});
66+
67+
it('should add X-Response-Time header', function (done) {
68+
should.exist(res.getHeader("X-Response-Time"));
69+
res.getHeader("X-Response-Time").should.equal("10ms");
70+
done();
71+
});
72+
73+
it('should run the original end method', function (done) {
74+
end.calledOnce.should.be.true;
75+
done();
76+
});
77+
78+
it('should add metrics data', function (done) {
79+
metricsModelSpy.calledOnce.should.be.true;
80+
done();
81+
});
82+
});
83+
});
84+
85+
describe('gets metrics request', function () {
86+
var res = httpMocks.createResponse();
87+
var end = sinon.spy();
88+
res.end = end;
89+
var req = httpMocks.createRequest({
90+
method: 'GET',
91+
url: '/metrics/42',
92+
params: {
93+
}
94+
});
95+
req.baseUrl = '/metrics';
96+
req.route = {};
97+
req.route.path = ':id';
98+
var metricsModelSpy;
99+
100+
before(function () {
101+
sandbox = sinon.sandbox.create();
102+
metricsModelSpy = sandbox.spy(metricsModel, 'addApiData');
103+
});
104+
105+
after(function () {
106+
sandbox.restore();
107+
});
108+
109+
it('should return new end method to res', function (done) {
110+
metrics(req, res, function () {
111+
res.end.should.not.be.equal(end);
112+
done();
113+
})
114+
});
115+
116+
it('should set start time to req', function (done) {
117+
should.exist(req.startTime);
118+
req.startTime.should.deep.equal(new Date());
119+
done();
120+
});
121+
122+
describe('test the end method of res', function () {
123+
124+
before(function (done) {
125+
clock.tick(10);
126+
res.end();
127+
done();
128+
});
129+
130+
it('should add X-Response-Time header', function (done) {
131+
should.exist(res.getHeader("X-Response-Time"));
132+
res.getHeader("X-Response-Time").should.equal("10ms");
133+
done();
134+
});
135+
136+
it('should run the original end method', function (done) {
137+
end.calledOnce.should.be.true;
138+
done();
139+
});
140+
141+
it('should add metrics data', function (done) {
142+
metricsModelSpy.calledOnce.should.be.false;
143+
done();
144+
});
145+
});
146+
});
147+
});

‎typings.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"globalDevDependencies": {
3+
"chai": "registry:dt/chai#3.4.0+20160601211834",
4+
"istanbul": "registry:dt/istanbul#0.4.0+20160316155526",
5+
"mocha": "registry:dt/mocha#2.2.5+20160720003353",
6+
"rewire": "registry:dt/rewire#2.5.1+20160317120654",
7+
"sinon": "registry:dt/sinon#1.16.0+20160517064723",
8+
"sleep": "registry:dt/sleep#0.0.0+20160728152313"
9+
},
10+
"globalDependencies": {
11+
"node-schedule": "registry:dt/node-schedule#1.1.0+20160521153413"
12+
}
13+
}

0 commit comments

Comments
 (0)
Please sign in to comment.