├── .eslintrc.yml
├── .gitignore
├── .prettierrc
├── .travis.yml
├── LICENSE
├── README.md
├── examples
├── package.json
├── sample.js
├── sample_cluster.js
└── samplejson.js
├── lib
├── index.js
├── status.ejs
└── watcher.js
├── package-lock.json
├── package.json
└── test
└── index.js
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | es6: true
3 | node: true
4 | extends: 'eslint:recommended'
5 | rules:
6 | indent: [ error, 4, { "SwitchCase": 1 } ]
7 | linebreak-style: [ error, unix ]
8 | no-console: off
9 | quotes: [ error, single ]
10 | semi: [ error, always ]
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .nyc_output
2 | node_modules
3 | coverage
4 | .DS_Store
5 | .DS_Store?
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | arrowParens: always
2 | bracketSpacing: true
3 | endOfLine: lf
4 | jsxSingleQuote: false
5 | semi: true
6 | singleQuote: true
7 | tabWidth: 4
8 | trailingComma: es5
9 | useTabs: false
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - node
5 | - 'lts/*'
6 |
7 | cache: npm
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Yahoo! Inc. All rights reserved.
2 |
3 | Redistribution and use of this software in source and binary forms,
4 | with or without modification, are permitted provided that the following
5 | conditions are met:
6 |
7 | * Redistributions of source code must retain the above
8 | copyright notice, this list of conditions and the
9 | following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above
12 | copyright notice, this list of conditions and the
13 | following disclaimer in the documentation and/or other
14 | materials provided with the distribution.
15 |
16 | * Neither the name of Yahoo! Inc. nor the names of its
17 | contributors may be used to endorse or promote products
18 | derived from this software without specific prior
19 | written permission of Yahoo! Inc.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
22 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
23 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
24 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | mod_statuspage for Node
2 | ===================
3 |
4 | Simple express/connect middleware to provide a status page with following details of the nodejs host.
5 |
6 | * Various Versions - Prints NodeJS Version, OS Version, OS Release
7 | * CPU - Average Load on CPU
8 | * Memory - Total Memory, Free Memory
9 | * Traffic - Total Num of Requests, Requests per second, Total KBs Transferred, Total KBs Out etc.
10 | * Workers - List all the worker processes and the information listed above for each of the worker processes
11 |
12 | This module reads the above data from a unix socket generated from the npm module process-watcher.
13 | For more details on process-watcher, please refer https://github.com/yahoo/process-watcher.
14 |
15 | This module is recommended to be used only in a cluster environment. Also this module is designed to
16 | work together with monitr (https://github.com/yahoo/monitr) and process-watcher. For an example of them
17 | working together please check examples/sample_cluster.js.
18 |
19 | Installation
20 | ------------
21 |
22 | `npm install mod_statuspage`
23 |
24 | Usage
25 | -----
26 |
27 | ```javascript
28 | var express = require('express'),
29 | status = require('../lib/index.js');
30 |
31 | var app = express();
32 |
33 | app.use(status({
34 | url: '/status',
35 | check: function(req) {
36 | if (req.something == false) {
37 | return false; //Don't show status
38 | }
39 | return true; //Show status
40 | },
41 | responseContentType : 'html'
42 | }));
43 |
44 | console.log('Go to: http://127.0.0.1:8000/status');
45 | app.listen(8000);
46 | ```
47 |
48 | Configuration
49 | -------------
50 |
51 | * `url` - The URL to respond to, defaults to `/status`
52 | * `check` - A function to check the request to see if the status page should be shown. Default: `returns true to always show`
53 | * `responseContentType` - The Content-Type of the Response, can be html or json, defaults to `html`
54 | * `ejsTemplate` - EJS Template file for html rendering if responseContentType is html, defaults to `status.ejs` bundled with the module
55 | * `socketPath` - The socket path written by watchr, defaults to `/tmp/watcher.sock`
56 |
57 |
58 |
59 | Build Status
60 | ------------
61 |
62 | [](http://travis-ci.org/yahoo/mod_statuspage)
63 |
64 |
65 | Node Badge
66 | ----------
67 |
68 | [](https://nodei.co/npm/mod_statuspage/)
69 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sample_cluster",
3 | "version": "0.0.1",
4 | "main": "sample_cluster.js",
5 | "description": "status page example for sample_cluster",
6 | "os": [ "linux" ],
7 | "cpu": [ "x64", "ia32" ],
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/yahoo/mod_statuspage.git"
11 | },
12 | "license": "",
13 | "engines": { "node": ">=0.6" },
14 | "dependencies": {
15 | "monitr": "*"
16 | }
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/examples/sample.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 |
7 | var express = require('express'),
8 | status = require('../lib/index.js');
9 |
10 | var app = express();
11 |
12 | app.use(status({
13 | url: '/status',
14 | check: function(req) {
15 | if (req.something == false) {
16 | return false; //Don't show status
17 | }
18 | return true; //Show status
19 | }
20 | }));
21 |
22 | console.log('Go to: http://127.0.0.1:8000/status');
23 | app.listen(8000);
24 |
--------------------------------------------------------------------------------
/examples/sample_cluster.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 | /*
7 | * This Example shows how monitr, proces-watcher and mod-statuspage works together.
8 | * process-watcher needs to be started as a separate process.
9 | * It also shows one of the ways to set clusterStartTime on the worker process.
10 | */
11 |
12 | var cluster = require('cluster'),
13 | http = require('http'),
14 | express = require('express'),
15 | monitor = require('monitr'),
16 | statuspage = require('../lib/index.js'),
17 | numWorker = require('os').cpus().length > 1 ? require('os').cpus().length : 2,
18 | clusterStartTime = Date.now(),
19 | newWorkerEnv = {};
20 |
21 | if (cluster.isMaster) {
22 | var i;
23 | newWorkerEnv.clusterStartTime = clusterStartTime;
24 |
25 | for (i = 0; i < numWorker; i++) {
26 | console.log("Starting worker: " + i);
27 | cluster.fork(newWorkerEnv);
28 | }
29 |
30 | cluster.on('exit', function (worker, code, signal) {
31 | console.log('worker ' + worker.process.pid + ' died');
32 | cluster.fork(newWorkerEnv);
33 | });
34 |
35 | } else {
36 | //Worker Process
37 | monitor.start();
38 | if (process.env.clusterStartTime) {
39 | process.clusterStartTime = new Date(parseInt(process.env.clusterStartTime,10));
40 | }
41 |
42 | process.on('exit', function () {
43 | monitor.stop();
44 | });
45 |
46 | process.on('SIGINT', function () {
47 | process.exit();
48 | });
49 |
50 | var app = express();
51 | app.use(statuspage({
52 | url: '/status'
53 | }));
54 |
55 | console.log('Go to: http://127.0.0.1:8000/status');
56 | app.listen(8000);
57 | }
58 |
--------------------------------------------------------------------------------
/examples/samplejson.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 |
7 | var express = require('express'),
8 | status = require('../lib/index.js');
9 |
10 | var app = express();
11 |
12 | app.use(status({
13 | url: '/status',
14 | check: function(req) {
15 | if (req.something == false) {
16 | return false; //Don't show status
17 | }
18 | return true; //Show status
19 | },
20 | responseContentType: 'json'
21 | }));
22 |
23 | console.log('Go to: http://127.0.0.1:8000/status');
24 | app.listen(8000);
25 |
26 |
27 |
28 | /*
29 | * Sample json output
30 | *
31 | {
32 | "hostname":"windowdistance.corp.yahoo.com",
33 | "node_version":"v0.10.18",
34 | "os_type":"Linux",
35 | "os_release":"2.6.18-164.el5",
36 | "currentTime":1379723936357,
37 | "cluster_start_time":1379723926367,
38 | "cluster_uptime":10,
39 | "total_memory":4140347392,
40 | "free_memory":360099840,
41 | "os_loadavg":[
42 | 1.8544921875,
43 | 2.4150390625,
44 | 2.63525390625
45 | ],
46 | "worker":[
47 | {
48 | "pid":"28766",
49 | "cpu":0,
50 | "mem":0.37,
51 | "cpu_per_req":0,
52 | "jiffy_per_req":0,
53 | "rps":0,
54 | "events":2,
55 | "open_conns":0,
56 | "open_requests":0,
57 | "total_requests":0,
58 | "kbs_out":0,
59 | "kbs_transferred":0,
60 | "start_time":1379723926000
61 | },
62 | {
63 | "pid":"28768",
64 | "cpu":0,
65 | "mem":0.37,
66 | "cpu_per_req":0,
67 | "jiffy_per_req":0,
68 | "rps":0,
69 | "events":2,
70 | "open_conns":0,
71 | "open_requests":0,
72 | "total_requests":0,
73 | "kbs_out":0,
74 | "kbs_transferred":0,
75 | "start_time":1379723926000
76 | }
77 | ],
78 | "total_requests":0,
79 | "total_kbs_transferred":0,
80 | "total_kbs_out":0,
81 | "total_rps":0
82 | }
83 | */
84 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 | const url = require('url'),
7 | ejs = require('ejs'),
8 | fs = require('fs'),
9 | watcher = require('./watcher');
10 | let ejsStr;
11 |
12 | module.exports = function mod_statuspage_init(config) {
13 | config = config || {};
14 | config.url = config.url || '/status';
15 | config.socketPath = config.socketPath || '/tmp/watcher.sock';
16 | config.errMessage = config.errMessage || 'Not Found';
17 | config.errCode = config.errCode || 404;
18 | config.responseContentType = config.responseContentType || 'html'; //valid values are html or json
19 | if (config.responseContentType !== 'json') {
20 | //if not json response will be in html
21 | config.ejsTemplate = config.ejsTemplate || __dirname + '/status.ejs';
22 | ejsStr = fs.readFileSync(config.ejsTemplate, 'utf8');
23 | }
24 |
25 | return function mod_statuspage(req, resp, next) {
26 | req.parsedUrl = req.parsedUrl || url.parse(req.url, true);
27 | if (req.parsedUrl.pathname === config.url) {
28 | if (typeof config.check !== 'function' || config.check(req)) {
29 | watcher(config.socketPath, function (err, data) {
30 | if (err) {
31 | // how do you diagnose a real errwatcherDataor?
32 | resp.writeHead(500, {
33 | 'Content-Type': 'text/plain',
34 | });
35 | resp.end('Watcher is not running');
36 | } else if (config.responseContentType === 'json') {
37 | resp.writeHead(200, {
38 | 'Content-Type': 'application/json',
39 | });
40 | resp.end(JSON.stringify(data));
41 | } else {
42 | resp.writeHead(200, {
43 | 'Content-Type': 'text/html',
44 | });
45 | resp.end(ejs.render(ejsStr, data));
46 | }
47 | });
48 | } else {
49 | resp.writeHead(config.errCode, {
50 | 'Content-Type': 'text/plain',
51 | });
52 | resp.end(config.errMessage);
53 | }
54 | } else {
55 | next();
56 | }
57 | };
58 | };
59 |
--------------------------------------------------------------------------------
/lib/status.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | function addCommas(nStr) {
3 | var x,
4 | x1,
5 | x2,
6 | rgx = /(\d+)(\d{3})/;
7 |
8 | nStr += '';
9 | x = nStr.split('.');
10 | x1 = x[0];
11 | x2 = x.length > 1 ? '.' + x[1] : '';
12 | while (rgx.test(x1)) {
13 | x1 = x1.replace(rgx, '$1' + ',' + '$2');
14 | }
15 | return x1 + x2;
16 | }
17 | %>
18 |
19 |
20 |
21 | NodeJS Status
22 |
23 |
24 | NodeJS Status for <%=hostname%>
25 | Server Version: node.js <%=node_version%> (<%=os_type%>/<%=os_release%>)
26 | Server Hostname: <%=hostname%>
27 | Current Time: <%=new Date(currentTime)%>
28 | <%
29 | if (locals.cluster_uptime) {
30 | var days = Math.floor(cluster_uptime / 86400),
31 | hrs = Math.floor((cluster_uptime % 86400) / 3600),
32 | mins_sec = ((cluster_uptime % 86400) % 3600),
33 | mins = Math.floor(mins_sec / 60),
34 | secs = mins_sec % 60,
35 | uptime = "" + (days !== 0 ? days + " day " : "")
36 | + (hrs !== 0 ? hrs + " hours " : "")
37 | + (mins !== 0 ? mins + " minutes " : "")
38 | + (secs !== 0 ? secs + " seconds" : "");
39 | %>
40 | Server uptime: <%=uptime%>
41 | <%
42 | }
43 | %>
44 | Total Requests: <%=total_requests%> - Total Traffic: <%=(total_kbs_transferred/(1000 *1000)).toFixed(2)%> GB (<%=addCommas(total_kbs_transferred.toFixed(2))%> KB)
45 | Memory: <%=addCommas(Math.round(free_memory/1000))%> KB free - Total : <%=addCommas(Math.round(total_memory/1000))%> KB available
46 | CPU Usage: - Load average <% os_loadavg.forEach(function (name) { %>
47 | <%=Math.round((parseFloat(name) || 0) * 100) / 100 %>
48 | <% }) %>
49 |
50 | <%=Math.round(total_rps * 10) / 10 %> requests/sec - <%=Math.round(total_kbs_out * 100) / 100%> kB/second
51 | <%if (total_rps === 0) {%>
52 |
53 | <%} else {%>
54 | - <%=Math.round(total_kbs_out / total_rps * 100) / 100%> kB/request
55 | <%}%>
56 |
57 |
58 |  Srv  |
59 |  PID  |
60 |  CPU  |
61 |  MEM  |
62 |  CPU/req  |
63 |  jiffy/req  |
64 |  req/sec  |
65 |  kB/sec  |
66 |  Events  |
67 |  Conn  |
68 |  OPEN REQ  |
69 |  Req  |
70 |  Trans  |
71 |  Started  |
72 | <% if (has_health_info) { %>
73 |  State  |
74 |  Health Code  |
75 |  Health Time  |
76 | <% } %>
77 |
78 | <% var index = 0, color; %>
79 | <% worker.forEach(function(name) { %>
80 |
81 | <%=++index%> |
82 | <%=name.pid%> |
83 | <%=name.cpu%> |
84 | <%=name.mem%> |
85 | <%=name.cpu_per_req%> |
86 | <%=name.jiffy_per_req%> |
87 | <%=name.rps%> |
88 | <%=name.kbs_out%> |
89 | <%=name.events%> |
90 | <%=name.open_conns%> |
91 | <%=name.open_requests%> |
92 | <%=name.total_requests%> |
93 | <%=Math.round(name.kbs_transferred / 100) / 10%> |
94 | <%=name.start_time_format%> |
95 | <% if (has_health_info) {
96 | color = name.health_is_down ? 'red' : 'green';
97 | %>
98 | <%=(name.health_is_down ? 'Down' : 'Running')%> |
99 | <%=name.health_status_code%> |
100 | <%=name.health_time_format%> |
101 | <% } %>
102 |
103 | <% }) %>
104 |
105 |
106 |
107 | Srv | Child Server number - generation
108 | |
---|
PID | OS process ID
109 | |
---|
CPU | CPU usage, number of seconds
110 | |
---|
MEM | Memory consume
111 | |
---|
CPU/req | CPU consumption per request
112 | |
---|
jiffy/req | Jiffy requests
113 | |
---|
req/sec | Request per second
114 | |
---|
kB/sec | Transfer rate in Kb/s
115 | |
---|
Events | Active events
116 | |
---|
Conn | Open connections
117 | |
---|
OPEN REQ | Number of open requests
118 | |
---|
Req | Total number of requests served
119 | |
---|
Trans | Total megabytes transferred this slot
120 | |
---|
Started | Process started time
121 | <% if (has_health_info) { %>
122 | |
---|
State | Application health
123 | |
---|
Health Code | Application returned health code
124 | |
---|
Health Time | Time when health information was received
125 | <% } %>
126 | |
---|
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/lib/watcher.js:
--------------------------------------------------------------------------------
1 | const os = require('os'),
2 | net = require('net'),
3 | bl = require('bl'),
4 | moment = require('moment');
5 |
6 | module.exports = function (socketPath, _callback) {
7 | var socket = net.createConnection(socketPath),
8 | response_obj = {};
9 |
10 | // because there's a possibility of multiple calls from the events
11 | // ensure it can't trigger a double-callback
12 | function callback(err, data) {
13 | if (_callback) {
14 | _callback(err, data);
15 | _callback = null;
16 | }
17 | }
18 |
19 | function collector(err, data) {
20 | if (err) {
21 | return callback(err);
22 | }
23 |
24 | var statuses = JSON.parse(data.toString()),
25 | hasHealthInfo = false;
26 | response_obj.hostname = os.hostname();
27 | response_obj.node_version = process.version;
28 | response_obj.os_type = os.type();
29 | response_obj.os_release = os.release();
30 | response_obj.currentTime = Date.now();
31 | if (process.clusterStartTime) {
32 | //Show Restart Time only if Process has the field clusterStartTime
33 | //this attribute should be set by the cluster master process
34 | response_obj.cluster_start_time =
35 | process.clusterStartTime.getTime();
36 | response_obj.cluster_uptime = Math.round(
37 | (new Date().getTime() - process.clusterStartTime.getTime()) /
38 | 1000
39 | );
40 | }
41 | response_obj.total_memory = os.totalmem();
42 | response_obj.free_memory = os.freemem();
43 | response_obj.os_loadavg = os.loadavg();
44 | response_obj.worker = [];
45 |
46 | response_obj.total_requests = 0;
47 | response_obj.total_kbs_transferred = 0;
48 | response_obj.total_kbs_out = 0;
49 | response_obj.total_rps = 0;
50 | Object.keys(statuses).forEach(function (pid) {
51 | var worker_json = {
52 | pid: pid,
53 | cpu: statuses[pid].curr.cpu,
54 | mem: statuses[pid].curr.mem,
55 | cpu_per_req: statuses[pid].curr.cpuperreq,
56 | jiffy_per_req: statuses[pid].curr.jiffyperreq,
57 | rps: statuses[pid].curr.rps,
58 | events: statuses[pid].curr.events,
59 | open_conns: statuses[pid].curr.oconns,
60 | open_requests: statuses[pid].curr.oreqs,
61 | total_requests: statuses[pid].curr.reqstotal,
62 | kbs_out: statuses[pid].curr.kbs_out,
63 | kbs_transferred: statuses[pid].curr.kb_trans,
64 | start_time: statuses[pid].curr.utcstart * 1000, //convert sec in millis
65 | start_time_format: moment
66 | .unix(statuses[pid].curr.utcstart)
67 | .fromNow(),
68 | };
69 | if (statuses[pid].curr.health_status_timestamp) {
70 | //convert sec in millis
71 | worker_json.health_status_timestamp =
72 | statuses[pid].curr.health_status_timestamp * 1000;
73 | worker_json.health_time_format = moment
74 | .unix(statuses[pid].curr.health_status_timestamp)
75 | .fromNow();
76 | worker_json.health_is_down = statuses[pid].curr.health_is_down;
77 | worker_json.health_status_code =
78 | statuses[pid].curr.health_status_code;
79 | hasHealthInfo = true;
80 | if (
81 | !response_obj.latest_health_timestamp ||
82 | statuses[pid].curr.health_status_timestamp >
83 | response_obj.latest_health_timestamp
84 | ) {
85 | response_obj.latest_health_timestamp =
86 | statuses[pid].curr.health_status_timestamp;
87 | response_obj.latest_health_is_down =
88 | worker_json.health_is_down;
89 | }
90 | }
91 | response_obj.worker.push(worker_json);
92 | response_obj.total_requests =
93 | response_obj.total_requests + statuses[pid].curr.reqstotal;
94 | response_obj.total_kbs_transferred =
95 | response_obj.total_kbs_transferred +
96 | statuses[pid].curr.kb_trans;
97 | response_obj.total_kbs_out =
98 | response_obj.total_kbs_out + statuses[pid].curr.kbs_out;
99 | response_obj.total_rps =
100 | response_obj.total_rps + statuses[pid].curr.rps;
101 | });
102 | response_obj.has_health_info = hasHealthInfo;
103 | return callback(null, response_obj);
104 | }
105 |
106 | socket.pipe(bl(collector));
107 | socket.on('close', function (error) {
108 | if (error) {
109 | callback(error);
110 | }
111 | });
112 | };
113 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mod_statuspage",
3 | "description": "mod_statuspage for Node",
4 | "author": "Vinit Sacheti ",
5 | "version": "1.1.1",
6 | "contributors": [
7 | "Sylvio Marcondes "
8 | ],
9 | "dependencies": {
10 | "bl": "^6.0.0",
11 | "ejs": "^3.1.8",
12 | "moment": "^2.29.4"
13 | },
14 | "devDependencies": {
15 | "eslint": "^8.30.0",
16 | "express": "^4.18.2",
17 | "nyc": "^15.1.0",
18 | "prettier": "^2.8.1",
19 | "vows": "^0.8.3"
20 | },
21 | "keywords": [
22 | "statuspage",
23 | "status",
24 | "nagios"
25 | ],
26 | "main": "./lib/index.js",
27 | "scripts": {
28 | "fix-lint": "prettier --write lib/*.js test/*.js",
29 | "pretest": "eslint lib/*.js test/*.js",
30 | "test": "nyc --reporter text-summary --reporter lcov --cache=false vows test/*.js"
31 | },
32 | "bugs": {
33 | "url": "http://github.com/yahoo/mod_statuspage/issues"
34 | },
35 | "licenses": [
36 | {
37 | "type": "BSD",
38 | "url": "https://github.com/yahoo/mod_statuspage/blob/master/LICENSE"
39 | }
40 | ],
41 | "repository": {
42 | "type": "git",
43 | "url": "http://github.com/yahoo/mod_statuspage.git"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 |
7 | const os = require('os'),
8 | vows = require('vows'),
9 | assert = require('assert'),
10 | bl = require('bl'),
11 | mod_status = require('../lib/index.js'),
12 | rightnow = new Date();
13 |
14 | var mockResponse = {
15 | 20780: {
16 | last: {
17 | cluster: 20780,
18 | title: '/home/y/libexec/node',
19 | pid: 20780,
20 | cpu: 0,
21 | user_cpu: 0,
22 | sys_cpu: 0,
23 | cpuperreq: 0,
24 | jiffyperreq: 0,
25 | events: 0.03225806451612906,
26 | elapsed: 151001.72,
27 | ts: 1337011626,
28 | mem: 0.57,
29 | reqstotal: 100,
30 | rps: 20,
31 | oreqs: 10,
32 | utcstart: 1337010906,
33 | oconns: 0,
34 | kb_trans: 0,
35 | kbs_out: 0,
36 | },
37 | kill: false,
38 | curr: {
39 | cluster: 20780,
40 | title: '/home/y/libexec/node',
41 | pid: 20780,
42 | cpu: 0,
43 | user_cpu: 0,
44 | sys_cpu: 0,
45 | cpuperreq: 0,
46 | jiffyperreq: 0,
47 | events: 0,
48 | elapsed: 0,
49 | ts: 1337011841,
50 | mem: 0,
51 | reqstotal: 0,
52 | rps: 0,
53 | oreqs: 0,
54 | utcstart: 1337010906,
55 | oconns: 0,
56 | kb_trans: 0,
57 | kbs_out: 0,
58 | health_status_timestamp: 1336010926,
59 | health_is_down: true,
60 | health_status_code: 200,
61 | },
62 | },
63 | 20799: {
64 | last: {
65 | cluster: 20799,
66 | title: '/home/y/libexec/node',
67 | pid: 20799,
68 | cpu: 0,
69 | user_cpu: 0,
70 | sys_cpu: 0,
71 | cpuperreq: 0,
72 | jiffyperreq: 0,
73 | events: 0.03225806451612906,
74 | elapsed: 151001.72,
75 | ts: 1337011626,
76 | mem: 0.57,
77 | reqstotal: 100,
78 | rps: 20,
79 | oreqs: 10,
80 | utcstart: 1337010906,
81 | oconns: 0,
82 | kb_trans: 0,
83 | kbs_out: 0,
84 | },
85 | kill: false,
86 | curr: {
87 | cluster: 20799,
88 | title: '/home/y/libexec/node',
89 | pid: 20799,
90 | cpu: 0,
91 | user_cpu: 0,
92 | sys_cpu: 0,
93 | cpuperreq: 0,
94 | jiffyperreq: 0,
95 | events: 0,
96 | elapsed: 0.01,
97 | ts: 1337011841,
98 | mem: 0.55,
99 | reqstotal: 100,
100 | rps: 20,
101 | oreqs: 2,
102 | utcstart: 1337010906,
103 | oconns: 0,
104 | kb_trans: 150,
105 | kbs_out: 100,
106 | health_status_timestamp: 1337010906,
107 | health_is_down: false,
108 | health_status_code: 200,
109 | },
110 | },
111 | 22760: {
112 | last: {
113 | cluster: 22760,
114 | title: '/home/y/libexec/node',
115 | pid: 22760,
116 | cpu: 6.252776074688882e-13,
117 | user_cpu: 5.5,
118 | sys_cpu: 0,
119 | cpuperreq: 0,
120 | jiffyperreq: 0,
121 | events: 0.12903225806610408,
122 | elapsed: 0,
123 | ts: 1337011798.44,
124 | mem: 0,
125 | reqstotal: 0,
126 | rps: 0,
127 | oreqs: 0,
128 | utcstart: 1337011798,
129 | oconns: 0,
130 | kb_trans: 0,
131 | kbs_out: 0,
132 | },
133 | kill: false,
134 | curr: {
135 | cluster: 22760,
136 | title: '/home/y/libexec/node',
137 | pid: 22760,
138 | cpu: 0,
139 | user_cpu: 0,
140 | sys_cpu: 0,
141 | cpuperreq: 0,
142 | jiffyperreq: 0,
143 | events: 0,
144 | elapsed: 0.01,
145 | ts: 1337011841.45,
146 | mem: 0.59,
147 | reqstotal: 50,
148 | rps: 5,
149 | oreqs: 2,
150 | utcstart: 1337011798,
151 | oconns: 1,
152 | kb_trans: 500,
153 | kbs_out: 125,
154 | },
155 | },
156 | };
157 | // mock net
158 | var netMock = {
159 | createConnection: function (socketPath) {
160 | var socket;
161 |
162 | // create a stream that'll pass on a Buffer
163 | socket = bl(new Buffer(JSON.stringify(mockResponse)));
164 |
165 | // faux errors from the stream for testing
166 | if (socketPath === 'error.sock') {
167 | process.nextTick(
168 | socket.emit.bind(socket, 'error', new Error('err'))
169 | );
170 | } else if (socketPath === 'error.on.close.sock') {
171 | process.nextTick(
172 | socket.emit.bind(socket, 'close', new Error('err'))
173 | );
174 | }
175 |
176 | return socket;
177 | },
178 | };
179 |
180 | require('net').createConnection = netMock.createConnection;
181 |
182 | process.on('uncaughtException', function (err) {
183 | console.log('Caught exception: ' + err + ' : ' + err.stack);
184 | });
185 |
186 | var tests = {
187 | loading: {
188 | topic: function () {
189 | return mod_status;
190 | },
191 | 'should be a function': function (topic) {
192 | assert.isFunction(topic);
193 | },
194 | 'and should return': {
195 | topic: function () {
196 | return mod_status();
197 | },
198 | 'a function': function (topic) {
199 | assert.isFunction(topic);
200 | },
201 | },
202 | },
203 | 'should go to next by default': {
204 | topic: function () {
205 | var fn = mod_status(),
206 | next = false;
207 | fn(
208 | {
209 | url: '/foo',
210 | },
211 | {},
212 | function () {
213 | next = true;
214 | }
215 | );
216 | return next;
217 | },
218 | 'should be true': function (topic) {
219 | assert.isTrue(topic);
220 | },
221 | },
222 | 'should send good json response': {
223 | topic: function () {
224 | var fn = mod_status({ responseContentType: 'json' }),
225 | code = null,
226 | next = false,
227 | text = null,
228 | self = this,
229 | req = {
230 | url: '/status',
231 | };
232 |
233 | process.clusterStartTime = rightnow;
234 | fn(
235 | req,
236 | {
237 | writeHead: function (c) {
238 | code = c;
239 | },
240 | end: function (d) {
241 | text = JSON.parse(d);
242 | self.callback(null, {
243 | code: code,
244 | text: text,
245 | next: next,
246 | parsedUrl: req.parsedUrl,
247 | });
248 | },
249 | },
250 | function () {
251 | next = true;
252 | }
253 | );
254 | },
255 | 'next should be false': function (topic) {
256 | assert.isFalse(topic.next);
257 | },
258 | 'request should be stamped with parsedUrl': function (topic) {
259 | assert.isObject(topic.parsedUrl);
260 | },
261 | 'json object returned should have valid values': function (topic) {
262 | var pid, i;
263 | assert.equal(topic.text.hostname, os.hostname());
264 | assert.equal(
265 | topic.text.total_requests,
266 | mockResponse['20799'].curr.reqstotal +
267 | mockResponse['22760'].curr.reqstotal
268 | );
269 | assert.equal(
270 | topic.text.total_kbs_out,
271 | mockResponse['20799'].curr.kbs_out +
272 | mockResponse['22760'].curr.kbs_out
273 | );
274 | assert.equal(
275 | topic.text.total_kbs_transferred,
276 | mockResponse['20799'].curr.kb_trans +
277 | mockResponse['22760'].curr.kb_trans
278 | );
279 | assert.equal(
280 | topic.text.total_rps,
281 | mockResponse['20799'].curr.rps + mockResponse['22760'].curr.rps
282 | );
283 | assert.equal(topic.text.worker.length, 3);
284 | for (i = 0; i < 3; i++) {
285 | pid = topic.text.worker[i].pid;
286 | assert.equal(
287 | topic.text.worker[i].cpu,
288 | mockResponse[pid].curr.cpu
289 | );
290 | assert.equal(
291 | topic.text.worker[i].mem,
292 | mockResponse[pid].curr.mem
293 | );
294 | assert.equal(
295 | topic.text.worker[i].cpu_per_req,
296 | mockResponse[pid].curr.cpuperreq
297 | );
298 | assert.equal(
299 | topic.text.worker[i].jiffy_per_req,
300 | mockResponse[pid].curr.jiffyperreq
301 | );
302 | assert.equal(
303 | topic.text.worker[i].rps,
304 | mockResponse[pid].curr.rps
305 | );
306 | assert.equal(
307 | topic.text.worker[i].events,
308 | mockResponse[pid].curr.events
309 | );
310 | assert.equal(
311 | topic.text.worker[i].open_conns,
312 | mockResponse[pid].curr.oconns
313 | );
314 | assert.equal(
315 | topic.text.worker[i].open_requests,
316 | mockResponse[pid].curr.oreqs
317 | );
318 | assert.equal(
319 | topic.text.worker[i].total_requests,
320 | mockResponse[pid].curr.reqstotal
321 | );
322 | assert.equal(
323 | topic.text.worker[i].kbs_out,
324 | mockResponse[pid].curr.kbs_out
325 | );
326 | assert.equal(
327 | topic.text.worker[i].kbs_transferred,
328 | mockResponse[pid].curr.kb_trans
329 | );
330 | assert.equal(
331 | topic.text.worker[i].start_time,
332 | mockResponse[pid].curr.utcstart * 1000
333 | );
334 | assert.ok(topic.text.worker[i].start_time_format);
335 | if (pid === '22760') {
336 | assert.equal(
337 | undefined,
338 | topic.text.worker[i].health_status_timestamp,
339 | 'health timestamp for pid: ' + pid
340 | );
341 | assert.equal(
342 | undefined,
343 | topic.text.worker[i].health_time_format,
344 | 'health time format for pid: ' + pid
345 | );
346 | assert.equal(
347 | undefined,
348 | topic.text.worker[i].health_is_down,
349 | 'health is down for pid: ' + pid
350 | );
351 | assert.equal(
352 | undefined,
353 | topic.text.worker[i].health_status_code,
354 | 'health status code for pid: ' + pid
355 | );
356 | } else if (pid === '20799') {
357 | assert.equal(
358 | mockResponse[pid].curr.health_status_timestamp * 1000,
359 | topic.text.worker[i].health_status_timestamp,
360 | 'health timestamp for pid: ' + pid
361 | );
362 | assert.ok(
363 | topic.text.worker[i].health_time_format,
364 | 'health time format for pid: ' + pid
365 | );
366 | assert.equal(
367 | mockResponse[pid].curr.health_is_down,
368 | topic.text.worker[i].health_is_down,
369 | 'health is down for pid: ' + pid
370 | );
371 | assert.equal(
372 | mockResponse[pid].curr.health_status_code,
373 | topic.text.worker[i].health_status_code,
374 | 'health status code for pid: ' + pid
375 | );
376 | } else if (pid === '20780') {
377 | assert.equal(
378 | mockResponse[pid].curr.health_status_timestamp * 1000,
379 | topic.text.worker[i].health_status_timestamp,
380 | 'health timestamp for pid: ' + pid
381 | );
382 | } else {
383 | assert.ok(false, 'unreachable code'); //should not reach here
384 | }
385 | }
386 | assert.equal(topic.text.hostname, os.hostname());
387 | assert.equal(topic.text.node_version, process.version);
388 | assert.equal(topic.text.os_type, os.type());
389 | assert.equal(topic.text.os_release, os.release());
390 | assert.equal(topic.text.cluster_start_time, rightnow.getTime());
391 | assert.equal(1337010906, topic.text.latest_health_timestamp);
392 | assert.equal(false, topic.text.latest_health_is_down);
393 | },
394 | 'code should be 200': function (topic) {
395 | assert.equal(topic.code, 200);
396 | },
397 | },
398 | 'should send bad response if check config returns false': {
399 | topic: function () {
400 | var fn = mod_status({
401 | check: function () {
402 | return false;
403 | },
404 | }),
405 | code = null,
406 | next = false,
407 | text = null;
408 |
409 | fn(
410 | {
411 | url: '/status',
412 | },
413 | {
414 | writeHead: function (c) {
415 | code = c;
416 | },
417 | end: function (d) {
418 | text = d;
419 | },
420 | },
421 | function () {
422 | next = true;
423 | }
424 | );
425 | return {
426 | code: code,
427 | text: text,
428 | next: next,
429 | };
430 | },
431 | 'next should be false': function (topic) {
432 | assert.isFalse(topic.next);
433 | },
434 | 'data should be Not Found': function (topic) {
435 | assert.equal(topic.text, 'Not Found');
436 | },
437 | 'code should be 404': function (topic) {
438 | assert.equal(topic.code, 404);
439 | },
440 | },
441 | 'should send bad response on socket error': {
442 | topic: function () {
443 | var fn = mod_status({
444 | socketPath: 'error.sock',
445 | }),
446 | code = null,
447 | next = false,
448 | text = null,
449 | self = this;
450 |
451 | fn(
452 | {
453 | url: '/status',
454 | },
455 | {
456 | writeHead: function (c) {
457 | code = c;
458 | },
459 | end: function (d) {
460 | text = d;
461 | self.callback(null, {
462 | code: code,
463 | text: text,
464 | next: next,
465 | });
466 | },
467 | },
468 | function () {
469 | next = true;
470 | }
471 | );
472 | },
473 | 'next should be false': function (topic) {
474 | assert.isFalse(topic.next);
475 | },
476 | 'data should be Watcher is not running': function (topic) {
477 | assert.equal(topic.text, 'Watcher is not running');
478 | },
479 | 'code should be 500': function (topic) {
480 | assert.equal(topic.code, 500);
481 | },
482 | },
483 | 'should send bad response on socket errors out on close': {
484 | topic: function () {
485 | var fn = mod_status({
486 | socketPath: 'error.on.close.sock',
487 | }),
488 | code = null,
489 | next = false,
490 | text = null,
491 | self = this;
492 |
493 | fn(
494 | {
495 | url: '/status',
496 | },
497 | {
498 | writeHead: function (c) {
499 | code = c;
500 | },
501 | end: function (d) {
502 | text = d;
503 | self.callback(null, {
504 | code: code,
505 | text: text,
506 | next: next,
507 | });
508 | },
509 | },
510 | function () {
511 | next = true;
512 | }
513 | );
514 | },
515 | 'next should be false': function (topic) {
516 | assert.isFalse(topic.next);
517 | },
518 | 'data should be Watcher is not running': function (topic) {
519 | assert.equal(topic.text, 'Watcher is not running');
520 | },
521 | 'code should be 500': function (topic) {
522 | assert.equal(topic.code, 500);
523 | },
524 | },
525 | 'should send good html response if responseContentType is something but not json':
526 | {
527 | topic: function () {
528 | var fn = mod_status({ responseContentType: 'xyz' }),
529 | code = null,
530 | next = false,
531 | text = null,
532 | self = this;
533 |
534 | process.clusterStartTime = rightnow;
535 | fn(
536 | {
537 | parsedUrl: {
538 | pathname: '/status',
539 | },
540 | url: '/foo', //foo should be ignored as request has a parsedUrl
541 | },
542 | {
543 | writeHead: function (c) {
544 | code = c;
545 | },
546 | end: function (d) {
547 | text = d;
548 | self.callback(null, {
549 | code: code,
550 | text: text,
551 | next: next,
552 | });
553 | },
554 | },
555 | function () {
556 | next = true;
557 | }
558 | );
559 | },
560 | 'next should be false': function (topic) {
561 | assert.isFalse(topic.next);
562 | },
563 | 'text should be html': function (topic) {
564 | assert.ok(topic.text.indexOf('html') !== -1);
565 | },
566 | 'code should be 200': function (topic) {
567 | assert.equal(topic.code, 200);
568 | },
569 | },
570 | 'response page should have proper error code and message': {
571 | topic: function () {
572 | var fn = mod_status({
573 | check: function () {
574 | return false;
575 | },
576 | errCode: 403,
577 | errMessage: 'Auth ERROR',
578 | }),
579 | code = null,
580 | next = false,
581 | text = null;
582 |
583 | fn(
584 | {
585 | url: '/status',
586 | },
587 | {
588 | writeHead: function (c) {
589 | code = c;
590 | },
591 | end: function (d) {
592 | text = d;
593 | },
594 | },
595 | function () {
596 | next = true;
597 | }
598 | );
599 | return {
600 | code: code,
601 | text: text,
602 | next: next,
603 | };
604 | },
605 | 'next should be false': function (topic) {
606 | assert.isFalse(topic.next);
607 | },
608 | 'data should be Auth ERROR': function (topic) {
609 | assert.equal(topic.text, 'Auth ERROR');
610 | },
611 | 'code should be 403': function (topic) {
612 | assert.equal(topic.code, 403);
613 | },
614 | },
615 | };
616 |
617 | vows.describe('mod_status').addBatch(tests)['export'](module);
618 |
--------------------------------------------------------------------------------