├── .editorconfig ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── builder.js ├── chrono.js ├── client.cluster.proxy.js ├── client.js ├── client.proxy.js ├── header.js ├── logger.js ├── options.checker.js ├── server.cluster.proxy.js ├── server.js └── server.proxy.js ├── package.json └── test ├── integration ├── middleware.cluster.spec.js └── middleware.spec.js ├── unit ├── chrono.spec.js ├── header.spec.js ├── middleware.spec.js └── statsd.spec.js └── util ├── app.cluster.launcher.js ├── app.js ├── app.launcher.js ├── array.generator.js └── quest.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | 11 | pids 12 | logs 13 | results 14 | 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr": 10, 3 | "bitwise": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "freeze": true, 8 | "forin": true, 9 | "immed": false, 10 | "indent": 2, 11 | "latedef": false, 12 | "newcap": false, 13 | "noarg": true, 14 | "noempty": true, 15 | "nonbsp": true, 16 | "nonew": false, 17 | "plusplus": false, 18 | "quotmark": "single", 19 | "undef": true, 20 | "unused": true, 21 | "strict": false, 22 | "maxparams": false, 23 | "maxdepth": false, 24 | "maxstatements": false, 25 | "maxcomplexity": false, 26 | "maxlen": false, 27 | "smarttabs": true, 28 | "regexp": true, 29 | "asi": false, 30 | "boss": false, 31 | "debug": false, 32 | "eqnull": false, 33 | "es5": false, 34 | "esnext": false, 35 | "evil": false, 36 | "expr": true, 37 | "funcscope": false, 38 | "globalstrict": true, 39 | "iterator": false, 40 | "lastsemic": false, 41 | "laxbreak": false, 42 | "laxcomma": false, 43 | "loopfunc": false, 44 | "multistr": false, 45 | "noyield": false, 46 | "notypeof": false, 47 | "proto": false, 48 | "scripturl": false, 49 | "shadow": false, 50 | "sub": false, 51 | "supernew": false, 52 | "validthis": false, 53 | "mocha": true, 54 | "node": true, 55 | "globals": { 56 | "describe": false, 57 | "before": false, 58 | "beforeEach": false, 59 | "after": false, 60 | "afterEach": false, 61 | "it": false, 62 | "expect": false, 63 | "sinon": true 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | test/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | - __Cluster mode available!__. Now, application that uses more than one process can be monitored as a single application. 3 | - Removed __.jsonSummary__ and __.getSummary__ methods. Now, express-metrics launches a new server that exposes the metrics on another port. The metrics can be reached on _/metrics_ path. 4 | 5 | ## 0.5.3 6 | - Now test are launched by the runner installed locally 7 | 8 | ## 0.5.2 9 | - Integrated Travis CI 10 | 11 | ## 0.5.0 12 | - Improved metrics report. 13 | 14 | ## 0.4.0 15 | - Added header "X-Response-Time" with the response time in ms. 16 | 17 | ## 0.3.0 18 | - Now options can be passed for customization. 19 | 20 | ## 0.2.0 21 | - new API: 22 | - summary -> jsonSummary 23 | - getData -> getSummary 24 | 25 | ## 0.1.0 26 | - Initial version 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Daniel García Aubert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-metrics 2 | 3 | [![Build Status](https://travis-ci.org/dgaubert/express-metrics.svg?branch=master)](https://travis-ci.org/dgaubert/express-metrics) 4 | 5 | Express middleware for collecting and reporting metrics about response times. 6 | 7 | ## Installation 8 | 9 | On project path: 10 | 11 | ``` 12 | npm install express-metrics --save 13 | ``` 14 | 15 | ## Example 16 | 17 | Express projects: 18 | 19 | ```js 20 | var express = require('express'); 21 | var expressMetrics = require('express-metrics'); 22 | var app = express(); 23 | 24 | // start a metrics server 25 | app.use(expressMetrics({ 26 | port: 8091 27 | })); 28 | 29 | // every time this handler returns the greet, express-metrics 30 | // will update the metrics with the calculated response time 31 | app.get('/', function (req, res, next) { 32 | res.json({ greet: 'Hello world!' }); 33 | }); 34 | ``` 35 | 36 | In _:8091/metrics_: 37 | ```js 38 | { 39 | global: { 40 | all: { 41 | type: "timer", 42 | duration: { 43 | type: "histogram", 44 | min: 0, 45 | max: 109.713, 46 | sum: 674.927, 47 | variance: 239.8825911142156, 48 | mean: 5.624391666666665, 49 | std_dev: 15.488143565780103, 50 | count: 1, 51 | median: 0.8055000000000001, 52 | p75: 1.738, 53 | p95: 31.57105, 54 | p99: 107.1568799999999, 55 | p999: 109.713 56 | }, 57 | rate: { 58 | type: "meter", 59 | count: 1, 60 | m1: 2.2284012252758894, 61 | m5: 4.550172188270242, 62 | m15: 5.220474962604762, 63 | mean: 1.3997597079168076, 64 | unit: "seconds" 65 | } 66 | }, 67 | static: { 68 | type: "timer", 69 | duration: { 70 | ... 71 | }, 72 | rate: { 73 | ... 74 | } 75 | } 76 | }, 77 | status: { 78 | 200: { 79 | ... 80 | }, 81 | 302: { 82 | ... 83 | }, 84 | }, 85 | method: { 86 | get: { 87 | ... 88 | }, 89 | post: { 90 | ... 91 | }, 92 | ... 93 | }, 94 | '/blog': { 95 | get: { 96 | ... 97 | } 98 | }, 99 | '/blog/:slug': { 100 | post: { 101 | ... 102 | } 103 | } 104 | } 105 | ``` 106 | Metrics are grouped by: 107 | - global, all and statics (i.e. global: { all: {...}, static: {...} }) 108 | - code status (i.e. status: { 200: {...} }) 109 | - method (i.e. method: { get: {...} }) 110 | - path and method (i.e. '/blog': { get: {...} }) 111 | 112 | ## Options 113 | 114 | Example using all options with its default values: 115 | ```js 116 | app.use(expressMetrics({ 117 | port: 8091, 118 | cluster: false, 119 | decimals: false, 120 | header: false 121 | })); 122 | ``` 123 | ### port: Number (default: undefined) 124 | 125 | Only used when cluster option is false, start a metrics servers on the same process that the application is running. 126 | 127 | ### decimals: Boolean (default: false) 128 | 129 | If decimals is __true__, times are measured in millisecond with three decimals. Otherwise, times are rounded to milliseconds. 130 | 131 | ### header: Boolean (default: false) 132 | 133 | If header is __true__, "X-Response-Time" is added as HTTP header in the response. 134 | 135 | ### statsd: Object (default: undefined) 136 | 137 | Optionally you can send the metrics to statsd. In order to do that you just need to provide the statsd config in the options. 138 | Thanks to metrics you are able to explore at any time if there is something wierd in your application. And with statsd you are able to collect 139 | stats for you more representative resources. 140 | 141 | Example: 142 | 143 | ```js 144 | 145 | 146 | app.use(expressMetrics({ 147 | statsd: { 148 | 'host': 'localhost', 149 | 'port': 8125, 150 | 'prefix': require('os').hostname() + '.myService', 151 | 'routes': { 152 | 'showUserCampaigns': [{ path: '/campaigns/:userId/lite', methods: ['get']}], 153 | 'showCampaign': [{ path: '/campaign/:campaignId', methods: ['get']}], 154 | 'showUserShops': { path: '/shop/:userId', method: 'get'} 155 | } 156 | } 157 | })); 158 | 159 | 160 | ``` 161 | 162 | Just the routes that you indicate in the 'routes' option will be sent to statsd. 163 | 164 | 165 | ### cluster: Boolean (default: false) 166 | 167 | If cluster is __true__, delegate the start of the metrics server to master process. Due to this, express-metrics provides one way to run a metrics server in master, i.e: 168 | 169 | ```js 170 | var cluster = require('cluster'); 171 | var express = require('express'); 172 | var expressMetrics = require('express-metrics'); 173 | var numCPUs = require('os').cpus().length; 174 | 175 | if (cluster.isMaster) { 176 | // Fork workers. 177 | for (var i = 0; i < numCPUs; i++) { 178 | cluster.fork(); 179 | } 180 | 181 | // start a metrics server on master process 182 | expressMetrics.listen(8091); 183 | } else { 184 | var app = express(); 185 | 186 | // with cluster option set to true says to the express-metrics that 187 | // it must send the measured times to master process 188 | app.use(expressMetrics({ 189 | cluster: true 190 | })); 191 | 192 | app.get('/', function (req, res, next) { 193 | res.json({ greet: 'Hello world!' }); 194 | }); 195 | 196 | app.listen(8090); 197 | } 198 | ``` 199 | 200 | When one request is handled by one worker, express-metrics measures the response time and send it to the master. Then, master receives the data and updates the corresponding metrics. Furthermore, master exposes the metrics on port previously configured. 201 | 202 | ## Logging 203 | Logs are sent to 'express-metrics' log4js logger. 204 | 205 | 206 | ## Contributions 207 | 208 | Do you want to contribute?. Please, follow the below suggestions: 209 | - To add features, `pull requests` to `develop` branch. 210 | - To fix bugs in release version, `pull request` both `master` and `develop` branches. 211 | - Be consistent with style and design decisions. 212 | - Cover your implementation with tests, add it under `test/*.spec.js`. 213 | 214 | ## Change history 215 | 216 | To view change history, please visit: [HISTORY.md](https://github.com/dgaubert/express-metrics/blob/master/HISTORY.md) 217 | 218 | Versioning strategy: 219 | 220 | - The major version will increase for any backward-incompatible changes. 221 | - The minor version will increase for added features. 222 | - The patch version will increase for bug-fixes. 223 | 224 | ## License 225 | 226 | To view the MIT license, please visit: [The MIT License (MIT)](https://github.com/dgaubert/express-metrics/blob/master/LICENSE) 227 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var builder = require('./lib/builder'); 4 | var header = require('./lib/header'); 5 | var chrono = require('./lib/chrono'); 6 | var optionsChecker = require('./lib/options.checker'); 7 | 8 | module.exports = function expressMetrics(options) { 9 | var client; 10 | 11 | options = optionsChecker.check(options); 12 | 13 | builder.init(options); 14 | client = builder.getClient(); 15 | 16 | header.init({ header: options.header }); 17 | chrono.init({ decimals: options.decimals }); 18 | 19 | return function (req, res, next) { 20 | chrono.start(); 21 | 22 | // decorate response#end method from express 23 | var end = res.end; 24 | res.end = function () { 25 | var responseTime = chrono.stop(); 26 | 27 | header.setResponseTime(res, responseTime); 28 | 29 | // call to original express#res.end() 30 | end.apply(res, arguments); 31 | 32 | client.send({ 33 | route: req.route, 34 | method: req.method, 35 | status: res.statusCode, 36 | time: responseTime 37 | }); 38 | }; 39 | 40 | next(); 41 | }; 42 | 43 | }; 44 | 45 | module.exports.listen = function listen(port) { 46 | if (builder.getServer()) { 47 | return builder.getMetricsServer(); 48 | } 49 | 50 | return builder.startServer(port); 51 | }; 52 | 53 | module.exports.close = function close(callback) { 54 | builder.getServer().stop(callback); 55 | }; 56 | -------------------------------------------------------------------------------- /lib/builder.js: -------------------------------------------------------------------------------- 1 | var StatsD = require('node-statsd').StatsD; 2 | 3 | var Client = require('./client'); 4 | var Server = require('./server'); 5 | var ClientProxy = require('./client.proxy'); 6 | var ServerProxy = require('./server.proxy'); 7 | var ClientClusterProxy = require('./client.cluster.proxy'); 8 | var ServerClusterProxy = require('./server.cluster.proxy'); 9 | var logger = require('./logger'); 10 | 11 | var server; 12 | var serverProxy; 13 | var clientProxy; 14 | var client; 15 | 16 | /** 17 | * Converts input stats estructure to the internal structure. In converts from: 18 | * name: [{path: '', methods: []}] 19 | * 20 | * To: 21 | * path: { methods: [], name: ''} 22 | * 23 | * It also supports the following format: 24 | * 25 | * name: {path: '', method: ''} 26 | */ 27 | function generateStatsdRoutes(stats) { 28 | var result = {}; 29 | Object.keys(stats).forEach(function(name) { 30 | var routes = stats[name]; 31 | if (!(routes instanceof Array)) { 32 | routes = [routes]; 33 | } 34 | routes.forEach(function(route) { 35 | result[route.path] = { 36 | name: name, 37 | methods: route.method ? route.method : route.methods 38 | }; 39 | }); 40 | 41 | 42 | }); 43 | return result; 44 | } 45 | 46 | function initServer(port, isCluster, statsdConfig) { 47 | var statsd = null; 48 | var statsdRoutes = null; 49 | 50 | if (statsdConfig) { 51 | if (statsdConfig.instance) { 52 | statsd = statsdConfig.instance; 53 | } else { 54 | statsd = new StatsD(statsdConfig.host, statsdConfig.port, statsdConfig.prefix); 55 | statsd.socket.on('error', function (error) { 56 | logger.error('Error sending stats: ', error); 57 | }); 58 | } 59 | 60 | statsdRoutes = generateStatsdRoutes(statsdConfig.routes); 61 | } 62 | 63 | server = new Server(port, statsd, statsdRoutes); 64 | 65 | serverProxy = isCluster ? 66 | new ServerClusterProxy(server) : 67 | new ServerProxy(server); 68 | 69 | return server; 70 | } 71 | 72 | function initClient(isCluster) { 73 | clientProxy = isCluster ? 74 | new ClientClusterProxy() : 75 | new ClientProxy(serverProxy); 76 | 77 | client = new Client(clientProxy); 78 | 79 | return client; 80 | } 81 | 82 | module.exports.init = function init(options) { 83 | server = null; 84 | serverProxy = null; 85 | clientProxy = null; 86 | client = null; 87 | 88 | 89 | if (!options.cluster && options.port) { 90 | initServer(options.port, false, options.statsd); 91 | } 92 | 93 | return initClient(options.cluster); 94 | }; 95 | 96 | module.exports.startServer = function startServer(port, statsdConfig) { 97 | return initServer(port, true, statsdConfig); 98 | }; 99 | 100 | module.exports.getServer = function getServer() { 101 | return server; 102 | }; 103 | 104 | module.exports.getClient = function getClient() { 105 | return client; 106 | }; 107 | -------------------------------------------------------------------------------- /lib/chrono.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var decimals = false; 4 | var startAt = []; 5 | 6 | var chrono = { 7 | 8 | init: function (options) { 9 | decimals = options && options.decimals || false; 10 | }, 11 | 12 | start: function () { 13 | startAt = process.hrtime(); 14 | }, 15 | 16 | stop: function () { 17 | if (!startAt.length) { 18 | return 0; 19 | } 20 | 21 | var diff = process.hrtime(startAt); 22 | var nanoseconds = diff[0] * 1e9 + diff[1]; 23 | 24 | startAt = []; // clear for further calls 25 | 26 | return decimals ? 27 | Math.round(nanoseconds / 1e3) / 1e3 : // time in ms with 3 decimals 28 | Math.round(nanoseconds / 1e6); // rounded time to ms 29 | } 30 | 31 | }; 32 | 33 | module.exports = chrono; 34 | -------------------------------------------------------------------------------- /lib/client.cluster.proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ClientClusterProxy() { 4 | } 5 | 6 | ClientClusterProxy.prototype.forwardMessage = function forwardMessage(message) { 7 | message.cmd = 'express-metrics'; 8 | process.send(message); 9 | }; 10 | 11 | module.exports = ClientClusterProxy; 12 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Client(proxy) { 4 | this.proxy = proxy; 5 | } 6 | 7 | Client.prototype.send = function send(message) { 8 | message.method = message.method.toLowerCase(); 9 | this.proxy.forwardMessage(message); 10 | }; 11 | 12 | module.exports = Client; 13 | -------------------------------------------------------------------------------- /lib/client.proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ClientProxy(serverProxy) { 4 | this.serverProxy = serverProxy; 5 | } 6 | 7 | ClientProxy.prototype.forwardMessage = function forwardMessage(message) { 8 | this.serverProxy.forwardMessage(message); 9 | }; 10 | 11 | module.exports = ClientProxy; 12 | -------------------------------------------------------------------------------- /lib/header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var onHeaders = require('on-headers'); 4 | 5 | var header = false; 6 | var addResponseTime = function () { 7 | return function () {}; 8 | }; 9 | 10 | var headerResponseTime = { 11 | 12 | init: function (options) { 13 | header = options && options.header || false; 14 | if (header) { 15 | addResponseTime = function (responseTime) { 16 | return function () { 17 | if (!this.getHeader('X-Response-Time')) { 18 | this.setHeader('X-Response-Time', responseTime + 'ms'); 19 | } 20 | }; 21 | }; 22 | } 23 | }, 24 | 25 | setResponseTime: function (res, responseTime) { 26 | return onHeaders(res, addResponseTime(responseTime)); 27 | } 28 | 29 | }; 30 | 31 | module.exports = headerResponseTime; 32 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var logger = require('log4js').getLogger('express-metrics'); 4 | 5 | module.exports = logger; 6 | -------------------------------------------------------------------------------- /lib/options.checker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function statsdCheck(options) { 4 | var statsd = options.statsd; 5 | if (statsd) { 6 | if(!statsd.prefix) { 7 | throw new Error('Missing statsd stats prefix'); 8 | } 9 | if(!statsd.instance && !(statsd.host && statsd.port)) { 10 | throw new Error('Missing statsd creational configuration'); 11 | } 12 | 13 | if(!statsd.routes || typeof(statsd.routes) !== 'object' ) { 14 | throw new Error('Missing statsd routes definition '); 15 | } 16 | 17 | } 18 | } 19 | 20 | module.exports.check = function check(options) { 21 | options = options || {}; 22 | options.cluster = options.cluster || false; 23 | options.header = options.header || false; 24 | options.decimals = options.decimals || false; 25 | options.port = options.port || 0; 26 | 27 | if (!options.cluster && !options.port) { 28 | throw new TypeError('Port number is mandatory when cluster option is disabled.'); 29 | } 30 | 31 | statsdCheck(options); 32 | 33 | 34 | return options; 35 | }; 36 | -------------------------------------------------------------------------------- /lib/server.cluster.proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cluster = require('cluster'); 4 | 5 | function ServerClusterProxy(server) { 6 | this.server = server; 7 | this.init(); 8 | } 9 | 10 | ServerClusterProxy.prototype.init = function init() { 11 | 12 | function isFromExpressMetrics(msg) { 13 | return (msg.cmd && msg.cmd === 'express-metrics'); 14 | } 15 | 16 | if (cluster.isMaster) { 17 | var _this = this; 18 | var workers = cluster.workers; 19 | Object.keys(workers).forEach(function (id) { 20 | workers[id].on('message', function (message) { 21 | if (isFromExpressMetrics(message)) { 22 | _this.server.update(message); 23 | } 24 | }); 25 | }); 26 | } 27 | }; 28 | 29 | module.exports = ServerClusterProxy; 30 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var Metrics = require('metrics'); 2 | var logger = require('./logger'); 3 | 4 | var CATEGORIES = { 5 | all: 'global.all', 6 | static: 'global.static', // i.e. '/favicon.ico' 7 | status: 'status', 8 | method: 'method' 9 | }; 10 | 11 | function Server(port, statsd, statsdRoutes) { 12 | this.metrics = new Metrics.Server(port); 13 | this.statsd = statsd; 14 | this.statsdRoutes = statsdRoutes; 15 | } 16 | 17 | Server.prototype.getMetricName = function getMetricName(route, methodName) { 18 | var routeName = CATEGORIES.static; 19 | 20 | if (route && route.path) { 21 | routeName = route.path; 22 | 23 | if (Object.prototype.toString.call(routeName) === '[object RegExp]') { 24 | routeName = routeName.source; 25 | } 26 | 27 | routeName = routeName + '.' + methodName.toLowerCase(); 28 | } 29 | 30 | return routeName; 31 | }; 32 | 33 | Server.prototype.update = function update(message) { 34 | var metricName = this.getMetricName(message.route, message.method); 35 | var path = message.route ? message.route.path : undefined; 36 | 37 | this.updateMetric(CATEGORIES.all, message.time); 38 | this.updateMetric(CATEGORIES.status + '.' + message.status, message.time); 39 | this.updateMetric(CATEGORIES.method + '.' + message.method, message.time); 40 | this.updateMetric(metricName, message.time); 41 | 42 | if (this.statsd && this.statsdRoutes[path]) { 43 | var route = this.statsdRoutes[path]; 44 | if (route.methods.indexOf(message.method) !== -1) { 45 | this.sendToStatsD(route.name, message.time); 46 | } 47 | } 48 | }; 49 | 50 | Server.prototype.updateMetric = function updateMetric(name, time) { 51 | if (!this.metrics.report.getMetric(name)) { 52 | this.metrics.addMetric(name, new Metrics.Timer()); 53 | } 54 | 55 | this.metrics.report.getMetric(name).update(time); 56 | }; 57 | 58 | 59 | Server.prototype.sendToStatsD = function sendToStatsD(name, time) { 60 | this.statsd.timing('.' + name, time, null, function (error) { 61 | if (error) { 62 | logger.error('Error sending response time to StatsD: ', error); 63 | } 64 | }); 65 | }; 66 | 67 | 68 | Server.prototype.stop = function stop(callback) { 69 | this.metrics.server.close(callback); 70 | }; 71 | 72 | module.exports = Server; 73 | -------------------------------------------------------------------------------- /lib/server.proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ServerProxy(server) { 4 | this.server = server; 5 | } 6 | 7 | ServerProxy.prototype.forwardMessage = function forwardMessage(message) { 8 | this.server.update(message); 9 | }; 10 | 11 | module.exports = ServerProxy; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-metrics", 3 | "description": "Express middleware for collecting and reporting metrics about response times", 4 | "version": "1.1.0", 5 | "author": "Daniel García Aubert ", 6 | "main": "index.js", 7 | "dependencies": { 8 | "log4js": "^0.6.26", 9 | "metrics": "latest", 10 | "node-statsd": "^0.1.1", 11 | "on-headers": "^1.0.0" 12 | }, 13 | "devDependencies": { 14 | "express": "^4.12.2", 15 | "jshint": "latest", 16 | "q": "^1.2.0", 17 | "request": "^2.53.0", 18 | "should": "^5.1.0", 19 | "supertest": "^0.15.0", 20 | "sinon": "^1.15.3", 21 | "mocha": "^2.2.1" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git://github.com/dgaubert/express-metrics.git" 26 | }, 27 | "scripts": { 28 | "test": "./node_modules/mocha/bin/mocha test/**/*.spec.js --require should", 29 | "test:watch": "npm run test -- -w", 30 | "lint": "jshint *.js" 31 | }, 32 | "keywords": [ 33 | "nodejs", 34 | "express", 35 | "middleware", 36 | "metrics", 37 | "cluster", 38 | "response", 39 | "times" 40 | ], 41 | "bugs": { 42 | "url": "https://github.com/dgaubert/express-metrics/issues" 43 | }, 44 | "license": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /test/integration/middleware.cluster.spec.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var Quest = require('../util/quest'); 3 | var clusterLauncher = require('../util/app.cluster.launcher.js'); 4 | var arrayGenerator = require('../util/array.generator.js'); 5 | 6 | 7 | describe('Middleware in cluster mode', function () { 8 | before(function (done) { 9 | clusterLauncher.start(function () { 10 | done(); 11 | }); 12 | }); 13 | 14 | describe('when makes a request nine times to root path', function () { 15 | 16 | before(function (done) { 17 | Q.all(arrayGenerator.generate(9).map(function (/* index */) { 18 | return Quest.get({ url: 'http://localhost:4000/', json: true }); 19 | })) 20 | .then(function (/*result*/) { 21 | done(); 22 | }) 23 | .fail(function (err) { 24 | done(err); 25 | }); 26 | }); 27 | 28 | it('server should report metrics with count rate equal to 9', function (done) { 29 | Quest.get({ url: 'http://localhost:4001/metrics', json: true }) 30 | .then(function (result) { 31 | result.global.all.rate.count.should.be.equal(9); 32 | done(); 33 | }) 34 | .fail(function (err) { 35 | done(err); 36 | }); 37 | }); 38 | 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /test/integration/middleware.spec.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var Quest = require('../util/quest'); 3 | var appLauncher = require('../util/app.launcher.js'); 4 | var arrayGenerator = require('../util/array.generator.js'); 5 | 6 | describe('Middleware in single thread mode', function () { 7 | 8 | before(function (done) { 9 | appLauncher.start(function () { 10 | done(); 11 | }); 12 | }); 13 | 14 | describe('when makes a request nine times to root path', function () { 15 | 16 | before(function (done) { 17 | Q.all(arrayGenerator.generate(9).map(function (/* index */) { 18 | return Quest.get({ url: 'http://localhost:3000/', json: true }); 19 | })) 20 | .then(function () { 21 | done(); 22 | }) 23 | .fail(function (err) { 24 | done(err); 25 | }); 26 | }); 27 | 28 | it('server should report metrics with count rate equal to 9', function (done) { 29 | Quest.get({ url: 'http://localhost:3001/metrics', json: true }) 30 | .then(function (result) { 31 | result.global.all.rate.count.should.be.equal(9); 32 | done(); 33 | }) 34 | .fail(function (err) { 35 | done(err); 36 | }); 37 | }); 38 | 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/chrono.spec.js: -------------------------------------------------------------------------------- 1 | var chrono = require('../../lib/chrono'); 2 | 3 | describe('Chrono', function () { 4 | it('module should be loaded properly', function () { 5 | chrono.should.Object; 6 | chrono.init.should.Function; 7 | chrono.start.should.Function; 8 | chrono.stop.should.Function; 9 | }); 10 | 11 | describe('checking options behavior', function () { 12 | 13 | it('when init is not called should measure the time with no decimals', function () { 14 | chrono.start(); 15 | (chrono.stop() % 1 !== 0).should.be.false; 16 | }); 17 | 18 | it('when decimals is not specified should measure the time with no decimals', function () { 19 | chrono.init(); 20 | chrono.start(); 21 | (chrono.stop() % 1 !== 0).should.be.false; 22 | }); 23 | 24 | it('when decimals is set to false, should measure the time with no decimals', function () { 25 | chrono.init({ decimals: false }); 26 | chrono.start(); 27 | (chrono.stop() % 1 !== 0).should.be.false; 28 | }); 29 | 30 | 31 | it('when decimals is set to true, should measure the time with decimals', function () { 32 | chrono.init({ decimals: true }); 33 | chrono.start(); 34 | (chrono.stop() % 1 !== 0).should.be.true; 35 | }); 36 | 37 | }); 38 | 39 | describe('checking chronometer behavior', function () { 40 | it('when start is not called previously stop should return 0', function () { 41 | chrono.stop().should.be.equal(0); 42 | }); 43 | 44 | it('when start is called twice, should only consider last call to measure the time', function () { 45 | chrono.init({ decimals: false }); 46 | chrono.start(); 47 | setTimeout(function () { 48 | chrono.start(); 49 | chrono.stop().should.lessThan(1); 50 | }, 1); 51 | }); 52 | 53 | it('when stop is called twice, the second one should return 0', function () { 54 | chrono.init({ decimals: true }); 55 | chrono.start(); 56 | chrono.stop().should.greaterThan(0); 57 | chrono.stop().should.be.equal(0); 58 | }); 59 | 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/unit/header.spec.js: -------------------------------------------------------------------------------- 1 | var header = require('../../lib/header'); 2 | 3 | describe('Header', function () { 4 | it('module should be loaded properly', function () { 5 | header.should.Object; 6 | header.init.should.Function; 7 | header.setResponseTime.should.Function; 8 | }); 9 | 10 | describe('checking options behavior', function () { 11 | 12 | it('when init is not called should do nothing', function () { 13 | var res = {}; 14 | header.setResponseTime(res, 0); 15 | }); 16 | 17 | it('when header is not specified should do nothing', function () { 18 | var res = {}; 19 | header.init(); 20 | header.setResponseTime(res, 0); 21 | }); 22 | 23 | it('when header is set to false, should do nothing', function () { 24 | var res = {}; 25 | header.init({ header: false }); 26 | header.setResponseTime(res, 0); 27 | }); 28 | 29 | it('when header is set to true, should do set X-Response-Time header', function () { 30 | var res = {}; 31 | header.init({ decimals: true }); 32 | header.setResponseTime(res, 0); 33 | res.writeHead.should.Function; 34 | }); 35 | 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/unit/middleware.spec.js: -------------------------------------------------------------------------------- 1 | var expressMetrics = require('../../'); 2 | 3 | describe('Express Metrics', function () { 4 | it('module should return the express middelware', function () { 5 | expressMetrics.should.Function; 6 | }); 7 | 8 | describe('checking options', function () { 9 | it('when both cluster & port are undefined should throw exception', function () { 10 | (function (){ 11 | expressMetrics(); 12 | }).should.throw(); 13 | }); 14 | 15 | it('when cluster is false and port is undefined should throw exception', function () { 16 | (function (){ 17 | expressMetrics({ cluster: false }); 18 | }).should.throw(); 19 | }); 20 | 21 | it('when statsd is set when when missing a parameter an exception is thrown', function () { 22 | (function (){ 23 | expressMetrics({ statsd: {port: 8125} }); 24 | }).should.throw(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/unit/statsd.spec.js: -------------------------------------------------------------------------------- 1 | var Server = require('../../lib/server'); 2 | var optionsChecker = require('../../lib/options.checker'); 3 | var builder = require('../../lib/builder'); 4 | 5 | var StatsD = require('node-statsd').StatsD; 6 | var sinon = require('sinon'); 7 | 8 | 9 | describe('StatsD Integration', function () { 10 | 11 | describe('Options Management', function () { 12 | it('Default instantiation when no creational options are passed', function () { 13 | (function () { 14 | optionsChecker.check({ 15 | port: 1234, 16 | statsd: { 17 | routes: {}, 18 | prefix: 'foo' 19 | } 20 | }); 21 | }).should.throw(); 22 | }); 23 | 24 | it('Already existant instance of statsd provided', function () { 25 | (function (){ 26 | optionsChecker.check({ 27 | port: 1234, 28 | statsd: { 29 | routes: {}, 30 | prefix: 'foo', 31 | instance: new StatsD() 32 | } 33 | }); 34 | }).should.not.throw(); 35 | }); 36 | 37 | it('Internal instantiation', function () { 38 | (function (){ 39 | optionsChecker.check({ 40 | port: 1234, 41 | statsd: { 42 | host: 'localhost', 43 | port: 8195, 44 | prefix: 'foo', 45 | routes: {} 46 | } 47 | }); 48 | }).should.not.throw(); 49 | }); 50 | 51 | it('No prefix provided', function () { 52 | (function (){ 53 | optionsChecker.check({ 54 | port: 1234, 55 | statsd: { 56 | routes: {}, 57 | host: 'localhost', 58 | port: 8195 59 | } 60 | }); 61 | }).should.throw(); 62 | }); 63 | 64 | it('No routes provided', function () { 65 | (function (){ 66 | optionsChecker.check({ 67 | port: 1234, 68 | statsd: { 69 | prefix: 'foo', 70 | host: 'localhost', 71 | port: 8195 72 | } 73 | }); 74 | }).should.throw(); 75 | }); 76 | 77 | }); 78 | 79 | describe('Routes Management', function () { 80 | var server; 81 | 82 | after(function(){ 83 | if(server) { 84 | // Ugly, but when the metrics server is instantiated it already starts 85 | server.stop(); 86 | } 87 | }); 88 | 89 | it('Stats options are properly converted to routes definition', function () { 90 | var expectedRoutes = { 91 | '/campaigns/:userId/lite': {'name': 'showUserCampaigns', 'methods': ['get']}, 92 | '/campaign/:campaignId': {'name': 'showCampaign', 'methods': ['get']}, 93 | '/shop/:userId': {'name': 'showUserShops', 'methods': ['get']} 94 | }; 95 | 96 | server = builder.startServer(1234, { 97 | prefix: 'foo', 98 | host: 'localhost', 99 | port: 8195, 100 | routes: { 101 | showUserCampaigns: { 102 | path: '/campaigns/:userId/lite', 103 | methods: ['get'] 104 | }, 105 | showCampaign: { 106 | path: '/campaign/:campaignId', 107 | methods: ['get'] 108 | }, 109 | showUserShops: { 110 | path: '/shop/:userId', 111 | methods: ['get'] 112 | } 113 | } 114 | }); 115 | 116 | server.statsdRoutes.should.eql(expectedRoutes); 117 | 118 | }); 119 | }); 120 | 121 | describe('Server integration', function () { 122 | var server; 123 | var statsd = true; 124 | var routes = { 125 | '/campaigns/:userId/lite': {'name': 'showUserCampaigns', 'methods': ['get']}, 126 | '/campaign/:campaignId': {'name': 'showCampaign', 'methods': ['get']}, 127 | '/shop/:userId': {'name': 'showUserShops', 'methods': ['get']} 128 | }; 129 | 130 | beforeEach(function() { 131 | server = new Server(1234, statsd, routes); 132 | }); 133 | 134 | afterEach(function(){ 135 | if(server) { 136 | // Ugly, but when the metrics server is instantiated it already starts 137 | server.stop(); 138 | server = null; 139 | } 140 | }); 141 | 142 | it('No stat is generated', function () { 143 | var mock = sinon.mock(server); 144 | mock.expects('sendToStatsD').never(); 145 | 146 | server.update({ 147 | route: { path: '/shop/:userId'}, 148 | method: 'post' 149 | }); 150 | 151 | mock.verify(); 152 | }); 153 | 154 | it('Stat is generated', function () { 155 | var mock = sinon.mock(server); 156 | mock.expects('sendToStatsD').once(); 157 | 158 | server.update({ 159 | route: { path: '/shop/:userId'}, 160 | method: 'get' 161 | }); 162 | 163 | mock.verify(); 164 | }); 165 | }); 166 | 167 | 168 | }); -------------------------------------------------------------------------------- /test/util/app.cluster.launcher.js: -------------------------------------------------------------------------------- 1 | var cluster = require('cluster'); 2 | var path = require('path'); 3 | var expressMetrics = require('../../'); 4 | 5 | module.exports.start = function (callback) { 6 | var workersCount = 0; 7 | var maxWorkersCount = 2; 8 | 9 | function msgReceived(msg) { 10 | if (msg === 'Worker up!') { 11 | workersCount += 1; 12 | if (workersCount === maxWorkersCount) { 13 | callback(); 14 | } 15 | } 16 | } 17 | 18 | if (cluster.isMaster) { 19 | cluster.setupMaster({ 20 | exec : path.join(__dirname, 'app.js'), 21 | }); 22 | 23 | for (var i = 0; i < maxWorkersCount; i += 1) { 24 | var worker = cluster.fork(); 25 | worker.on('message', msgReceived); 26 | } 27 | 28 | expressMetrics.listen(4001); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /test/util/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var expressMetrics = require('../../'); 3 | 4 | var app = express(); 5 | 6 | app.use(expressMetrics({ 7 | cluster: true 8 | })); 9 | 10 | app.get('/', function (req, res) { 11 | res.json({ worker: process.pid }); 12 | }); 13 | 14 | process.send('Worker up!'); 15 | 16 | module.exports = app.listen(4000); 17 | -------------------------------------------------------------------------------- /test/util/app.launcher.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var expressMetrics = require('../../'); 3 | var app; 4 | var server; 5 | 6 | module.exports.start = function (callback) { 7 | app = express(); 8 | 9 | app.use(expressMetrics({ port: 3001 })); 10 | 11 | app.get('/', function (req, res) { 12 | res.json({ master: process.pid }); 13 | }); 14 | 15 | server = app.listen(3000); 16 | 17 | callback(); 18 | }; 19 | -------------------------------------------------------------------------------- /test/util/array.generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.generate = function generate(size) { 4 | var arr = []; 5 | 6 | for(var i = 0; i < size; i += 1) { 7 | arr.push(i); 8 | } 9 | 10 | return arr; 11 | }; 12 | -------------------------------------------------------------------------------- /test/util/quest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request'); 4 | var Q = require('q'); 5 | 6 | var Quest = { 7 | get: function (options) { 8 | return Q.ninvoke(request, 'get', options) 9 | .then(function (result) { 10 | // response = result[0], body = result [1] 11 | if (result[0].statusCode !== 200) { 12 | return Q.reject(result[1].message); 13 | } 14 | return result[1]; 15 | }); 16 | } 17 | }; 18 | 19 | module.exports = Quest; 20 | --------------------------------------------------------------------------------