├── .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 | [![Build Status](https://secure.travis-ci.org/yahoo/mod_statuspage.png?branch=master)](http://travis-ci.org/yahoo/mod_statuspage) 63 | 64 | 65 | Node Badge 66 | ---------- 67 | 68 | [![NPM](https://nodei.co/npm/mod_statuspage.png)](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 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | <% if (has_health_info) { %> 73 | 74 | 75 | 76 | <% } %> 77 | 78 | <% var index = 0, color; %> 79 | <% worker.forEach(function(name) { %> 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | <% if (has_health_info) { 96 | color = name.health_is_down ? 'red' : 'green'; 97 | %> 98 | 99 | 100 | 101 | <% } %> 102 | 103 | <% }) %> 104 |
 Srv  PID  CPU  MEM  CPU/req  jiffy/req  req/sec  kB/sec  Events  Conn  OPEN REQ  Req  Trans  Started  State  Health Code  Health Time 
<%=++index%><%=name.pid%><%=name.cpu%><%=name.mem%><%=name.cpu_per_req%><%=name.jiffy_per_req%> <%=name.rps%><%=name.kbs_out%><%=name.events%><%=name.open_conns%><%=name.open_requests%><%=name.total_requests%><%=Math.round(name.kbs_transferred / 100) / 10%><%=name.start_time_format%><%=(name.health_is_down ? 'Down' : 'Running')%><%=name.health_status_code%><%=name.health_time_format%>
105 |
106 | 107 |
SrvChild Server number - generation 108 |
PIDOS process ID 109 |
CPUCPU usage, number of seconds 110 |
MEMMemory consume 111 |
CPU/reqCPU consumption per request 112 |
jiffy/reqJiffy requests 113 |
req/secRequest per second 114 |
kB/secTransfer rate in Kb/s 115 |
EventsActive events 116 |
ConnOpen connections 117 |
OPEN REQNumber of open requests 118 |
ReqTotal number of requests served 119 |
TransTotal megabytes transferred this slot 120 |
StartedProcess started time 121 | <% if (has_health_info) { %> 122 |
StateApplication health 123 |
Health CodeApplication returned health code 124 |
Health TimeTime 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 | --------------------------------------------------------------------------------