├── webserver ├── public │ ├── less │ │ ├── header.less │ │ ├── validation.less │ │ ├── ng-table-custom.less │ │ ├── footer.less │ │ ├── service-edition.less │ │ ├── general.less │ │ ├── services-sidebar.less │ │ ├── charting.less │ │ ├── service-details.less │ │ └── service-list.less │ ├── js │ │ ├── controllers │ │ │ ├── controllers.js │ │ │ ├── service-add.js │ │ │ ├── service-edit.js │ │ │ ├── services-list.js │ │ │ └── service-details.js │ │ ├── directives │ │ │ ├── directives.js │ │ │ └── ping-service-options.js │ │ ├── app.js │ │ ├── factories.js │ │ ├── ngTable-utils.js │ │ └── charting │ │ │ └── charting.js │ └── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 ├── views │ ├── partials │ │ ├── outages.html │ │ ├── ping-service-options.html │ │ └── services-sidebar.html │ ├── header.html │ ├── index.html │ └── service-list.html ├── routes │ ├── web-route.js │ ├── api-ping-plugins-route.js │ ├── api-report-route.js │ ├── web-auth-route.js │ └── api-service-route.js └── app.js ├── .travis.yml ├── .bowerrc ├── test ├── redis.test.conf ├── fixtures │ ├── notifications │ │ └── services │ │ │ ├── service1 │ │ │ ├── config.js │ │ │ └── service.js │ │ │ └── service2 │ │ │ ├── config.js │ │ │ └── service.js │ ├── dummy-services.js │ └── real-services.js ├── lib │ ├── mock │ │ ├── request-mocked.js │ │ └── storage-mocked.js │ └── util │ │ ├── super-agent-assertions.js │ │ └── populator.js ├── test-aggregator.js ├── test-api-ping-plugins-route.js ├── test-sentinel.js ├── test-service-access-filter.js └── test-service-validation.js ├── Procfile ├── screenshots ├── pm2-01.png ├── watchmen-01.png ├── watchmen-add.png ├── watchmen-details-01.png ├── watchmen-droplet-01.png ├── ping-service-selection.png ├── test-coverage-node-01.png ├── watchmen-list-wide-01.png ├── watchmen-list-mobile-01.png └── watchmen-details-mobile-01.png ├── scripts ├── populate-dummy-data-7days.sh ├── populate-dummy-data-120days.sh ├── populate-dummy-data-30days.sh ├── data-load │ ├── flush-database.js │ ├── lib │ │ └── response-randomizer.js │ ├── populate-services-from-fixtures.js │ ├── find-memory-leaks.js │ └── populate-dummy-data.js ├── show-services.js └── report-service.js ├── coverage └── lcov-report │ ├── sort-arrow-sprite.png │ ├── prettify.css │ ├── config │ ├── notifications │ │ ├── services │ │ │ ├── postmark.js.html │ │ │ ├── aws-ses.js.html │ │ │ └── index.html │ │ ├── index.html │ │ └── notifications.js.html │ ├── general.js.html │ ├── web.js.html │ ├── index.html │ └── storage.js.html │ ├── lib │ ├── aggregator.js.html │ ├── storage │ │ ├── index.html │ │ ├── providers │ │ │ └── index.html │ │ ├── storage-factory.js.html │ │ └── base.js.html │ ├── notifications │ │ ├── index.html │ │ └── services │ │ │ ├── aws-ses │ │ │ └── index.html │ │ │ └── postmark │ │ │ └── index.html │ ├── ping_services │ │ └── index.html │ └── utils.js.html │ ├── webserver │ ├── index.html │ └── routes │ │ └── web-route.js.html │ ├── base.css │ └── sorter.js ├── .dockerignore ├── terraform └── digital-ocean │ ├── provider.tf │ ├── apply.sh │ ├── plan.sh │ ├── destroy.sh │ ├── watchmen.tf │ └── user-data.yml ├── .gitignore ├── redis.conf ├── Dockerfile ├── lib ├── aggregator.js ├── storage │ └── storage-factory.js ├── plugin-loader.js ├── utils.js ├── service-access-filter.js ├── service-validator.js └── sentinel.js ├── redis.dev.conf ├── docker-compose.yml ├── bower.json ├── config ├── web.js └── storage.js ├── docker-compose.env ├── LICENSE-MIT ├── run-web-server.js ├── run-monitor-server.js ├── gulpfile.js └── package.json /webserver/public/less/header.less: -------------------------------------------------------------------------------- 1 | #header { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "webserver/public/bower_components" 3 | } -------------------------------------------------------------------------------- /test/redis.test.conf: -------------------------------------------------------------------------------- 1 | port 6666 2 | daemonize yes 3 | pidfile test/redis.pid -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | monitor: node run-monitor-server.js 2 | web: node run-web-server.js 3 | -------------------------------------------------------------------------------- /webserver/public/js/controllers/controllers.js: -------------------------------------------------------------------------------- 1 | angular.module('watchmenControllers', []); -------------------------------------------------------------------------------- /webserver/public/js/directives/directives.js: -------------------------------------------------------------------------------- 1 | angular.module('watchmenDirectives', []); -------------------------------------------------------------------------------- /screenshots/pm2-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/pm2-01.png -------------------------------------------------------------------------------- /screenshots/watchmen-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/watchmen-01.png -------------------------------------------------------------------------------- /screenshots/watchmen-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/watchmen-add.png -------------------------------------------------------------------------------- /scripts/populate-dummy-data-7days.sh: -------------------------------------------------------------------------------- 1 | export set DEBUG='data-load' 2 | node data-load/populate-dummy-data.js -d 7 -s 30 -------------------------------------------------------------------------------- /screenshots/watchmen-details-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/watchmen-details-01.png -------------------------------------------------------------------------------- /screenshots/watchmen-droplet-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/watchmen-droplet-01.png -------------------------------------------------------------------------------- /scripts/populate-dummy-data-120days.sh: -------------------------------------------------------------------------------- 1 | export set DEBUG='data-load' 2 | node data-load/populate-dummy-data.js -d 120 -s 30 -------------------------------------------------------------------------------- /scripts/populate-dummy-data-30days.sh: -------------------------------------------------------------------------------- 1 | export set DEBUG='data-load' 2 | node data-load/populate-dummy-data.js -d 30 -s 30 -------------------------------------------------------------------------------- /screenshots/ping-service-selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/ping-service-selection.png -------------------------------------------------------------------------------- /screenshots/test-coverage-node-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/test-coverage-node-01.png -------------------------------------------------------------------------------- /screenshots/watchmen-list-wide-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/watchmen-list-wide-01.png -------------------------------------------------------------------------------- /screenshots/watchmen-list-mobile-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/watchmen-list-mobile-01.png -------------------------------------------------------------------------------- /coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /screenshots/watchmen-details-mobile-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/screenshots/watchmen-details-mobile-01.png -------------------------------------------------------------------------------- /webserver/public/less/validation.less: -------------------------------------------------------------------------------- 1 | input.ng-dirty.ng-invalid, input.ng-touched.ng-invalid { 2 | color: red; 3 | border-color: red; 4 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | screenshots 3 | logs/ 4 | .monitor 5 | .DS_Store 6 | node_modules/ 7 | *.rdb 8 | .idea/ 9 | webserver/public/bower_components/ -------------------------------------------------------------------------------- /test/fixtures/notifications/services/service1/config.js: -------------------------------------------------------------------------------- 1 | module.exports = (function () { 2 | return { 3 | CONFIG_KEY: "empty" 4 | }; 5 | })(); -------------------------------------------------------------------------------- /test/fixtures/notifications/services/service2/config.js: -------------------------------------------------------------------------------- 1 | module.exports = (function () { 2 | return { 3 | CONFIG_KEY: "empty" 4 | }; 5 | })(); -------------------------------------------------------------------------------- /webserver/public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/webserver/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /webserver/public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/webserver/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /webserver/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/webserver/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /webserver/public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iloire/watchmen/HEAD/webserver/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /terraform/digital-ocean/provider.tf: -------------------------------------------------------------------------------- 1 | variable "do_token" {} 2 | variable "ssh_fingerprint" {} 3 | 4 | provider "digitalocean" { 5 | token = "${var.do_token}" 6 | } 7 | -------------------------------------------------------------------------------- /webserver/public/less/ng-table-custom.less: -------------------------------------------------------------------------------- 1 | .ng-table th.sortable.sort-desc, .ng-table th.sortable.sort-asc { 2 | background-color: #f5f5f5; 3 | border-radius: 3px; 4 | } -------------------------------------------------------------------------------- /webserver/public/less/footer.less: -------------------------------------------------------------------------------- 1 | footer { 2 | 3 | color: #ccc; 4 | padding: 20px; 5 | text-align: center; 6 | 7 | a { 8 | color: #c2c2c2; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | .monitor 3 | .DS_Store 4 | node_modules/ 5 | *.rdb 6 | .idea/ 7 | webserver/public/bower_components/ 8 | terraform/digital-ocean/*.tfstate 9 | terraform/digital-ocean/*.tfstate.backup 10 | -------------------------------------------------------------------------------- /redis.conf: -------------------------------------------------------------------------------- 1 | bind 127.0.0.1 2 | port 1216 3 | daemonize yes 4 | 5 | # data directory 6 | dir /home/ec2-user/watchmen 7 | 8 | # data filename 9 | dbfilename watchmen.rdb 10 | 11 | save 60 1 12 | save 60 100 13 | -------------------------------------------------------------------------------- /webserver/public/less/service-edition.less: -------------------------------------------------------------------------------- 1 | .service-edition { 2 | 3 | .descr { 4 | color: #c2c2c2; 5 | padding-top: 0.4em; 6 | } 7 | 8 | .ping-service-option-panel { 9 | padding: 0.5em; 10 | } 11 | } -------------------------------------------------------------------------------- /terraform/digital-ocean/apply.sh: -------------------------------------------------------------------------------- 1 | # make sure you: 2 | # export DIGITALOCEAN_TOKEN= 3 | # export SSH_FINGERPRINT= 4 | terraform apply \ 5 | -var "do_token=${DIGITALOCEAN_TOKEN}" \ 6 | -var "ssh_fingerprint=$SSH_FINGERPRINT" 7 | -------------------------------------------------------------------------------- /terraform/digital-ocean/plan.sh: -------------------------------------------------------------------------------- 1 | # make sure you: 2 | # export DIGITALOCEAN_TOKEN= 3 | # export SSH_FINGERPRINT= 4 | terraform plan \ 5 | -var "do_token=${DIGITALOCEAN_TOKEN}" \ 6 | -var "ssh_fingerprint=$SSH_FINGERPRINT" 7 | -------------------------------------------------------------------------------- /terraform/digital-ocean/destroy.sh: -------------------------------------------------------------------------------- 1 | # make sure you: 2 | # export DIGITALOCEAN_TOKEN= 3 | # export SSH_FINGERPRINT= 4 | terraform destroy \ 5 | -var "do_token=${DIGITALOCEAN_TOKEN}" \ 6 | -var "ssh_fingerprint=$SSH_FINGERPRINT" 7 | -------------------------------------------------------------------------------- /scripts/data-load/flush-database.js: -------------------------------------------------------------------------------- 1 | var storageFactory = require('../../lib/storage/storage-factory'); 2 | var storage = storageFactory.getStorageInstance('development'); //TODO 3 | 4 | storage.flush_database(function(){ 5 | storage.quit(); 6 | console.log('done'); 7 | }); -------------------------------------------------------------------------------- /test/fixtures/notifications/services/service1/service.js: -------------------------------------------------------------------------------- 1 | module.exports = Service; 2 | 3 | function Service(config) { 4 | this.config = config; 5 | } 6 | 7 | Service.prototype.getName = function () { 8 | return 'service 1'; 9 | }; 10 | 11 | Service.prototype.send = function (options, cb) { 12 | cb(); 13 | }; -------------------------------------------------------------------------------- /terraform/digital-ocean/watchmen.tf: -------------------------------------------------------------------------------- 1 | resource "digitalocean_droplet" "watchmen" { 2 | image = "ubuntu-14-04-x64" 3 | name = "watchen-droplet" 4 | region = "nyc2" 5 | size = "512mb" 6 | user_data = "${file("user-data.yml")}" 7 | ssh_keys = [ 8 | "${var.ssh_fingerprint}" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/notifications/services/service2/service.js: -------------------------------------------------------------------------------- 1 | module.exports = Service2; 2 | 3 | function Service2(config) { 4 | this.config = config; 5 | } 6 | 7 | Service2.prototype.getName = function () { 8 | return 'service 2'; 9 | }; 10 | 11 | Service2.prototype.send = function (options, cb) { 12 | cb(); 13 | }; -------------------------------------------------------------------------------- /webserver/public/less/general.less: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, *:before, *:after { 5 | box-sizing: inherit; 6 | } 7 | 8 | body { 9 | padding-top: 60px; 10 | } 11 | 12 | a { 13 | 14 | } 15 | 16 | .spinner { 17 | opacity: 0.8; 18 | } 19 | 20 | h2 { 21 | margin: 10px 0; 22 | color: gray; 23 | font-size: 2.1em; 24 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4.4 2 | 3 | WORKDIR /watchmen 4 | 5 | # Installs dependencies first 6 | ADD package.json bower.json .bowerrc /watchmen/ 7 | RUN set -x \ 8 | && npm install -g bower \ 9 | && npm install \ 10 | && bower install --allow-root 11 | 12 | # Add all the project 13 | ADD . /watchmen 14 | 15 | ENV WATCHMEN_WEB_PORT=3000 16 | 17 | EXPOSE 3000 18 | -------------------------------------------------------------------------------- /lib/aggregator.js: -------------------------------------------------------------------------------- 1 | var spigot = require("stream-spigot"); 2 | var agg = require("timestream-aggregates"); 3 | var concat = require("concat-stream"); 4 | 5 | exports = module.exports = (function(){ 6 | 7 | return { 8 | aggregate: function(arr, timeunit, cb){ 9 | spigot({objectMode: true}, arr) 10 | .pipe(agg.mean("t", timeunit)) 11 | .pipe(concat(cb)); 12 | } 13 | }; 14 | 15 | })(); -------------------------------------------------------------------------------- /redis.dev.conf: -------------------------------------------------------------------------------- 1 | bind 127.0.0.1 2 | port 1216 3 | daemonize no 4 | dbfilename watchmen-dev.rdb 5 | 6 | # Specify the server verbosity level. 7 | # This can be one of: 8 | # debug (a lot of information, useful for development/testing) 9 | # verbose (many rarely useful info, but not a mess like the debug level) 10 | # notice (moderately verbose, what you want in production probably) 11 | # warning (only very important / critical messages are logged) 12 | loglevel debug 13 | 14 | save 900 1 15 | -------------------------------------------------------------------------------- /test/lib/mock/request-mocked.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Convenient HTTP ping service mock for testing purposes 4 | */ 5 | 6 | exports = module.exports = (function(){ 7 | 8 | return { 9 | 10 | mockedResponse : {}, 11 | 12 | ping: function (service, callback) { 13 | var res = this.mockedResponse; 14 | setTimeout(function () { // simulate async 15 | callback(res.error, res.body, res.response, res.latency); 16 | }, res.latency); 17 | } 18 | 19 | }; 20 | 21 | })(); 22 | -------------------------------------------------------------------------------- /webserver/views/partials/outages.html: -------------------------------------------------------------------------------- 1 |

Latest outages

2 |
3 |
4 |
5 | {{log.error}} 6 |
7 |
8 | Downtime: 9 |
10 |
11 | {{log.timestamp | amDateFormat:'MMMM Do, HH:mm:ss'}} 12 | - 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | redis: 2 | image: redis:2 3 | ports: 4 | - "6379" 5 | # expose db in the host /data dir, make sure to create and set permissions for this folder 6 | volumes: 7 | - ./redis.conf:/usr/local/etc/redis/redis.conf 8 | - /data:/data 9 | watchmenweb: 10 | build: ./ 11 | ports: 12 | - "3000:3000" 13 | links: 14 | - redis 15 | command: node run-web-server.js 16 | env_file: docker-compose.env 17 | watchmenserver: 18 | build: ./ 19 | links: 20 | - redis 21 | command: node run-monitor-server.js 22 | env_file: docker-compose.env 23 | -------------------------------------------------------------------------------- /lib/storage/storage-factory.js: -------------------------------------------------------------------------------- 1 | var storageConfiguration = require ('../../config/storage'); 2 | 3 | module.exports = { 4 | 5 | getStorageInstance : function (env){ 6 | 7 | var config = storageConfiguration[env]; 8 | if (!config) { 9 | console.error('No environment found for ', env); 10 | return null; 11 | } 12 | 13 | console.log('Using storage env: ', env); 14 | 15 | var provider = storageConfiguration[env].provider; 16 | var providerOptions = require ('../../config/storage')[env].options[provider]; 17 | var storage = require ('./providers/' + provider); 18 | 19 | return new storage(providerOptions); 20 | } 21 | }; -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "watchmen", 3 | "description": "A simple service monitor", 4 | "version": "3.3.1", 5 | "homepage": "https://github.com/iloire/watchmen", 6 | "license": "MIT", 7 | "private": true, 8 | "dependencies": { 9 | "angular": "1.3.x", 10 | "angular-resource": "1.3.x", 11 | "angular-moment": "0.9.x", 12 | "ng-table": "^0.5.4", 13 | "bootstrap": "3.3.x", 14 | "moment": "2.9.x", 15 | "angular-spinner": "~0.6.1", 16 | "c3": "~0.4.10", 17 | "angular-ui-router": "~0.2.14", 18 | "angular-ms-time": "~1.0.0", 19 | "lodash": "~3.8.0" 20 | }, 21 | "resolutions": { 22 | "bootstrap": "~3.3.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/web.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | no_auth: process.env.WATCHMEN_WEB_NO_AUTH === 'true', 4 | 5 | public_host_name: process.env.WATCHMEN_BASE_URL, // required for OAuth dance 6 | 7 | auth: { 8 | GOOGLE_CLIENT_ID: process.env.WATCHMEN_GOOGLE_CLIENT_ID || '', 9 | GOOGLE_CLIENT_SECRET: process.env.WATCHMEN_GOOGLE_CLIENT_SECRET || '' 10 | }, 11 | 12 | port: process.env.WATCHMEN_WEB_PORT, // default port 13 | 14 | admins: process.env.WATCHMEN_ADMINS, 15 | 16 | ga_analytics_ID: process.env.WATCHMEN_GOOGLE_ANALYTICS_ID, 17 | 18 | baseUrl: '/' 19 | }; 20 | -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /lib/plugin-loader.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | exports = module.exports = (function(){ 4 | 5 | var PREFIX_PING_PLUGIN = 'watchmen-plugin-'; 6 | var pkgJson = require(path.resolve(__dirname, '../package.json')); 7 | 8 | return { 9 | 10 | /** 11 | * Load plugins from node_modules 12 | * @param watchmen 13 | * @param cb 14 | */ 15 | 16 | loadPlugins : function (watchmen, options, cb) { 17 | console.log('\nloading plugins from nodemodules... '.gray); 18 | for (var dep in pkgJson.dependencies) { 19 | if (dep.indexOf(PREFIX_PING_PLUGIN) === 0){ 20 | new require(dep)(watchmen); 21 | } 22 | } 23 | cb(); 24 | } 25 | }; 26 | 27 | })(); -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = (function(){ 2 | 3 | return { 4 | 5 | /** 6 | * Round number 7 | * @param number 8 | * @param decimals 9 | * @returns {number} 10 | */ 11 | 12 | round: function (number, decimals) { 13 | if (typeof decimals === 'undefined') { 14 | decimals = 2; 15 | } 16 | return Math.round(number * Math.pow(10, decimals)) / Math.pow(10, decimals); 17 | }, 18 | 19 | /** 20 | * Get random integer on the range specified 21 | * @param min 22 | * @param max 23 | * @returns {Number} random integer 24 | */ 25 | 26 | getRandomInt: function (min, max) { 27 | return Math.floor(Math.random() * (max - min)) + min; 28 | } 29 | }; 30 | 31 | })(); -------------------------------------------------------------------------------- /webserver/views/partials/ping-service-options.html: -------------------------------------------------------------------------------- 1 |
2 |

Ping options

3 |
4 |
5 | 6 |
7 | 13 |
14 |
{{obj.descr}}
15 |
16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /docker-compose.env: -------------------------------------------------------------------------------- 1 | ## ---------------------------------------------------------------------- 2 | ## Create a docker-compose.env based on this template 3 | ## ---------------------------------------------------------------------- 4 | 5 | # Basic configuration 6 | WATCHMEN_BASE_URL='http://localhost:3000' 7 | WATCHMEN_WEB_PORT=3000 8 | WATCHMEN_ADMINS='admin-email@domain.com' 9 | WATCHMEN_GOOGLE_ANALYTICS_ID='your-GA-ID' 10 | 11 | # Google OAuth configuration 12 | WATCHMEN_GOOGLE_CLIENT_ID='' 13 | WATCHMEN_GOOGLE_CLIENT_SECRET='' 14 | 15 | # Ignore Unauthorized SSL certificates 16 | NODE_TLS_REJECT_UNAUTHORIZED=0 17 | 18 | # Run in production mode 19 | NODE_ENV=production 20 | 21 | # Use redis service link, DNS entry 22 | WATCHMEN_REDIS_PORT_PRODUCTION=6379 23 | WATCHMEN_REDIS_ADDR_PRODUCTION=redis 24 | -------------------------------------------------------------------------------- /webserver/public/less/services-sidebar.less: -------------------------------------------------------------------------------- 1 | .services-sidebar { 2 | padding:1em 0 0 0.5em; 3 | font-size: 1em; 4 | 5 | table { 6 | 7 | width: 100%; 8 | 9 | td.name { 10 | max-width: 0; 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | white-space: nowrap; 14 | } 15 | 16 | td.uptime { 17 | width:30%; 18 | padding-right:5px; 19 | 20 | span { 21 | display: inline-block; 22 | min-width: 45px; 23 | font-weight: bold; 24 | 25 | &.down { 26 | color:#d9534f; 27 | } 28 | 29 | &.up { 30 | color: green; 31 | } 32 | } 33 | } 34 | 35 | tr.selected { 36 | a { 37 | color:black; 38 | font-weight: bold; 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /test/lib/util/super-agent-assertions.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = (function(){ 2 | 3 | return { 4 | 5 | shouldReturnStatusCode: function (agent, options, done) { 6 | agent 7 | .get(options.url) 8 | .set('Accept', 'application/json') 9 | .expect('Content-Type', /json/) 10 | .expect(options.statusCode) 11 | .send() 12 | .end(function (err) { 13 | done(err); 14 | }); 15 | }, 16 | 17 | shouldDenyPostingTo: function(agent, url, done) { 18 | agent 19 | .post(url) 20 | .set('Accept', 'application/json') 21 | .expect('Content-Type', /json/) 22 | .expect(401) 23 | .send() 24 | .end(function (err) { 25 | done(err); 26 | }); 27 | } 28 | }; 29 | 30 | })(); -------------------------------------------------------------------------------- /scripts/show-services.js: -------------------------------------------------------------------------------- 1 | var storageFactory = require('../lib/storage/storage-factory'); 2 | var program = require('commander'); 3 | 4 | function run(program, cb){ 5 | var env = program.env || 'development'; 6 | var storage = storageFactory.getStorageInstance(env); 7 | if (!storage){ 8 | return cb('Invalid storage'); 9 | } 10 | storage.getServices({}, function(err, services){ 11 | services.forEach(function(s){ 12 | console.log(s.id, s.name, s.url, s.interval); 13 | }); 14 | storage.quit(); 15 | cb(); 16 | }); 17 | } 18 | 19 | program 20 | .option('-e, --env [env]', 'Storage environment key') 21 | .parse(process.argv); 22 | 23 | run(program, function (err) { 24 | if (err) { 25 | console.error(err); 26 | } 27 | else { 28 | console.log('done!'); 29 | } 30 | process.exit(0); 31 | }); -------------------------------------------------------------------------------- /webserver/routes/web-route.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config/web'); 2 | var express = require('express'); 3 | 4 | module.exports.getRoutes = function (){ 5 | 6 | var router = express.Router(); 7 | 8 | function serveIndex(req, res){ 9 | res.render('index.html', { 10 | title: 'watchmen' 11 | }); 12 | } 13 | 14 | router.all('*', function(req, res, next){ 15 | res.locals.no_auth = config.no_auth; 16 | res.locals.user = req.user; 17 | res.locals.baseUrl = config.baseUrl; 18 | res.locals.ga_analytics_ID = config.ga_analytics_ID; 19 | next(); 20 | }); 21 | 22 | router.get('/services', serveIndex); 23 | router.get('/services/:id/view', serveIndex); 24 | router.get('/services/:id/edit', serveIndex); 25 | router.get('/services/add', serveIndex); 26 | router.get('/', serveIndex); 27 | 28 | return router; 29 | }; 30 | -------------------------------------------------------------------------------- /webserver/views/partials/services-sidebar.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 9 | 14 | 15 | 16 |
6 | {{serviceReport.status.last24Hours.uptime}}% 7 | {{serviceReport.status.last24Hours.uptime}}% 8 | 10 | 11 | {{serviceReport.service.name}} 12 | 13 |
17 |
-------------------------------------------------------------------------------- /test/fixtures/dummy-services.js: -------------------------------------------------------------------------------- 1 | var faker = require('faker'); 2 | 3 | exports = module.exports = (function(){ 4 | 5 | function getRandomName(long) { 6 | if (long) { 7 | return faker.internet.domainName() + ' ' + faker.finance.accountName(); // long enough for layout testing purposes 8 | } 9 | else { 10 | return faker.internet.domainName(); 11 | } 12 | } 13 | 14 | function generateDummyService (i){ 15 | return { 16 | name: getRandomName(false), 17 | interval: 60 * 1000, 18 | failureInterval: 20 * 1000, 19 | url: 'http://apple.com', 20 | port: 443, 21 | timeout: 10000, 22 | warningThreshold: 3000, 23 | pingServiceName: 'http-head' 24 | }; 25 | } 26 | 27 | return { 28 | generate : function(number){ 29 | var services = []; 30 | for (var i = 0; i < number; i++) { 31 | services.push(generateDummyService(i)); 32 | } 33 | return services; 34 | } 35 | }; 36 | 37 | })(); -------------------------------------------------------------------------------- /config/storage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | production: { 4 | provider : 'redis', 5 | options : { 6 | 'redis' : { 7 | port: process.env.WATCHMEN_REDIS_PORT_PRODUCTION || 1216, 8 | host: process.env.WATCHMEN_REDIS_ADDR_PRODUCTION || '127.0.0.1', 9 | db: process.env.WATCHMEN_REDIS_DB_PRODUCTION || 1 10 | } 11 | } 12 | }, 13 | 14 | development: { 15 | provider : 'redis', 16 | options : { 17 | 'redis' : { 18 | port: process.env.WATCHMEN_REDIS_PORT_DEVELOPMENT || 1216, 19 | host: process.env.WATCHMEN_REDIS_ADDR_DEVELOPMENT || '127.0.0.1', 20 | db: process.env.WATCHMEN_REDIS_DB_DEVELOPMENT || 2 21 | } 22 | } 23 | }, 24 | 25 | test: { 26 | provider : 'redis', 27 | options : { 28 | 'redis' : { 29 | port: process.env.WATCHMEN_REDIS_PORT_TEST || 6666, 30 | host: process.env.WATCHMEN_REDIS_ADDR_TEST || '127.0.0.1', 31 | db: process.env.WATCHMEN_REDIS_DB_TEST || 1 32 | } 33 | } 34 | } 35 | 36 | }; -------------------------------------------------------------------------------- /scripts/data-load/lib/response-randomizer.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = (function(){ 2 | 3 | var DEFAULT_TARGET_UPTIME = 0.99; 4 | var DEFAULT_TARGET_WARNING_PERCENTAGE = 0.02; 5 | 6 | function getRandomResponse(service, targetUptime, frecuencyWarning) { 7 | targetUptime = targetUptime || DEFAULT_TARGET_UPTIME; 8 | frecuencyWarning = frecuencyWarning || DEFAULT_TARGET_WARNING_PERCENTAGE; 9 | 10 | var err = Math.random() >= targetUptime ? 'Error connecting with server - message' : null; 11 | var response = { statusCode: err ? 500 : 200 }; 12 | var latency; 13 | if (Math.random() <= frecuencyWarning) { // warning 14 | latency = Math.round((service.warningThreshold) + (Math.random() * service.warningThreshold)); 15 | } else{ 16 | latency = Math.round(service.warningThreshold - service.warningThreshold * Math.random()); 17 | } 18 | return { error: err, body: 'body', response: response, latency: latency }; 19 | } 20 | 21 | return { 22 | getRandomResponse : getRandomResponse 23 | }; 24 | })(); -------------------------------------------------------------------------------- /test/lib/util/populator.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var serviceValidator = require('../../../lib/service-validator'); 3 | 4 | exports = module.exports = (function(){ 5 | 6 | return { 7 | 8 | /** 9 | * Utility to populate a list of services using the provided storage 10 | * @param services 11 | * @param storage 12 | * @param callback 13 | */ 14 | 15 | populate: function(services, storage, callback){ 16 | 17 | if (!services || !services.length) { 18 | return callback('services not provided'); 19 | } 20 | 21 | function addService(service, cb) { 22 | var errors = serviceValidator.validate(service); 23 | if (errors.length === 0){ 24 | storage.addService(service, cb); 25 | } else { 26 | cb(errors); 27 | } 28 | } 29 | 30 | storage.flush_database(function() { 31 | async.eachSeries(services, addService, function (err) { 32 | if (err) { 33 | return callback(err); 34 | } 35 | callback(); 36 | }); 37 | }); 38 | } 39 | }; 40 | 41 | })(); -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Iván Loire 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /webserver/public/less/charting.less: -------------------------------------------------------------------------------- 1 | .details-page { 2 | 3 | .chart { 4 | margin-left: -10px; 5 | 6 | .c3-line-Latency { 7 | stroke-width: 1px; 8 | } 9 | 10 | .c3 .c3-axis-x path, .c3 .c3-axis-x line { 11 | stroke: #c2c2c2; 12 | } 13 | 14 | .c3 .c3-axis-y path, .c3 .c3-axis-y line { 15 | stroke: #c2c2c2; 16 | } 17 | 18 | .c3-region.region-outage { 19 | fill: #d9534f; 20 | } 21 | 22 | .c3-region.region-latency-warning { 23 | fill: #c2c2c2; 24 | } 25 | 26 | .c3-ygrid-line.threshold line { 27 | stroke: #c2c2c2; 28 | stroke-width: 1px; 29 | } 30 | 31 | .c3-ygrid-line.threshold text { 32 | fill: gray; 33 | font-size: 1.2em; 34 | } 35 | 36 | span { 37 | .caption { 38 | color: #c2c2c2; 39 | font-size: 0.9em; 40 | } 41 | margin-right: 10px; 42 | font-size: 1.1em; 43 | } 44 | 45 | /*-- Tooltip overrides --*/ 46 | .c3-tooltip tr { 47 | border: 1px solid #c2c2c2; 48 | border-radius: 4px; 49 | } 50 | 51 | .c3-tooltip th { 52 | background-color: #333; 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /webserver/public/js/directives/ping-service-options.js: -------------------------------------------------------------------------------- 1 | var watchmenDirectives = angular.module('watchmenDirectives'); 2 | 3 | watchmenDirectives.directive('pingServiceOptions', function (PingPlugins) { 4 | return { 5 | restrict: 'EA', //E = element, A = attribute, C = class, M = comment 6 | templateUrl: 'ping-service-options.html', 7 | scope: false, 8 | link: function (scope) { 9 | 10 | function updateOptions() { 11 | for (var i = 0; i < scope.pingServices.length; i++) { 12 | if (scope.pingServices[i].name === scope.service.pingServiceName) { 13 | scope.selectedPingServiceOptions = $.extend({}, scope.pingServices[i].options, 14 | (scope.service.pingServiceOptions || {})[scope.service.pingServiceName]); 15 | } 16 | } 17 | } 18 | 19 | scope.pingServices = PingPlugins.query(function(){ 20 | scope.$watch('service.pingServiceName', function() { 21 | updateOptions(); 22 | }); 23 | }); 24 | 25 | scope.hasPingServiceOptions = function(){ 26 | return !angular.equals({}, scope.selectedPingServiceOptions); 27 | }; 28 | } 29 | }; 30 | }); 31 | -------------------------------------------------------------------------------- /webserver/public/less/service-details.less: -------------------------------------------------------------------------------- 1 | .details-page { 2 | 3 | .timestamp { 4 | font-size: 0.8em; 5 | line-height: 1.8em; 6 | 7 | .timeago { 8 | color: #c2c2c2; 9 | } 10 | } 11 | 12 | .glyphicon-ok-circle { 13 | color: green; 14 | font-size: 0.8em; 15 | } 16 | 17 | .glyphicon-exclamation-sign { 18 | color: #d9534f; 19 | font-size: 0.8em; 20 | } 21 | 22 | .chart-wrapper { 23 | margin-bottom: 1.5em; 24 | } 25 | 26 | .current-outage { 27 | font-size: 1.2em; 28 | border: 1px solid #eee; 29 | border-radius: 3px; 30 | border-left-color: #ce4844; 31 | border-left-width: 5px; 32 | padding: 20px; 33 | margin: 10px 0; 34 | } 35 | 36 | .chart-wrapper > span { 37 | font-size: 1.2em; 38 | color:gray; 39 | display: inline-block; 40 | width: 32%; 41 | text-align: center; 42 | padding:0.2em; 43 | 44 | &.uptime { 45 | color:green; 46 | } 47 | 48 | &.downtime { 49 | color:#d9534f; 50 | } 51 | } 52 | 53 | .edit-link { 54 | font-size:0.5em; 55 | } 56 | 57 | .outages-list { 58 | .outage { 59 | padding:0.3em; 60 | border-top: solid #d3d3d3 1px; 61 | 62 | } 63 | .odd { 64 | background-color: #f9f9f9; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /run-web-server.js: -------------------------------------------------------------------------------- 1 | var program = require('commander'); 2 | var storageFactory = require ('./lib/storage/storage-factory'); 3 | var config = require('./config/web'); 4 | var expressApp = require('./webserver/app'); 5 | 6 | var RETURN_CODES = { 7 | OK: 0, 8 | BAD_STORAGE: 1, 9 | BAD_PORT: 2 10 | }; 11 | 12 | program 13 | .option('-p, --port [port]', 'Port to bind web process to', config.port || 3000) 14 | .option('-e, --env [env]', 'Storage environment key', process.env.NODE_ENV || 'development') 15 | .parse(process.argv); 16 | 17 | var storage = storageFactory.getStorageInstance(program.env); 18 | if (!storage) { 19 | console.error('Error creating storage for env: ', program.env); 20 | return process.exit(RETURN_CODES.BAD_STORAGE); 21 | } 22 | 23 | var app = expressApp(storage); 24 | var server = app.listen(program.port, function () { 25 | if (server.address()) { 26 | console.log("watchmen server listening on port %d in %s mode", program.port, program.env); 27 | } else { 28 | console.log('something went wrong... couldn\'t listen to port %d', program.port); 29 | process.exit(RETURN_CODES.BAD_PORT); 30 | } 31 | process.on('SIGINT', function () { 32 | console.log('stopping web server.. bye!'); 33 | server.close(); 34 | process.exit(RETURN_CODES.OK); 35 | }); 36 | }); -------------------------------------------------------------------------------- /test/test-aggregator.js: -------------------------------------------------------------------------------- 1 | var aggregator = require('../lib/aggregator'); 2 | var assert = require('assert'); 3 | 4 | describe('aggregator', function () { 5 | 6 | var SECOND = 1000; 7 | var MINUTE = 60 * SECOND; 8 | var HOUR = 60 * MINUTE; 9 | 10 | var initialTime = +new Date(2015, 1, 1); // start with 0 hours, 0 minutes, 0 seconds 11 | 12 | it('should aggregate by minute', function (done) { 13 | var arr = [ 14 | {t: initialTime, l: 200}, 15 | {t: initialTime + 20 * SECOND, l: 100}, 16 | 17 | {t: initialTime + MINUTE + 20 * SECOND, l: 400}, 18 | {t: initialTime + MINUTE + 40 * SECOND, l: 500} 19 | ]; 20 | aggregator.aggregate(arr, 'minute', function (data) { 21 | assert.equal(data.length, 2); 22 | assert.equal(data[0].l, 150); 23 | assert.equal(data[1].l, 450); 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should aggregate by hour', function (done) { 29 | var arr = [ 30 | {t: initialTime, l: 200}, 31 | {t: initialTime + 20 * MINUTE, l: 100}, 32 | 33 | {t: initialTime + HOUR + 20 * MINUTE, l: 700}, 34 | {t: initialTime + HOUR + 20 * MINUTE, l: 800} 35 | ]; 36 | aggregator.aggregate(arr, 'hour', function (data) { 37 | assert.equal(data.length, 2); 38 | assert.equal(data[0].l, 150); 39 | assert.equal(data[1].l, 750); 40 | done(); 41 | }); 42 | 43 | }); 44 | 45 | }); -------------------------------------------------------------------------------- /scripts/data-load/populate-services-from-fixtures.js: -------------------------------------------------------------------------------- 1 | var storageFactory = require('../../lib/storage/storage-factory'); 2 | var populator = require('../../test/lib/util/populator'); 3 | var dummyServiceGenerator = require('../../test/fixtures/dummy-services'); 4 | var services; 5 | 6 | function run(program){ 7 | var env = program.env || 'development'; 8 | var storage = storageFactory.getStorageInstance(env); 9 | 10 | if (!storage) { 11 | console.error('Not available storage for the provided environment ' + env); 12 | return; 13 | } 14 | 15 | if (program.real) { 16 | console.log('Populating real services...'); 17 | services = require('../../test/fixtures/real-services'); 18 | } else { 19 | console.log('Populating real services...'); 20 | services = dummyServiceGenerator.generate(program.numberServices || 20); 21 | } 22 | 23 | populator.populate(services, storage, function(err){ 24 | if (err) { 25 | console.error(err); 26 | } else { 27 | console.log('done! ' + services.length + ' services populated'); 28 | } 29 | storage.quit(); 30 | }); 31 | } 32 | 33 | 34 | var program = require('commander'); 35 | program 36 | .option('-r, --real', 'User real data') 37 | .option('-e, --env [env]', 'Storage environment key') 38 | .option('-s, --number-services [numberServices]', 'Number of services') 39 | .parse(process.argv); 40 | 41 | run(program); 42 | -------------------------------------------------------------------------------- /scripts/data-load/find-memory-leaks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script helps finding memory leaks in watchmen while during pings 3 | * It uses a mocked storage. 4 | */ 5 | var storageMocked = require ('./../../test/lib/mock/storage-mocked'); 6 | var debug = require('debug')('data-load'); 7 | 8 | var Watchmen = require('../../lib/watchmen.js'); 9 | var mockedPingService = require('../../test/lib/mock/request-mocked'); 10 | 11 | var watchmen = new Watchmen(null, new storageMocked(null)); 12 | 13 | var service = { 14 | host : { host: 'www.correcthost.com', port:'80', name : 'test'}, 15 | url : '/', 16 | interval: 4000, 17 | failedInterval: 5000, 18 | warningThreshold: 1500, //miliseconds 19 | method : 'get', 20 | expected : { statuscode: 200, contains: '' }, 21 | 22 | ping_service: mockedPingService // mock // TODO 23 | }; 24 | 25 | var numberTimes = 100000; 26 | var current = 0; 27 | 28 | function run (service, cb){ 29 | watchmen.ping({service:service, timestamp: +new Date()}, function(err, status){ 30 | if (current < numberTimes){ 31 | if (current % 1000 === 0) { 32 | debug('processing ', current); 33 | } 34 | current++; 35 | setTimeout(function(){ // get out of the stack 36 | run (service, cb); 37 | },0); 38 | } 39 | else { 40 | cb(null, 'done!'); 41 | } 42 | }); 43 | } 44 | 45 | run (service, function(err, msg){ 46 | console.log(msg); 47 | }); -------------------------------------------------------------------------------- /webserver/public/js/app.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | 'use strict'; 4 | 5 | /* App Module */ 6 | 7 | var watchmenApp = angular.module('watchmenApp', [ 8 | 'ui.router', 9 | 'angularSpinner', 10 | 'ngTable', // table sorting and pagination 11 | 'angularMoment', 12 | 'angularMSTime', 13 | 'watchmenControllers', 14 | 'watchmenDirectives', 15 | 'watchmenFactories', 16 | 'ngResource' 17 | ]); 18 | 19 | watchmenApp.config(function($stateProvider, $locationProvider, $urlRouterProvider) { 20 | 21 | $locationProvider.html5Mode(true); 22 | 23 | $stateProvider.state('services', { 24 | url: '/services', 25 | templateUrl: 'service-list.html', 26 | controller: 'ServiceListCtrl' 27 | }).state('viewService', { 28 | url: '/services/:id/view', 29 | templateUrl: 'service-detail.html', 30 | controller: 'ServiceDetailCtrl' 31 | }).state('newService', { 32 | url: '/services/add', 33 | templateUrl: 'service-edit.html', 34 | controller: 'ServiceAddCtrl' 35 | }).state('editService', { 36 | url: '/services/:id/edit', 37 | templateUrl: 'service-edit.html', 38 | controller: 'ServiceEditCtrl' 39 | }); 40 | 41 | $urlRouterProvider.when('/', '/services'); 42 | }); 43 | 44 | })(); 45 | -------------------------------------------------------------------------------- /webserver/routes/api-ping-plugins-route.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | 4 | exports = module.exports.getRoutes = function (){ 5 | 6 | var PREFIX_PING_PLUGIN = 'watchmen-ping-'; 7 | 8 | var router = express.Router(); 9 | var pkgJson = require(path.resolve(__dirname, '../../package.json')); 10 | 11 | function getOptions(dep){ 12 | try { 13 | var pluginFactory = require(dep); 14 | var plugin = new pluginFactory(); 15 | return plugin.getDefaultOptions(); 16 | } catch (e) { 17 | console.error('Error parsing options for ' + dep); 18 | console.error(e); 19 | return {}; 20 | } 21 | } 22 | 23 | /** 24 | * Returns the list of available plugin ping plugins (ex: watchmen-ping-http-head) 25 | * Those plugins should be installed in node_modules with the prefix "watchmen-ping" 26 | */ 27 | router.get('/', function(req, res){ 28 | 29 | var pingServicesPluginsInPackageJson = []; 30 | for (var dep in pkgJson.dependencies) { 31 | if (dep.indexOf(PREFIX_PING_PLUGIN) === 0){ 32 | pingServicesPluginsInPackageJson.push(dep); 33 | } 34 | } 35 | 36 | var plugins = pingServicesPluginsInPackageJson.map(function(pluginName){ 37 | return { 38 | name: pluginName.replace(PREFIX_PING_PLUGIN, ''), 39 | options: getOptions(pluginName) 40 | }; 41 | }); 42 | 43 | return res.json(plugins); 44 | }); 45 | 46 | return router; 47 | 48 | }; -------------------------------------------------------------------------------- /webserver/public/less/service-list.less: -------------------------------------------------------------------------------- 1 | 2 | .error, .failure, .critical {color:#d9534f;} 3 | .success {color:green;} 4 | .warning, .warnings{color:orange;} 5 | 6 | .table-services { 7 | 8 | tr.result-success .result-uptime a { 9 | color:green; 10 | } 11 | 12 | tr.result-error .result-uptime a { 13 | color: #d9534f; 14 | } 15 | 16 | tr.result-warning .result-uptime a { 17 | color: orange; 18 | } 19 | 20 | .result-uptime { 21 | font-size: 1.3em; 22 | } 23 | 24 | th { 25 | text-align: left; 26 | } 27 | 28 | td { 29 | font-size:120%; 30 | } 31 | 32 | td.outages { 33 | color:#d9534f; 34 | } 35 | 36 | td.warnings { 37 | color:orange; 38 | } 39 | 40 | td.service { 41 | text-transform: uppercase; 42 | margin-top: 20px; 43 | opacity: 0.6; 44 | font-size:100%; 45 | color: gray; 46 | } 47 | 48 | td.status { 49 | width:80px; 50 | } 51 | 52 | td.admin-operations { 53 | width: 70px; 54 | 55 | .dropdown-menu{ 56 | padding:10px; 57 | width:250px; 58 | text-align: center; 59 | 60 | .btn { 61 | font-size: 11pt; 62 | margin:0 5px; 63 | } 64 | } 65 | } 66 | } 67 | 68 | .filter-container { 69 | margin: 10px 0; 70 | 71 | input.query { 72 | padding-left:30px; 73 | display:block; 74 | } 75 | 76 | span.icon { 77 | position: absolute; 78 | left: 10px; 79 | padding: 10px 12px; 80 | pointer-events: none; 81 | } 82 | 83 | div.filter-to-me { 84 | padding-top: 5px; 85 | } 86 | } -------------------------------------------------------------------------------- /test/lib/mock/storage-mocked.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convenient Redis Storage mock for testing purposes 3 | */ 4 | 5 | var util = require ('util'); 6 | 7 | function StorageMocked(data){ 8 | data = data || {}; 9 | this.currentOutage = data.currentOutage; 10 | this.failureCount = data.failureCount || 0; 11 | } 12 | 13 | exports = module.exports = StorageMocked; 14 | 15 | StorageMocked.prototype.startOutage = function (service, outageData, callback) { 16 | this.currentOutage = outageData; 17 | process.nextTick(callback); 18 | }; 19 | 20 | StorageMocked.prototype.getCurrentOutage = function (service, callback) { 21 | var self = this; 22 | process.nextTick(function(){ 23 | callback(null, self.currentOutage); 24 | }); 25 | }; 26 | 27 | StorageMocked.prototype.saveLatency = function (service, timestamp, latency, cb) { 28 | process.nextTick(cb); 29 | }; 30 | 31 | StorageMocked.prototype.archiveCurrentOutageIfExists = function (service, callback) { 32 | var self = this; 33 | process.nextTick(function(){ 34 | callback(null, self.currentOutage); 35 | }); 36 | }; 37 | 38 | StorageMocked.prototype.increaseOutageFailureCount = function (service, callback){ 39 | var self = this; 40 | process.nextTick(function () { 41 | callback(null, ++self.failureCount); 42 | }); 43 | }; 44 | 45 | StorageMocked.prototype.resetOutageFailureCount = function (service, callback){ 46 | this.failureCount = 0; 47 | process.nextTick(function () { 48 | callback(); 49 | }); 50 | }; 51 | 52 | StorageMocked.prototype.flush_database = function (callback){ 53 | process.nextTick(callback); 54 | }; 55 | 56 | -------------------------------------------------------------------------------- /webserver/public/js/factories.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | angular.module('watchmenFactories', []); 6 | 7 | var CACHE_EXPIRATION = 30000; // ms 8 | 9 | var factories = angular.module('watchmenFactories'); 10 | var reportCache; 11 | var pingPluginsCache; 12 | 13 | factories.factory('Report', function ($resource, $cacheFactory) { 14 | reportCache = $cacheFactory('Services'); 15 | 16 | setInterval(function(){ 17 | reportCache.removeAll(); 18 | }, CACHE_EXPIRATION); 19 | 20 | var Report = $resource('/api/report/services/:id', {id: '@id'}, { 21 | 'get': { method:'GET', cache: reportCache}, 22 | 'query': { method:'GET', isArray:true, cache: reportCache} 23 | }); 24 | 25 | Report.clearCache = function () { 26 | if (reportCache) { 27 | reportCache.removeAll(); 28 | } 29 | }; 30 | 31 | return Report; 32 | }); 33 | 34 | factories.factory('Service', function ($resource) { 35 | return $resource('/api/services/:id', 36 | {id: '@id'}, { 37 | 38 | /** 39 | * Rest service data 40 | */ 41 | reset: { 42 | method: 'POST', 43 | url: '/api/services/:id/reset' 44 | } 45 | 46 | }); 47 | }); 48 | 49 | factories.factory('PingPlugins', function ($resource, $cacheFactory) { 50 | pingPluginsCache = $cacheFactory('PingPlugins'); 51 | return $resource('/api/plugins/:id', 52 | {id: '@id'}, { 53 | 'query': { method:'GET', isArray:true, cache: pingPluginsCache} 54 | }); 55 | }); 56 | })(); -------------------------------------------------------------------------------- /webserver/public/js/controllers/service-add.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | 6 | var watchmenControllers = angular.module('watchmenControllers'); 7 | 8 | /** 9 | * Add service 10 | */ 11 | 12 | watchmenControllers.controller('ServiceAddCtrl', 13 | function ( 14 | $scope, 15 | $state, 16 | $filter, 17 | $stateParams, 18 | Service, 19 | Report 20 | ) { 21 | $scope.service = new Service(); 22 | 23 | $scope.editServiceTitle = "New service"; 24 | // defaults 25 | $scope.service.timeout = 10000; 26 | $scope.service.warningThreshold = 5000; 27 | $scope.service.interval = 60000; 28 | $scope.service.failureInterval = 30000; 29 | $scope.service.failuresToBeOutage = 1; 30 | $scope.service.port = 80; 31 | $scope.service.pingServiceName = 'http-head'; 32 | 33 | $scope.save = function () { 34 | 35 | $scope.service.pingServiceOptions = {}; 36 | $scope.service.pingServiceOptions[$scope.service.pingServiceName] = $scope.selectedPingServiceOptions; 37 | 38 | $scope.service.$save(function () { 39 | Report.clearCache(); 40 | $state.go('services'); 41 | }, function(response){ 42 | console.error(response); 43 | if (response && response.data && response.data.errors) { 44 | $scope.serviceAddErrors = response.data.errors; 45 | } 46 | }); 47 | }; 48 | 49 | $scope.cancel = function () { 50 | $state.go('services'); 51 | }; 52 | 53 | }); 54 | 55 | })(); 56 | -------------------------------------------------------------------------------- /test/test-api-ping-plugins-route.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest'); 2 | var assert = require('assert'); 3 | var storageFactory = require('../lib/storage/storage-factory'); 4 | 5 | var storage = storageFactory.getStorageInstance('test'); 6 | var app = require('../webserver/app')(storage); 7 | 8 | describe('ping plugins route', function () { 9 | 10 | var server; 11 | var PORT = 3355; 12 | 13 | var API_ROOT = '/api/plugins'; 14 | 15 | var agent = request.agent(app); 16 | 17 | before(function (done) { 18 | 19 | server = app.listen(PORT, function () { 20 | if (server.address()) { 21 | console.log('starting server in port ' + PORT); 22 | done(); 23 | } else { 24 | console.log('something went wrong... couldn\'t listen to that port.'); 25 | process.exit(1); 26 | } 27 | }); 28 | }); 29 | 30 | after(function () { 31 | server.close(); 32 | }); 33 | 34 | describe('plugin list', function () { 35 | 36 | describe('with an anonymous user ', function () { 37 | 38 | it('should return list of plugins', function (done) { 39 | agent 40 | .get(API_ROOT + '/') 41 | .set('Accept', 'application/json') 42 | .expect('Content-Type', /json/) 43 | .expect(200) 44 | .send() 45 | .end(function (err, res) { 46 | assert.equal(res.body.length, 2); 47 | var plugins = res.body.sort(function(a, b){ return a.name > b.name; }); 48 | assert.equal(plugins[0].name, 'http-contains'); 49 | assert.equal(plugins[1].name, 'http-head'); 50 | done(err); 51 | }); 52 | }); 53 | 54 | }); 55 | 56 | }); 57 | 58 | }); -------------------------------------------------------------------------------- /webserver/public/js/ngTable-utils.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | var SAVE_PARAMS_IN_LOCALSTORAGE = true; 6 | 7 | angular.module('watchmenFactories').factory('ngTableUtils', function (ngTableParams) { 8 | 9 | /** 10 | * Returns local stored or default parameters for ngTable. 11 | * @param key 12 | * @param pageSize 13 | * @returns {Object} parameters 14 | */ 15 | 16 | function getDefaultParameters(key) { 17 | var defaults = {"sorting": {"status.last24Hours.uptime": "asc"}}; 18 | 19 | if (SAVE_PARAMS_IN_LOCALSTORAGE && window.localStorage) { 20 | if (window.localStorage.getItem(key)) { 21 | return JSON.parse(window.localStorage.getItem(key)); 22 | } else { 23 | return defaults; 24 | } 25 | } 26 | else { 27 | return defaults; 28 | } 29 | } 30 | 31 | function createngTableParams(key, $scope, $filter) { 32 | return new ngTableParams(getDefaultParameters(key), 33 | { 34 | total: $scope[key].length, // length of data 35 | counts: [], 36 | getData: function ($defer, params) { 37 | var data = $scope[key]; 38 | var orderedData = params.sorting() ? $filter('orderBy')(data, params.orderBy()) : data; 39 | $defer.resolve(orderedData); 40 | 41 | if (window.localStorage) { 42 | window.localStorage.setItem(key, JSON.stringify({ 43 | sorting: params.sorting() 44 | })); 45 | } 46 | } 47 | }); 48 | } 49 | 50 | return { 51 | createngTableParams: createngTableParams 52 | }; 53 | 54 | }); 55 | 56 | })(); -------------------------------------------------------------------------------- /run-monitor-server.js: -------------------------------------------------------------------------------- 1 | var colors = require('colors'); 2 | var program = require('commander'); 3 | var pluginLoader = require('./lib/plugin-loader'); 4 | var storageFactory = require('./lib/storage/storage-factory'); 5 | var WatchMenFactory = require('./lib/watchmen'); 6 | var sentinelFactory = require('./lib/sentinel'); 7 | 8 | var RETURN_CODES = { 9 | OK: 0, 10 | BAD_STORAGE: 1, 11 | GENERIC_ERROR: 2 12 | }; 13 | 14 | function exit(code) { 15 | storage.quit(); 16 | process.exit(code); 17 | } 18 | 19 | program 20 | .option('-e, --env [env]', 'Storage environment key', process.env.NODE_ENV || 'development') 21 | .option('-d, --max-initial-delay [value]', 'Initial random delay max bound', 20000) 22 | .parse(process.argv); 23 | 24 | var storage = storageFactory.getStorageInstance(program.env); 25 | if (!storage) { 26 | console.error('Error creating storage for env: ', program.env); 27 | return process.exit(RETURN_CODES.BAD_STORAGE); 28 | } 29 | 30 | storage.getServices({}, function (err, services) { 31 | if (err) { 32 | console.error('error loading services'.red); 33 | console.error(err); 34 | return exit(RETURN_CODES.GENERIC_ERROR); 35 | } 36 | 37 | var watchmen = new WatchMenFactory(services, storage); 38 | 39 | pluginLoader.loadPlugins(watchmen, {}, function(){ 40 | watchmen.startAll({randomDelayOnInit: program.maxInitialDelay}); 41 | console.log('\nwatchmen has started. ' + services.length + ' services loaded\n'); 42 | 43 | var sentinel = new sentinelFactory(storage, watchmen, {interval: 10000}); 44 | sentinel.watch(); 45 | }); 46 | 47 | }); 48 | 49 | process.on('SIGINT', function () { 50 | console.log('stopping watchmen..'.gray); 51 | exit(RETURN_CODES.OK); 52 | }); 53 | -------------------------------------------------------------------------------- /webserver/public/js/controllers/service-edit.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | var watchmenControllers = angular.module('watchmenControllers'); 6 | 7 | /** 8 | * Add service 9 | */ 10 | 11 | watchmenControllers.controller('ServiceEditCtrl', 12 | 13 | function ( 14 | $scope, 15 | $state, 16 | $filter, 17 | $stateParams, 18 | Service, 19 | Report, 20 | usSpinnerService 21 | ) { 22 | 23 | function loading(){ 24 | usSpinnerService.spin('spinner-1'); 25 | $scope.loading = true; 26 | } 27 | 28 | function loaded(){ 29 | usSpinnerService.stop('spinner-1'); 30 | $scope.loading = false; 31 | } 32 | 33 | loading(); 34 | 35 | $scope.editServiceTitle = "Update service"; 36 | 37 | $scope.service = Service.get({id: $stateParams.id}, function(){ 38 | loaded(); 39 | }, function(err){ 40 | console.error(err); 41 | if (err.status === 401) { 42 | $state.go('services'); 43 | } 44 | loaded(); 45 | }); 46 | 47 | $scope.save = function () { 48 | 49 | $scope.service.pingServiceOptions = {}; 50 | $scope.service.pingServiceOptions[$scope.service.pingServiceName] = $scope.selectedPingServiceOptions; 51 | 52 | $scope.service.$save(function () { 53 | Report.clearCache(); 54 | $state.go('services'); 55 | }, function(response){ 56 | console.error(response); 57 | if (response && response.data && response.data.errors) { 58 | $scope.serviceAddErrors = response.data.errors; 59 | } 60 | }); 61 | }; 62 | 63 | $scope.cancel = function () { 64 | $state.go('services'); 65 | }; 66 | 67 | }); 68 | 69 | })(); 70 | -------------------------------------------------------------------------------- /scripts/report-service.js: -------------------------------------------------------------------------------- 1 | var storageFactory = require('../lib/storage/storage-factory'); 2 | var reporterFactory = require('../lib/reporter'); 3 | var program = require('commander'); 4 | 5 | function printServiceReport(serviceReport){ 6 | var service = serviceReport.service; 7 | console.log('\n'); 8 | console.log(service.id, service.name, service.url, service.interval); 9 | console.log('Outage:', serviceReport.status.currentOutage); 10 | var latestLatency = serviceReport.status.last24Hours.latency.list; 11 | latestLatency.sort(function(a, b){ 12 | return b.t - a.t; 13 | }); 14 | if (latestLatency.length) { 15 | console.log('Latest latency record: ', new Date(latestLatency[0].t)); 16 | } 17 | else { 18 | console.log('No latency records found'); 19 | } 20 | console.log('Latest outages:', serviceReport.status.latestOutages); 21 | console.log('\n'); 22 | } 23 | 24 | function run(program, cb){ 25 | if (!program.serviceId) { 26 | return cb('service ID is required'); 27 | } 28 | var env = program.env || 'development'; 29 | var storage = storageFactory.getStorageInstance(env); 30 | if (!storage){ 31 | return cb('Invalid storage'); 32 | } 33 | 34 | var reporter = new reporterFactory(storage); 35 | reporter.getService(program.serviceId, function(err, service){ 36 | if (!service) { 37 | return cb('service not found with id ' + program.serviceId); 38 | } 39 | printServiceReport(service); 40 | storage.quit(); 41 | cb(); 42 | }); 43 | } 44 | 45 | program 46 | .option('-e, --env [env]', 'Storage environment key') 47 | .option('-s, --service-id ', 'Storage environment key') 48 | .parse(process.argv); 49 | 50 | run(program, function (err) { 51 | if (err) { 52 | console.error(err); 53 | } 54 | else { 55 | console.log('done!'); 56 | } 57 | process.exit(0); 58 | }); -------------------------------------------------------------------------------- /webserver/views/header.html: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /webserver/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var moment = require ('moment'); 4 | var bodyParser = require('body-parser'); 5 | var methodOverride = require('method-override'); 6 | var errorHandler = require('errorhandler'); 7 | var session = require('express-session'); 8 | var compress = require('compression'); 9 | var api = require('./routes/api-service-route'); 10 | var report = require('./routes/api-report-route'); 11 | var web = require('./routes/web-route'); 12 | var plugins = require('./routes/api-ping-plugins-route'); 13 | var auth = require('./routes/web-auth-route'); 14 | 15 | exports = module.exports = function(storage){ 16 | if (!storage) { 17 | throw new Error('storage is required'); 18 | } 19 | 20 | app.set('views', __dirname + '/views'); 21 | app.set('view engine', 'ejs'); 22 | app.engine('.html', require('ejs').renderFile); 23 | 24 | app.use(compress()); 25 | app.use(session({ 26 | store: storage.getSessionStore(session), 27 | secret: 'myBigSecret', 28 | saveUninitialized: true, 29 | resave: true 30 | })); 31 | app.use(bodyParser.json()); 32 | app.use(bodyParser.urlencoded({ extended: false })); 33 | app.use(methodOverride()); 34 | 35 | auth.configureApp(app); 36 | 37 | app.all('/*', function(req, res, next) { 38 | res.header("Access-Control-Allow-Origin", "*"); 39 | res.header("Access-Control-Allow-Headers", "X-Requested-With"); 40 | next(); 41 | }); 42 | 43 | app.use('/api/plugins', plugins.getRoutes()); 44 | app.use('/api/report', report.getRoutes(storage)); 45 | app.use('/api', api.getRoutes(storage)); 46 | app.use('/', web.getRoutes(storage)); 47 | 48 | app.use(express.static(__dirname + '/public')); 49 | 50 | if (process.env.NODE_ENV === 'development') { 51 | console.log('development mode'); 52 | app.use(errorHandler()); 53 | } 54 | 55 | return app; 56 | 57 | }; -------------------------------------------------------------------------------- /webserver/routes/api-report-route.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | var express = require('express'); 3 | var reporterFactory = require('./../../lib/reporter'); 4 | var accessFilter = require('./../../lib/service-access-filter'); 5 | 6 | module.exports.getRoutes = function (storage){ 7 | 8 | if (!storage) { 9 | throw new Error('storage needed'); 10 | } 11 | 12 | var reporter = new reporterFactory(storage); 13 | var router = express.Router(); 14 | 15 | function handlePrivateFields(service) { 16 | delete service.alertTo; 17 | 18 | service.isRestricted = !!service.restrictedTo; 19 | 20 | delete service.restrictedTo; 21 | return service; 22 | } 23 | 24 | /** 25 | * Load service report 26 | */ 27 | 28 | router.get('/services/:id', function(req, res){ 29 | if (!req.params.id) { 30 | return res.status(400).json({ error: 'ID parameter not found' }); 31 | } 32 | reporter.getService(req.params.id, function (err, serviceReport){ 33 | if (err) { 34 | console.error(err); 35 | return res.status(500).json({ error: err }); 36 | } 37 | 38 | serviceReport = accessFilter.filterReports(serviceReport, req.user); 39 | 40 | if (!serviceReport) { 41 | return res.status(404).json({ error: 'Service not found' }); 42 | } 43 | 44 | serviceReport.service = handlePrivateFields(serviceReport.service); 45 | 46 | res.json(serviceReport); 47 | }); 48 | }); 49 | 50 | /** 51 | * Load services report 52 | */ 53 | 54 | router.get('/services', function(req, res){ 55 | reporter.getServices({}, function (err, serviceReports){ 56 | if (err) { 57 | console.error(err); 58 | return res.status(500).json({ error: err }); 59 | } 60 | serviceReports = accessFilter.filterReports(serviceReports, req.user).map(function(s){ 61 | s.service = handlePrivateFields(s.service); 62 | return s; 63 | }); 64 | res.json(serviceReports); 65 | }); 66 | }); 67 | 68 | return router; 69 | 70 | }; 71 | -------------------------------------------------------------------------------- /webserver/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | watchmen, http monitor for node.js - <%= title %> 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 | 18 | 19 | 22 | 23 | 26 | 27 | 30 | 31 | 34 | 35 | 36 | 37 | 44 | 45 | 46 | 47 | <% if (ga_analytics_ID) { %> 48 | 57 | <% } %> 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /lib/service-access-filter.js: -------------------------------------------------------------------------------- 1 | var config = require ('../config/web'); 2 | 3 | exports = module.exports = (function () { 4 | 5 | function isServiceRestrictedToEmail(service, email) { 6 | if (!service) { 7 | return false; 8 | } 9 | var restrictedTo = (service.restrictedTo || '').split(',') 10 | .map(function(s){ return s.trim(); }) 11 | .filter(function(s){ return s; }); 12 | 13 | if (!email) { 14 | return restrictedTo.length; // no email provided. restricted access if restrictions are enabled 15 | } 16 | else { 17 | return restrictedTo.length > 0 && restrictedTo.indexOf(email) === -1; 18 | } 19 | } 20 | 21 | return { 22 | 23 | /** 24 | * Filter allowed services for a particular user 25 | * @param services 26 | * @param user 27 | * @returns {Array} 28 | */ 29 | 30 | filterServices: function (services, user) { 31 | user = user || {}; 32 | var isArray = Array.isArray(services); 33 | 34 | if (!isArray) { 35 | services = [services]; 36 | } 37 | services = services.filter(function(service){ 38 | return config.no_auth || user.isAdmin || !isServiceRestrictedToEmail(service, user.email); 39 | }); 40 | 41 | return isArray ? services : services[0]; 42 | }, 43 | 44 | /** 45 | * Filter allowed service reports for a particular user 46 | * @param serviceReports 47 | * @param user 48 | * @returns {Array} 49 | */ 50 | 51 | filterReports: function (serviceReports, user) { 52 | user = user || {}; 53 | if (!serviceReports) { 54 | return serviceReports; 55 | } 56 | var isArray = Array.isArray(serviceReports); 57 | 58 | if (!isArray) { 59 | serviceReports = [serviceReports]; 60 | } 61 | 62 | serviceReports = serviceReports.filter(function (serviceReport) { 63 | return config.no_auth || user.isAdmin || !isServiceRestrictedToEmail(serviceReport.service, user.email); 64 | }); 65 | 66 | return isArray ? serviceReports : serviceReports[0]; 67 | }, 68 | 69 | // exposed for testing 70 | _isServiceRestrictedToEmail: isServiceRestrictedToEmail 71 | 72 | }; 73 | 74 | })(); -------------------------------------------------------------------------------- /test/test-sentinel.js: -------------------------------------------------------------------------------- 1 | var sentinelFactory = require('../lib/sentinel'); 2 | var assert = require('assert'); 3 | 4 | describe('sentinel', function () { 5 | 6 | it('should find new services', function () { 7 | var dbServices = [{id: 'Eg34'}, {id: 2}, {id: '33'}]; 8 | var runningServices = [{id: 'Eg34'}, {id: 2}]; 9 | var sentinel = new sentinelFactory([], null); 10 | var addedServices = sentinel._findAdded(dbServices, runningServices); 11 | assert.equal(addedServices.length, 1); 12 | }); 13 | 14 | it('should find removed services', function () { 15 | var dbServices = [{id: 'Eg34'}, {id: 2}]; 16 | var runningServices = [{id: 'Eg34'}, {id: 2}, {id: '98989'}, {id: '33'}]; 17 | var sentinel = new sentinelFactory([], null); 18 | var removedServices = sentinel._findRemoved(dbServices, runningServices); 19 | assert.equal(removedServices.length, 2); 20 | }); 21 | 22 | it('should find modified services when white-listed fields change', function () { 23 | var dbServices = [{id: '1', interval: 2000}, {id: '2', interval: 3000}]; 24 | var runningServices = [{id: '1', interval: 2300}, {id: '2', interval: 3000}]; 25 | var sentinel = new sentinelFactory([], null); 26 | var modifiedServices = sentinel._findModified(dbServices, runningServices); 27 | assert.equal(modifiedServices.length, 1); 28 | }); 29 | 30 | it('should ignore certain fields', function () { 31 | var dbServices = [{id: '1', ignoreField: 'ignored field'}, {id: '2', ignoreField: 'ignored field'}]; 32 | var runningServices = [{id: '1', ignoreField: 'change in ignored field'}, {id: '2', ignoreField: 'other change in ignored field'}]; 33 | var sentinel = new sentinelFactory([], null); 34 | var modifiedServices = sentinel._findModified(dbServices, runningServices); 35 | assert.equal(modifiedServices.length, 0); 36 | }); 37 | 38 | it('should handle json properties', function () { 39 | var dbServices = [{id: '1', pingServiceOptions: {'http-contains': [{name: 'my property'}]}}]; 40 | var runningServices = [{id: '1', pingServiceOptions: {'http-contains': [{name: 'my property'}]}}]; 41 | var sentinel = new sentinelFactory([], null); 42 | var modifiedServices = sentinel._findModified(dbServices, runningServices); 43 | assert.equal(modifiedServices.length, 0); 44 | }); 45 | 46 | }); -------------------------------------------------------------------------------- /test/fixtures/real-services.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | exports = module.exports = (function(){ 4 | 5 | var MIN = 60 * 1000; //ms 6 | 7 | var DEFAULTS = { 8 | interval: 2 * MIN, 9 | failureInterval: MIN, 10 | timeout: 10000, 11 | warningThreshold: 3000, 12 | pingServiceName: 'http-head' 13 | }; 14 | 15 | function generateService (service){ 16 | return _.defaults(service, DEFAULTS); 17 | } 18 | 19 | var services = []; 20 | services.push(generateService({ name: 'apple', url: 'https://apple.com', port: 443 })); 21 | services.push(generateService({ name: 'node', url: 'http://node.com', port: 80 })); 22 | services.push(generateService({ name: 'amazon', url: 'http://amazon.com', port: 80 })); 23 | services.push(generateService({ name: 'npm', url: 'http://npm.org', port: 80 })); 24 | services.push(generateService({ name: 'yahoo', url: 'http://yahoo.com', port: 80 })); 25 | services.push(generateService({ name: 'alexa', url: 'http://alexa.com', port: 80 })); 26 | services.push(generateService({ name: 'github', url: 'http://github.com', port: 80 })); 27 | services.push(generateService({ name: 'bitbucket', url: 'http://bitbucket.com', port: 80 })); 28 | services.push(generateService({ name: 'youtube', url: 'http://youtube.com', port: 80 })); 29 | services.push(generateService({ name: 'facebook', url: 'http://facebook.com', port: 80 })); 30 | services.push(generateService({ name: 'twitter', url: 'http://twitter.com', port: 80 })); 31 | services.push(generateService({ name: 'linkedin', url: 'http://linkedin.com', port: 80 })); 32 | services.push(generateService({ name: 'bing', url: 'http://bing.com', port: 80 })); 33 | services.push(generateService({ name: 'blogspot', url: 'http://blogspot.com', port: 80 })); 34 | services.push(generateService({ name: 'oracle', url: 'http://oracle.com', port: 80 })); 35 | services.push(generateService({ name: 'microsoft', url: 'http://microsoft.com', port: 80 })); 36 | services.push(generateService({ name: 'business', url: 'http://business.com', port: 80 })); 37 | services.push(generateService({ name: 'bbc', url: 'http://bbc.com', port: 80 })); 38 | services.push(generateService({ name: 'google', url: 'http://google.com', port: 80 })); 39 | services.push(generateService({ name: 'google maps', url: 'http://maps.google.com', port: 80 })); 40 | 41 | return services; 42 | 43 | })(); -------------------------------------------------------------------------------- /webserver/routes/web-auth-route.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config/web'); 2 | var passport = require('passport'); 3 | var url = require('url'); 4 | var crypto = require('crypto'); 5 | 6 | var GoogleStrategy = require( 'passport-google-oauth2' ).Strategy; 7 | 8 | module.exports = (function (){ 9 | 10 | function md5(str) { 11 | var hash = crypto.createHash('md5'); 12 | hash.update(str.toLowerCase().trim()); 13 | return hash.digest('hex'); 14 | } 15 | 16 | function isAdmin(email){ 17 | var admins = (config.admins || '').split(',').map(function(email){ return email.trim (); }); 18 | return admins.indexOf(email)>-1; 19 | } 20 | 21 | return { 22 | 23 | /** 24 | * Configure application with authentication mechanisms 25 | * @param {Application} app 26 | */ 27 | configureApp : function (app){ 28 | 29 | passport.use(new GoogleStrategy({ 30 | clientID: config.auth.GOOGLE_CLIENT_ID, 31 | clientSecret: config.auth.GOOGLE_CLIENT_SECRET, 32 | passReqToCallback: true, 33 | callbackURL: url.resolve(config.public_host_name || '', '/auth/google/callback') 34 | }, 35 | function(request, accessToken, refreshToken, profile, done) { 36 | var email = profile.emails[0].value; 37 | done(null, { 38 | email : email, 39 | emailHash: md5(email), 40 | isAdmin: isAdmin(email) 41 | }); 42 | } 43 | )); 44 | 45 | passport.serializeUser(function(user, done) { 46 | done(null, user); 47 | }); 48 | 49 | passport.deserializeUser(function(obj, done) { 50 | done(null, obj); 51 | }); 52 | 53 | app.use(passport.initialize()); 54 | app.use(passport.session()); 55 | 56 | app.get('/auth/google', passport.authenticate('google', { 57 | scope: 'https://www.google.com/m8/feeds https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile' 58 | })); 59 | 60 | app.get('/auth/google/callback', passport.authenticate('google', { 61 | failureRedirect: '/' 62 | }), function(req, res) { 63 | // successful authentication 64 | res.redirect('/'); 65 | }); 66 | 67 | app.get('/logout', function (req, res){ 68 | req.logOut(); 69 | res.redirect('/'); 70 | }); 71 | } 72 | }; 73 | 74 | }()); 75 | -------------------------------------------------------------------------------- /lib/service-validator.js: -------------------------------------------------------------------------------- 1 | var validator = require('validator'); 2 | 3 | exports = module.exports = (function(){ 4 | 5 | function validateOptionalInt(service, errors, field, options) { 6 | if (service[field]){ 7 | validateInt(service, errors, field, options); 8 | } 9 | } 10 | 11 | function validateInt(service, errors, field, options) { 12 | if (!validator.isInt(service[field], options || {})) { 13 | errors.push({ field: field, error: 'Invalid value for "'+ field + '"'}); 14 | } 15 | } 16 | 17 | function validateExistence (service, errors, field) { 18 | if (!service[field]) { 19 | errors.push({ field: field, error: 'A value is required for field "' + field + '"'}); 20 | } 21 | } 22 | 23 | function validateOptionalCommaSeparatedEmails(service, errors, field) { 24 | var fieldValue = service[field]; 25 | if (fieldValue) { 26 | var emails = fieldValue.split(',').map(function(email){return email.trim();}); 27 | emails.forEach(function(email){ 28 | if (!validator.isEmail(email)) { 29 | errors.push({ field: field, error: email + ' is not a valid email for field "' + field + '"'}); 30 | } 31 | }); 32 | } 33 | } 34 | 35 | return { 36 | 37 | /** 38 | * Validates service. Returns an array of errors if not valid or null if valid 39 | * @param service 40 | * @param cb 41 | */ 42 | 43 | validate: function(service) { 44 | var errors = []; 45 | 46 | if (service === null || typeof service !== 'object') { 47 | errors.push({ field: '', error: 'Invalid service object'}); 48 | return errors; 49 | } 50 | 51 | validateInt(service, errors, 'failureInterval', { min: 500 }); 52 | validateInt(service, errors, 'interval', { min: 500 }); 53 | validateInt(service, errors, 'warningThreshold'); 54 | validateOptionalInt(service, errors, 'failuresToBeOutage', { min: 1 }); 55 | validateInt(service, errors, 'port', { min: 0 }); 56 | validateInt(service, errors, 'timeout', { min: 0 }); 57 | validateExistence(service, errors, 'pingServiceName'); 58 | validateExistence(service, errors, 'name'); 59 | validateExistence(service, errors, 'url'); 60 | 61 | validateOptionalCommaSeparatedEmails(service, errors, 'restrictedTo'); 62 | validateOptionalCommaSeparatedEmails(service, errors, 'alertTo'); 63 | 64 | return errors; 65 | } 66 | 67 | }; 68 | 69 | })(); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var plugins = require('gulp-load-plugins')(); 3 | var mainBowerFiles = require('main-bower-files'); 4 | var del = require('del'); 5 | var runSequence = require('run-sequence'); 6 | 7 | function js(shouldMinify) { 8 | return gulp.src(mainBowerFiles().concat([ // not taking wildcards 9 | './webserver/public/js/controllers/**', 10 | './webserver/public/js/charting/**', 11 | './webserver/public/js/directives/**', 12 | './webserver/public/js/**' 13 | ])) 14 | .pipe(plugins.filter('*.js')) 15 | .pipe(plugins.concat('scripts.js')) 16 | .pipe(plugins.if(shouldMinify, plugins.ngAnnotate())) 17 | .pipe(plugins.if(shouldMinify, plugins.uglify())) 18 | .pipe(gulp.dest('./webserver/public/build')); 19 | } 20 | 21 | function css(shouldMinify) { 22 | return gulp.src(mainBowerFiles().concat(['./webserver/public/less/*.less'])) 23 | .pipe(plugins.filter(['*.css', '*.less'])) 24 | .pipe(plugins.less()) 25 | .pipe(plugins.concat('style.css')) 26 | .pipe(plugins.if(shouldMinify, plugins.minifyCss({keepBreaks: true}))) 27 | .pipe(gulp.dest('./webserver/public/build')); 28 | } 29 | 30 | gulp.task('clean', function () { 31 | return del([ 32 | './webserver/public/build/*', 33 | './webserver/public/fonts/*' 34 | ]); 35 | }); 36 | 37 | gulp.task('copy-fonts', function () { 38 | return gulp.src('./webserver/public/bower_components/bootstrap/fonts/*') 39 | .pipe(plugins.copy('./webserver/public/fonts/', {prefix: 5})); 40 | }); 41 | 42 | gulp.task('lint', function () { 43 | return gulp.src([ 44 | './webserver/**/*.js', 45 | '!./webserver/public/bower_components/**/*.js', 46 | '!./webserver/public/build/**/*.js', 47 | './lib/**/*.js', 48 | './test/**/*.js', 49 | './config/**/*.js', 50 | './scripts/**/*.js', 51 | './*.js' 52 | ]) 53 | .pipe(plugins.jshint()) 54 | .pipe(plugins.jshint.reporter('default')); 55 | }); 56 | 57 | gulp.task('js-dev', function () { 58 | return js(false); 59 | }); 60 | 61 | gulp.task('js-prod', function () { 62 | return js(true); 63 | }); 64 | 65 | gulp.task('css-prod', function () { 66 | return css(true); 67 | }); 68 | 69 | gulp.task('build', function () { 70 | return runSequence('clean', ['copy-fonts', 'js-prod', 'css-prod']); 71 | }); 72 | 73 | gulp.task('watch', function () { 74 | gulp.watch('./webserver/public/bower_components/**/*', ['css-prod', 'js-dev']); 75 | gulp.watch('./webserver/public/js/**', ['js-dev']); 76 | gulp.watch('./webserver/public/less/*', ['css-prod']); 77 | }); 78 | 79 | gulp.task('default', ['build']); -------------------------------------------------------------------------------- /coverage/lcov-report/config/notifications/services/postmark.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for config/notifications/services/postmark.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for config/notifications/services/postmark.js

17 |

18 | Statements: 100% (2 / 2)      19 | Branches: 100% (0 / 0)      20 | Functions: 100% (1 / 1)      21 | Lines: 100% (2 / 2)      22 | Ignored: none      23 |

24 | 25 |
26 |
27 |

28 | 
44 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 61 34 | 1 35 |   36 |   37 |   38 |  
module.exports = (function(){
39 |     return {
40 |         from: process.env.WATCHMEN_POSTMARK_FROM,
41 |         API_KEY: process.env.WATCHMEN_POSTMARK_API_KEY
42 |     };
43 | })();
45 | 46 |
47 | 50 | 51 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /coverage/lcov-report/config/general.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for config/general.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for config/general.js

17 |

18 | Statements: 100% (3 / 3)      19 | Branches: 100% (0 / 0)      20 | Functions: 100% (1 / 1)      21 | Lines: 100% (3 / 3)      22 | Ignored: none      23 |

24 |
All files » config/ » general.js
25 |
26 |
27 |

28 | 
53 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 8 36 | 91 37 |   38 | 1 39 |   40 | 1 41 |   42 |   43 |   44 |  
module.exports = (function(){
45 |  
46 |   var expirationForEventsInDays = 10;
47 |  
48 |   return {
49 |     remove_events_older_than_seconds : 60 * 60 * 24 * expirationForEventsInDays
50 |   }
51 |  
52 | })();
54 | 55 |
56 | 59 | 60 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /terraform/digital-ocean/user-data.yml: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo apt-get update 3 | sudo apt-get install -y build-essential git nginx make libc6-dev 4 | # Temp nginx placeholder: 5 | sudo echo "Installing droplet for watchmen..." > /usr/share/nginx/html/index.html 6 | 7 | # Installing node: 8 | git clone https://github.com/nodejs/node.git /node 9 | cd /node 10 | git checkout v4.4.1 11 | ./configure 12 | make 13 | sudo make install 14 | 15 | # Installing redis: 16 | curl -sSL http://download.redis.io/releases/redis-stable.tar.gz -o /tmp/redis.tar.gz 17 | mkdir -p /tmp/redis 18 | tar -xzf /tmp/redis.tar.gz -C /tmp/redis --strip-components=1 19 | make -C /tmp/redis 20 | make -C /tmp/redis install 21 | echo -n | /tmp/redis/utils/install_server.sh 22 | rm -rf /tmp/redis* 23 | # See: http://redis.io/topics/faq 24 | sysctl vm.overcommit_memory=1 25 | # Bind Redis to localhost. Comment out to make available externally. 26 | sed -ie 's/# bind 127.0.0.1/bind 127.0.0.1/g' /etc/redis/6379.conf 27 | service redis_6379 restart 28 | 29 | # We need to create swap space so npm install doesn't get killed in a 512Mb droplet 30 | sudo fallocate -l 2G /swapfile 31 | sudo mkswap /swapfile 32 | sudo swapon /swapfile 33 | 34 | # configure nginx 35 | sudo echo "server { listen 80; location / { proxy_pass http://127.0.0.1:3000/; } }" > /etc/nginx/sites-available/default 36 | sudo /etc/init.d/nginx restart 37 | 38 | # Install pm2 process management 39 | sudo npm install -g pm2 40 | 41 | # Add non-root user 42 | useradd watchmen 43 | mkdir /home/watchmen 44 | chown watchmen:watchmen /home/watchmen 45 | 46 | # Configure pm2 startup 47 | pm2 startup linux -u watchmen --hp /home/watchmen 48 | 49 | sudo su watchmen 50 | export HOME=/home/watchmen # needed by pm2 51 | 52 | # Installing watchmen: 53 | cd /home/watchmen 54 | git clone https://github.com/iloire/watchmen.git watchmen-app 55 | cd /home/watchmen/watchmen-app 56 | npm install 57 | 58 | export NODE_ENV=production 59 | export WATCHMEN_REDIS_PORT_PRODUCTION=6379 60 | 61 | # Watchmen private config 62 | export WATCHMEN_BASE_URL="http://watchmen-demo.letsnode.com/" # 63 | export WATCHMEN_ADMINS="" 64 | export WATCHMEN_WEB_PORT=3000 65 | export WATCHMEN_GOOGLE_CLIENT_ID="" 66 | export WATCHMEN_GOOGLE_CLIENT_SECRET="" 67 | export WATCHMEN_GOOGLE_ANALYTICS_ID="" 68 | 69 | # Notifications 70 | export WATCHMEN_NOTIFICATIONS_AWS_SES_ENABLED='true' 71 | export WATCHMEN_AWS_FROM='' 72 | export WATCHMEN_AWS_REGION='us-east-1' 73 | export WATCHMEN_AWS_KEY='' 74 | export WATCHMEN_AWS_SECRET='' 75 | 76 | # Run services: 77 | pm2 start run-monitor-server.js 78 | pm2 start run-web-server.js 79 | pm2 save 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Iván Loire (http://iloire.com/)", 3 | "name": "watchmen", 4 | "scripts": { 5 | "start": "node run-web-server", 6 | "build": "bower install && gulp build", 7 | "build:watch": "gulp watch", 8 | "coverage": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- --ui bdd -R spec -t 5000", 9 | "postcoverage": "kill `cat test/redis.pid`", 10 | "posttest": "kill `cat test/redis.pid`", 11 | "precoverage": "redis-server test/redis.test.conf", 12 | "pretest": "redis-server test/redis.test.conf", 13 | "test": "mocha -R list test/*.js" 14 | }, 15 | "description": "A simple service monitor", 16 | "version": "3.3.1", 17 | "homepage": "http://letsnode.com", 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/iloire/watchmen.git" 21 | }, 22 | "engines": { 23 | "node": ">=0.10" 24 | }, 25 | "keywords": [ 26 | "monitor", 27 | "ping", 28 | "service", 29 | "watchmen" 30 | ], 31 | "license": "MIT", 32 | "dependencies": { 33 | "async": "^0.9.0", 34 | "body-parser": "^1.12.4", 35 | "colors": "^1.1.0", 36 | "commander": "^2.8.1", 37 | "compression": "^1.4.3", 38 | "concat-stream": "^1.4.8", 39 | "connect-redis": "^3.0.1", 40 | "debug": "^2.2.0", 41 | "ejs": "2.3.x", 42 | "errorhandler": "^1.3.6", 43 | "express": "^4.12.3", 44 | "express-ejs-layouts": "^1.1.0", 45 | "express-session": "^1.10.4", 46 | "lodash": "^3.8.0", 47 | "method-override": "2.3.x", 48 | "moment": "^2.10.3", 49 | "passport": "^0.2.1", 50 | "passport-google-oauth2": "0.1.x", 51 | "q": "^1.3.0", 52 | "redis": "^0.12.1", 53 | "shortid": "^2.2.2", 54 | "stream-spigot": "^3.0.5", 55 | "timestream-aggregates": "^0.1.7", 56 | "validator": "^3.39.0", 57 | "watchmen-ping-http-contains": "^0.0.2", 58 | "watchmen-ping-http-head": "^0.2.0", 59 | "watchmen-plugin-aws-ses": "0.0.1", 60 | "watchmen-plugin-console": "0.2.0" 61 | }, 62 | "devDependencies": { 63 | "assert": "^1.3.0", 64 | "bower": "1.4.x", 65 | "del": "^2.0.2", 66 | "faker": "^2.1.5", 67 | "gulp": "^3.9.0", 68 | "gulp-concat": "^2.5.2", 69 | "gulp-copy": "0.0.2", 70 | "gulp-filter": "^2.0.2", 71 | "gulp-if": "^1.2.5", 72 | "gulp-jshint": "^1.10.0", 73 | "gulp-less": "^3.0.2", 74 | "gulp-load-plugins": "^0.9.0", 75 | "gulp-minify-css": "^1.0.0", 76 | "gulp-ng-annotate": "^1.0.0", 77 | "gulp-uglify": "^1.2.0", 78 | "gulp-watch": "^4.2.4", 79 | "istanbul": "0.3.x", 80 | "main-bower-files": "^2.6.2", 81 | "mocha": "^2.2.5", 82 | "passport-mock": "0.0.3", 83 | "run-sequence": "^1.1.4", 84 | "sinon": "^1.14.1", 85 | "supertest": "^1.0.1" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /coverage/lcov-report/config/notifications/services/aws-ses.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for config/notifications/services/aws-ses.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for config/notifications/services/aws-ses.js

17 |

18 | Statements: 100% (2 / 2)      19 | Branches: 100% (0 / 0)      20 | Functions: 100% (1 / 1)      21 | Lines: 100% (2 / 2)      22 | Ignored: none      23 |

24 | 25 |
26 |
27 |

28 | 
50 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 81 36 | 1 37 |   38 |   39 |   40 |   41 |   42 |  
module.exports = (function(){
43 |     return {
44 |         from: process.env.WATCHMEN_AWS_FROM,
45 |         region: process.env.WATCHMEN_AWS_REGION,
46 |         AWS_KEY: process.env.WATCHMEN_AWS_KEY,
47 |         AWS_SECRET: process.env.WATCHMEN_AWS_SECRET
48 |     };
49 | })();
51 | 52 |
53 | 56 | 57 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/aggregator.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/aggregator.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/aggregator.js

17 |

18 | Statements: 100% (6 / 6)      19 | Branches: 100% (0 / 0)      20 | Functions: 100% (2 / 2)      21 | Lines: 100% (6 / 6)      22 | Ignored: none      23 |

24 |
All files » lib/ » aggregator.js
25 |
26 |
27 |

28 | 
74 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 8 36 | 9 37 | 10 38 | 11 39 | 12 40 | 13 41 | 14 42 | 15 43 | 161 44 | 1 45 | 1 46 |   47 | 1 48 |   49 | 1 50 |   51 |   52 | 11 53 |   54 |   55 |   56 |   57 |   58 |  
var spigot = require("stream-spigot");
59 | var agg = require("timestream-aggregates");
60 | var concat = require("concat-stream");
61 |  
62 | exports = module.exports = (function(){
63 |  
64 |   return {
65 |  
66 |     aggregate: function(arr, timeunit, cb){
67 |       spigot({objectMode: true}, arr)
68 |           .pipe(agg.mean("t", timeunit))
69 |           .pipe(concat(cb));
70 |     }
71 |   }
72 |  
73 | })();
75 | 76 |
77 | 80 | 81 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /coverage/lcov-report/webserver/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for webserver/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for webserver/

17 |

18 | Statements: 92.11% (35 / 38)      19 | Branches: 50% (2 / 4)      20 | Functions: 100% (2 / 2)      21 | Lines: 92.11% (35 / 38)      22 | Ignored: none      23 |

24 |
All files » webserver/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
FileStatementsBranchesFunctionsLines
app.js92.11%(35 / 38)50%(2 / 4)100%(2 / 2)92.11%(35 / 38)
58 |
59 |
60 | 63 | 64 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/storage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/storage/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/storage/

17 |

18 | Statements: 81.82% (9 / 11)      19 | Branches: 50% (1 / 2)      20 | Functions: 100% (1 / 1)      21 | Lines: 81.82% (9 / 11)      22 | Ignored: none      23 |

24 |
All files » lib/storage/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
FileStatementsBranchesFunctionsLines
storage-factory.js81.82%(9 / 11)50%(1 / 2)100%(1 / 1)81.82%(9 / 11)
58 |
59 |
60 | 63 | 64 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /coverage/lcov-report/config/notifications/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for config/notifications/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for config/notifications/

17 |

18 | Statements: 100% (2 / 2)      19 | Branches: 100% (0 / 0)      20 | Functions: 100% (1 / 1)      21 | Lines: 100% (2 / 2)      22 | Ignored: none      23 |

24 |
All files » config/notifications/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
FileStatementsBranchesFunctionsLines
notifications.js100%(2 / 2)100%(0 / 0)100%(1 / 1)100%(2 / 2)
58 |
59 |
60 | 63 | 64 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/notifications/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/notifications/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/notifications/

17 |

18 | Statements: 83.33% (50 / 60)      19 | Branches: 55% (11 / 20)      20 | Functions: 82.35% (14 / 17)      21 | Lines: 82.76% (48 / 58)      22 | Ignored: none      23 |

24 |
All files » lib/notifications/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
FileStatementsBranchesFunctionsLines
notifications.js83.33%(50 / 60)55%(11 / 20)82.35%(14 / 17)82.76%(48 / 58)
58 |
59 |
60 | 63 | 64 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/storage/providers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/storage/providers/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/storage/providers/

17 |

18 | Statements: 93.16% (109 / 117)      19 | Branches: 69.05% (29 / 42)      20 | Functions: 96.67% (29 / 30)      21 | Lines: 93.16% (109 / 117)      22 | Ignored: none      23 |

24 |
All files » lib/storage/providers/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
FileStatementsBranchesFunctionsLines
redis.js93.16%(109 / 117)69.05%(29 / 42)96.67%(29 / 30)93.16%(109 / 117)
58 |
59 |
60 | 63 | 64 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/notifications/services/aws-ses/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/notifications/services/aws-ses/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/notifications/services/aws-ses/

17 |

18 | Statements: 84% (21 / 25)      19 | Branches: 88.24% (15 / 17)      20 | Functions: 75% (3 / 4)      21 | Lines: 84% (21 / 25)      22 | Ignored: none      23 |

24 |
All files » lib/notifications/services/aws-ses/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
FileStatementsBranchesFunctionsLines
aws-ses.js84%(21 / 25)88.24%(15 / 17)75%(3 / 4)84%(21 / 25)
58 |
59 |
60 | 63 | 64 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /webserver/public/js/charting/charting.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | window.Charting = window.Charting || {}; 6 | 7 | var MINUTE = 60 * 1000; 8 | 9 | /** 10 | * Renders a C3 chart 11 | * @param data 12 | */ 13 | Charting.render = function (options) { 14 | 15 | var latencyData = parseArrayObjectsForCharting(options.latency, 't', 'l'); 16 | 17 | var latencySerie = latencyData.data; 18 | var timeSerie = latencyData.time; 19 | 20 | timeSerie.splice(0, 0, 'x'); 21 | latencySerie.splice(0, 0, 'Latency'); 22 | 23 | var outagesRegions = []; 24 | if (options.outages) { 25 | for (var i = 0; i < options.outages.length; i++) { 26 | var outage = options.outages[i]; 27 | outagesRegions.push({ 28 | axis: 'x', 29 | start: outage.timestamp, 30 | end: outage.timestamp + outage.downtime, 31 | class: 'region-outage', 32 | opacity: 1 33 | }); 34 | } 35 | } 36 | 37 | var regions = [ 38 | {axis: 'y', start: options.threshold, class: 'region-latency-warning'}, 39 | ].concat(outagesRegions); 40 | 41 | return generateLatencyChart({ 42 | size: options.size, 43 | id: options.id, 44 | x_format: options.x_format, 45 | columns: [timeSerie, latencySerie], 46 | grid: { 47 | y: { 48 | lines: [ 49 | {value: options.threshold, text: 'latency threshold', class: 'threshold'} 50 | ] 51 | } 52 | }, 53 | regions: regions, 54 | max: options.max 55 | }); 56 | }; 57 | 58 | /** 59 | * Converts [{t: 1428220800000, l: 123.23}] 60 | * 61 | * into 62 | * 63 | * { time : [1428220800000], data: [123] } 64 | * 65 | */ 66 | function parseArrayObjectsForCharting(arr, fieldTime, fieldData) { 67 | var time = []; 68 | var latency = []; 69 | for (var i = 0; i < arr.length; i++) { 70 | time.push(new Date(arr[i][fieldTime])); 71 | latency.push(Math.round([arr[i][fieldData]])); 72 | } 73 | return {time: time, data: latency}; 74 | } 75 | 76 | function generateLatencyChart (options) { 77 | 78 | return c3.generate({ 79 | size: options.size, 80 | bindto: options.id, 81 | legend: { 82 | show: false 83 | }, 84 | data: { 85 | x: 'x', 86 | columns: options.columns, 87 | types: { 88 | Latency: 'area' 89 | }, 90 | colors: { 91 | Latency: 'green' 92 | } 93 | }, 94 | axis: { 95 | y: { 96 | max: isNaN(options.max) ? 0 : options.max, 97 | tick: { 98 | values: [200, 500, 1000, 2000, 3000, 4000, 5000, 7000, 10000, 15000, 20000, 30000] 99 | } 100 | }, 101 | x: { 102 | type: 'timeseries', 103 | tick: { 104 | format: options.x_format || '%H:%M' 105 | } 106 | } 107 | }, 108 | grid: options.grid, 109 | regions: options.regions, 110 | tooltip: { 111 | format: { 112 | title: function (d) { 113 | return moment(d).format('DD/MMM/YY HH:mm') + ' (' + moment(d).fromNow() + ')'; 114 | }, 115 | value: function (value, ratio, id) { 116 | if (id == 'Outages') { 117 | return moment.duration(value).humanize(); 118 | } 119 | else { 120 | return value + ' ms.'; 121 | } 122 | } 123 | } 124 | } 125 | }); 126 | 127 | } 128 | 129 | })(); 130 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/notifications/services/postmark/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/notifications/services/postmark/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/notifications/services/postmark/

17 |

18 | Statements: 83.33% (20 / 24)      19 | Branches: 86.67% (13 / 15)      20 | Functions: 75% (3 / 4)      21 | Lines: 83.33% (20 / 24)      22 | Ignored: none      23 |

24 |
All files » lib/notifications/services/postmark/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
FileStatementsBranchesFunctionsLines
postmark.js83.33%(20 / 24)86.67%(13 / 15)75%(3 / 4)83.33%(20 / 24)
58 |
59 |
60 | 63 | 64 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /test/test-service-access-filter.js: -------------------------------------------------------------------------------- 1 | var serviceFilter = require('../lib/service-access-filter'); 2 | var assert = require('assert'); 3 | 4 | describe('service access filter', function () { 5 | var SERVICES = [ 6 | { 7 | name: 'service 1', 8 | restrictedTo: 'admin@domain.com' 9 | }, 10 | { 11 | name: 'service 2', 12 | restrictedTo: 'user@domain.com' 13 | } 14 | ]; 15 | 16 | var SERVICES_REPORTS = [ 17 | { 18 | service: SERVICES[0] 19 | }, 20 | { 21 | service: SERVICES[1] 22 | } 23 | ]; 24 | 25 | describe('isServiceRestrictedToEmail', function(){ 26 | it('should return if email is restricted', function () { 27 | assert.ok(!serviceFilter._isServiceRestrictedToEmail(SERVICES[0], 'admin@domain.com')); 28 | assert.ok(serviceFilter._isServiceRestrictedToEmail(SERVICES[0], 'dmin@domain.com')); 29 | assert.ok(!serviceFilter._isServiceRestrictedToEmail({name : 'test'}, 'dmin@domain.com')); 30 | assert.ok(serviceFilter._isServiceRestrictedToEmail({ 31 | name : 'test', 32 | restrictedTo: 'admin@domain.com' 33 | }, 'user@domain.com')); 34 | }); 35 | 36 | it('should handle empty email', function () { 37 | assert.ok(serviceFilter._isServiceRestrictedToEmail(SERVICES[0], '')); 38 | assert.ok(serviceFilter._isServiceRestrictedToEmail(SERVICES[0], null)); 39 | assert.ok(serviceFilter._isServiceRestrictedToEmail(SERVICES[0], undefined)); 40 | assert.ok(!serviceFilter._isServiceRestrictedToEmail(null, undefined)); 41 | }); 42 | 43 | it('should handle empty email in restrictedTo', function () { 44 | assert.ok(serviceFilter._isServiceRestrictedToEmail({ 45 | name: 'test', 46 | restrictedTo: ' ,email1@domain.com' 47 | }, '')); 48 | }); 49 | 50 | }); 51 | 52 | describe('filter Reports', function () { 53 | it('should filter services', function () { 54 | var resports = serviceFilter.filterReports(SERVICES_REPORTS, {email : 'admin@domain.com'}); 55 | assert.equal(resports.length, 1); 56 | assert.equal(resports[0].service.name, 'service 1'); 57 | }); 58 | 59 | it('should match full address', function () { 60 | var reports = serviceFilter.filterReports(SERVICES_REPORTS, {email : 'min@domain.com'}); 61 | assert.equal(reports.length, 0); 62 | }); 63 | 64 | it('should filter a single service', function () { 65 | var reports = serviceFilter.filterReports(SERVICES_REPORTS[1], {email : 'admin@domain.com'}); 66 | assert.equal(reports, undefined); 67 | }); 68 | 69 | it('should handle null parameters', function () { 70 | var reports = serviceFilter.filterReports(null, {email : 'admin@domain.com'}); 71 | assert.equal(reports, undefined); 72 | }); 73 | }); 74 | 75 | describe('filter services', function () { 76 | it('should filter services', function () { 77 | var services = serviceFilter.filterServices(SERVICES, {email : 'admin@domain.com'}); 78 | assert.equal(services.length, 1); 79 | assert.equal(services[0].name, 'service 1'); 80 | }); 81 | 82 | it('should match full address', function () { 83 | var services = serviceFilter.filterServices(SERVICES, {email : 'min@domain.com'}); 84 | assert.equal(services.length, 0); 85 | }); 86 | 87 | it('should filter a single service', function () { 88 | var services = serviceFilter.filterServices(SERVICES[1], {email : 'admin@domain.com'}); 89 | assert.equal(services, undefined); 90 | }); 91 | 92 | it('should handle null parameters', function () { 93 | var services = serviceFilter.filterServices(null, {email : 'admin@domain.com'}); 94 | assert.equal(services, undefined); 95 | }); 96 | }); 97 | 98 | }); -------------------------------------------------------------------------------- /coverage/lcov-report/config/web.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for config/web.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for config/web.js

17 |

18 | Statements: 100% (1 / 1)      19 | Branches: 50% (2 / 4)      20 | Functions: 100% (0 / 0)      21 | Lines: 100% (1 / 1)      22 | Ignored: none      23 |

24 |
All files » config/ » web.js
25 |
26 |
27 |

28 | 
80 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 8 36 | 9 37 | 10 38 | 11 39 | 12 40 | 13 41 | 14 42 | 15 43 | 16 44 | 17 45 | 181 46 |   47 |   48 |   49 |   50 |   51 |   52 |   53 |   54 |   55 |   56 |   57 |   58 |   59 |   60 |   61 |   62 |  
module.exports = {
63 |  
64 |     public_host_name: process.env.WATCHMEN_BASE_URL, // required for OAuth dance
65 |  
66 |     auth: {
67 |         GOOGLE_CLIENT_ID: process.env.WATCHMEN_GOOGLE_CLIENT_ID || '<Create credentials in Google Dev Console>',
68 |         GOOGLE_CLIENT_SECRET: process.env.WATCHMEN_GOOGLE_CLIENT_SECRET || '<Create credentials in Google Dev Console>'
69 |     },
70 |  
71 |     port: process.env.WATCHMEN_WEB_PORT, // default port
72 |  
73 |     admins: process.env.WATCHMEN_ADMINS,
74 |  
75 |     ga_analytics_ID: process.env.WATCHMEN_GOOGLE_ANALYTICS_ID,
76 |  
77 |     baseUrl: '/'
78 | };
79 |  
81 | 82 |
83 | 86 | 87 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /webserver/public/js/controllers/services-list.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | var SERVICES_POLLING_INTERVAL = 10000; 6 | var timer; 7 | 8 | var watchmenControllers = angular.module('watchmenControllers'); 9 | 10 | watchmenControllers.controller('ServiceListCtrl', 11 | function ( 12 | $scope, 13 | $filter, 14 | $timeout, 15 | Report, 16 | Service, 17 | usSpinnerService, 18 | ngTableUtils 19 | ) { 20 | 21 | function scheduleNextTick() { 22 | $timeout.cancel(timer); 23 | timer = $timeout(function () { 24 | reload(scheduleNextTick, loadServicesErrHandler); 25 | }, SERVICES_POLLING_INTERVAL); 26 | } 27 | 28 | function loadServicesErrHandler(err) { 29 | $scope.errorLoadingServices = "Error loading data from remote server"; 30 | console.error(err); 31 | scheduleNextTick(); 32 | } 33 | 34 | function reload(doneCb, errorHandler) { 35 | Report.clearCache(); 36 | $scope.services = Report.query(function (services) { 37 | $scope[key] = services; 38 | $scope.tableParams.reload(); 39 | 40 | $scope.errorLoadingServices = null; // reset error 41 | transition.loaded(); 42 | doneCb(); 43 | }, errorHandler); 44 | } 45 | 46 | var transition = { 47 | loading: function () { 48 | usSpinnerService.spin('spinner-1'); 49 | $scope.loading = true; 50 | }, 51 | loaded: function () { 52 | usSpinnerService.stop('spinner-1'); 53 | $scope.loading = false; 54 | } 55 | }; 56 | 57 | var key = 'tableServicesData'; 58 | $scope[key] = []; 59 | $scope.tableParams = ngTableUtils.createngTableParams(key, $scope, $filter); 60 | 61 | var filterToMeCheckboxIsPresent = document.getElementById('filterRestrictedToMe'); 62 | if (filterToMeCheckboxIsPresent && window.localStorage) { 63 | var filterToMeStoredValue = (window.localStorage.getItem('filterRestrictedToMe') === 'true'); 64 | $timeout(function(){ 65 | filterToMeCheckboxIsPresent.checked = filterToMeStoredValue; 66 | $scope.filterRestrictedToMe = filterToMeStoredValue; 67 | }, 0); 68 | } 69 | 70 | $scope.$watch('filterRestrictedToMe', 71 | function (newValue) { 72 | if (window.localStorage) { 73 | window.localStorage.setItem('filterRestrictedToMe', newValue); 74 | } 75 | } 76 | ); 77 | 78 | transition.loading(); 79 | 80 | $scope.serviceFilter = function (row) { 81 | if ($scope.filterRestrictedToMe && !row.service.isRestricted) { 82 | return false; 83 | } 84 | return row.service.name.indexOf($scope.query || '') > -1; 85 | }; 86 | 87 | $scope.delete = function (id) { 88 | if (confirm('Are you sure you want to delete this service and all its data?')) { 89 | Service.delete({id: id}, function () { 90 | reload(function () { 91 | }, function () { 92 | $scope.errorLoadingServices = "Error loading data from remote server"; 93 | }); 94 | }); 95 | } 96 | }; 97 | 98 | $scope.reset = function (id) { 99 | if (confirm('Are you sure you want to reset this service\'s data?')) { 100 | Service.reset({id: id}, function () { 101 | reload(function () { 102 | }, function () { 103 | $scope.errorLoadingServices = "Error loading data from remote server"; 104 | }); 105 | }); 106 | } 107 | }; 108 | 109 | reload(scheduleNextTick, loadServicesErrHandler); 110 | 111 | }); 112 | 113 | })(); 114 | -------------------------------------------------------------------------------- /lib/sentinel.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var debug = require('debug')('sentinel'); 3 | 4 | /** 5 | * Watches for changes in services to restart them when they change. 6 | */ 7 | 8 | function Sentinel(storage, watchmen, options) { 9 | this.options = options || {}; 10 | this.storage = storage; 11 | this.watchmen = watchmen; 12 | } 13 | 14 | exports = module.exports = Sentinel; 15 | 16 | var DEFAULT_INTERVAL = 5000; 17 | var FIELDS_WHICH_MODIFICATION_TRIGGER_SERVICE_RESTART = [ 18 | 'name', 19 | 'interval', 20 | 'failureInterval', 21 | 'failuresToBeOutage', 22 | 'port', 23 | 'url', 24 | 'timeout', 25 | 'alertTo', 26 | 'pingServiceName', 27 | 'pingServiceOptions', 28 | 'warningThreshold' 29 | ]; 30 | var timeoutId = null; 31 | 32 | Sentinel.prototype._findAdded = function (databaseServices, runningServices) { 33 | var added = []; 34 | databaseServices.forEach(function (s) { 35 | if (!_.find(runningServices, function (rs) { 36 | return rs.id == s.id; 37 | })) { 38 | added.push(s); 39 | } 40 | }); 41 | return added; 42 | }; 43 | 44 | Sentinel.prototype._findRemoved = function (databaseServices, runningServices) { 45 | var removed = []; 46 | runningServices.forEach(function (runningService) { 47 | if (!_.find(databaseServices, function (s) { 48 | return s.id == runningService.id; 49 | })) { 50 | removed.push(runningService); 51 | } 52 | }); 53 | return removed; 54 | }; 55 | 56 | function propertyModified(propDb, propService) { 57 | return JSON.stringify(propDb) !== JSON.stringify(propService); 58 | } 59 | 60 | Sentinel.prototype._findModified = function (databaseServices, runningServices) { 61 | var modified = []; 62 | runningServices.forEach(function (runningService) { 63 | var dbService = _.find(databaseServices, function (rs) { 64 | return rs.id == runningService.id; 65 | }); 66 | if (dbService) { 67 | for (var key in dbService) { 68 | if (FIELDS_WHICH_MODIFICATION_TRIGGER_SERVICE_RESTART.indexOf(key) > -1) { 69 | if (propertyModified(dbService[key], runningService[key])) { 70 | 71 | debug('property ' + key + ' from service ' + dbService.name + ' changed.'); 72 | debug('db:'); 73 | debug(dbService[key]); 74 | debug('running service:'); 75 | debug(runningService[key]); 76 | 77 | modified.push(dbService); 78 | break; 79 | } 80 | } 81 | else { 82 | debug('detected changed in field ' + key + '. Ignored'); 83 | } 84 | } 85 | } 86 | }); 87 | return modified; 88 | }; 89 | 90 | Sentinel.prototype._tick = function () { 91 | 92 | var self = this; 93 | this.storage.getServices({}, function (err, services) { 94 | 95 | var newServices = self._findAdded(services, self.watchmen.services); 96 | if (newServices.length) { 97 | newServices.forEach(function (service) { 98 | console.log('service added: ', service.name); 99 | self.watchmen.addService(service); 100 | }); 101 | } 102 | 103 | var removedServices = self._findRemoved(services, self.watchmen.services); 104 | if (removedServices.length) { 105 | removedServices.forEach(function (s) { 106 | self.watchmen.removeService(s.id); 107 | console.log('service removed: ', s.name); 108 | }); 109 | } 110 | 111 | var modifiedServices = self._findModified(services, self.watchmen.services); 112 | if (modifiedServices.length) { 113 | modifiedServices.forEach(function (s) { 114 | self.watchmen.removeService(s.id); 115 | self.watchmen.addService(s); 116 | console.log('changes detected in service: ', s.name, '. Restarting...'); 117 | }); 118 | } 119 | }); 120 | }; 121 | 122 | Sentinel.prototype.watch = function () { 123 | var self = this; 124 | clearInterval(timeoutId); 125 | timeoutId = setInterval(function () { 126 | self._tick(); 127 | }, this.options.interval || DEFAULT_INTERVAL); 128 | }; 129 | 130 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/storage/storage-factory.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/storage/storage-factory.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/storage/storage-factory.js

17 |

18 | Statements: 81.82% (9 / 11)      19 | Branches: 50% (1 / 2)      20 | Functions: 100% (1 / 1)      21 | Lines: 81.82% (9 / 11)      22 | Ignored: none      23 |

24 |
All files » lib/storage/ » storage-factory.js
25 |
26 |
27 |

 28 | 
 89 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 8 36 | 9 37 | 10 38 | 11 39 | 12 40 | 13 41 | 14 42 | 15 43 | 16 44 | 17 45 | 18 46 | 19 47 | 20 48 | 211 49 |   50 | 1 51 |   52 |   53 |   54 | 4 55 | 4 56 |   57 |   58 |   59 |   60 | 4 61 |   62 | 4 63 | 4 64 | 4 65 |   66 | 4 67 |   68 |  
var storageConfiguration = require ('../../config/storage');
 69 |  
 70 | module.exports = {
 71 |  
 72 |   getStorageInstance : function (env){
 73 |  
 74 |     var config = storageConfiguration[env];
 75 |     Iif (!config) {
 76 |       console.error('No environment found for ', env);
 77 |       return null;
 78 |     }
 79 |  
 80 |     console.log('Using storage env: ', env);
 81 |  
 82 |     var provider = storageConfiguration[env].provider;
 83 |     var providerOptions = require ('../../config/storage')[env].options[provider];
 84 |     var storage = require ('./providers/' + provider);
 85 |  
 86 |     return new storage(providerOptions);
 87 |   }
 88 | };
90 | 91 |
92 | 95 | 96 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /coverage/lcov-report/config/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for config/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for config/

17 |

18 | Statements: 100% (2 / 2)      19 | Branches: 87.5% (14 / 16)      20 | Functions: 100% (0 / 0)      21 | Lines: 100% (2 / 2)      22 | Ignored: none      23 |

24 |
All files » config/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
FileStatementsBranchesFunctionsLines
storage.js100%(1 / 1)100%(12 / 12)100%(0 / 0)100%(1 / 1)
web.js100%(1 / 1)50%(2 / 4)100%(0 / 0)100%(1 / 1)
71 |
72 |
73 | 76 | 77 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/ping_services/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/ping_services/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/ping_services/

17 |

18 | Statements: 28.92% (24 / 83)      19 | Branches: 15.56% (7 / 45)      20 | Functions: 26.67% (4 / 15)      21 | Lines: 28.92% (24 / 83)      22 | Ignored: none      23 |

24 |
All files » lib/ping_services/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
FileStatementsBranchesFunctionsLines
http.js8.47%(5 / 59)0%(0 / 35)0%(0 / 9)8.47%(5 / 59)
smtp.js79.17%(19 / 24)70%(7 / 10)66.67%(4 / 6)79.17%(19 / 24)
71 |
72 |
73 | 76 | 77 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /coverage/lcov-report/config/notifications/services/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for config/notifications/services/ 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for config/notifications/services/

17 |

18 | Statements: 100% (4 / 4)      19 | Branches: 100% (0 / 0)      20 | Functions: 100% (2 / 2)      21 | Lines: 100% (4 / 4)      22 | Ignored: none      23 |

24 |
All files » config/notifications/services/
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
FileStatementsBranchesFunctionsLines
aws-ses.js100%(2 / 2)100%(0 / 0)100%(1 / 1)100%(2 / 2)
postmark.js100%(2 / 2)100%(0 / 0)100%(1 / 1)100%(2 / 2)
71 |
72 |
73 | 76 | 77 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /scripts/data-load/populate-dummy-data.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var sinon = require('sinon'); 3 | var debug = require('debug')('data-load'); 4 | var watchmenFactory = require('../../lib/watchmen.js'); 5 | var mockedPingService = require('../../test/lib/mock/request-mocked'); 6 | var storageFactory = require('../../lib/storage/storage-factory'); 7 | var populator = require('../../test/lib/util/populator'); 8 | var responseRandomizer = require('./lib/response-randomizer'); 9 | var dummyServiceGenerator = require('../../test/fixtures/dummy-services'); 10 | 11 | var DEFAULT_PING_INTERVAL = 1000 * 60 * 1; // ms 12 | var DEFAULT_NUMBER_DAYS_BACK = 7; 13 | 14 | var watchmen; 15 | 16 | function run(programOptions, callback) { 17 | 18 | var pingInterval = programOptions.pingInterval || DEFAULT_PING_INTERVAL; 19 | var numberDaysBack = programOptions.numberDaysBack || DEFAULT_NUMBER_DAYS_BACK; 20 | var numberPingsBack = numberDaysBack * 1000 * 60 * 60 * 24 / pingInterval; 21 | var initialTime = +new Date() - pingInterval * numberPingsBack; 22 | var services = dummyServiceGenerator.generate(programOptions.numberServices || 20); 23 | 24 | debug('ping interval:' + pingInterval); 25 | debug('number days back:' + numberDaysBack); 26 | 27 | function generateDataForService(service, callback) { 28 | var totalPings = 0; 29 | 30 | clock = sinon.useFakeTimers(initialTime); 31 | 32 | service.pingService = mockedPingService; 33 | 34 | function ping(cb) { 35 | var res = responseRandomizer.getRandomResponse(service, programOptions.targetUptime); 36 | mockedPingService.mockedResponse = res; 37 | watchmen.ping({service: service}, function (err) { 38 | totalPings++; 39 | clock.tick(DEFAULT_PING_INTERVAL - res.latency); 40 | cb(err); 41 | }); 42 | clock.tick(res.latency); 43 | } 44 | 45 | async.whilst( 46 | function () { return totalPings < numberPingsBack; }, 47 | ping, 48 | function (err) { 49 | callback(err); 50 | } 51 | ); 52 | } 53 | 54 | function populatedata(services, cb) { 55 | debug('populating data for ' + services.length + ' services'); 56 | async.eachSeries(services, generateDataForService, function (err) { 57 | debug('data populated for all services'); 58 | cb(err); 59 | }); 60 | } 61 | 62 | var env = programOptions.env || 'development'; 63 | var storage = storageFactory.getStorageInstance(env); 64 | if (!storage) { 65 | console.error('Not available storage for the provided environment ' + env); 66 | return; 67 | } 68 | 69 | watchmen = new watchmenFactory(null, storage); 70 | 71 | storage.flush_database(function () { 72 | 73 | debug('database flushed'); 74 | 75 | clock = sinon.useFakeTimers(initialTime); 76 | 77 | populator.populate(services, storage, function (err) { 78 | if (err) { 79 | return callback(err); 80 | } 81 | 82 | debug('services populated'); 83 | 84 | storage.getServices({}, function (err, services) { 85 | if (err) { 86 | return callback(err); 87 | } 88 | 89 | if (programOptions.filter) { 90 | services = services.filter(function (s) { 91 | return s.name.indexOf(programOptions.filter) > -1; 92 | }); 93 | } 94 | 95 | services.sort(function (a, b) { 96 | return a.name > b.name; 97 | }); 98 | 99 | debug('program started'); 100 | 101 | populatedata(services, function(err){ 102 | storage.quit(); 103 | callback(err); 104 | }); 105 | 106 | }); 107 | }); 108 | }); 109 | } 110 | 111 | var program = require('commander'); 112 | program 113 | .option('-f, --filter [filter]', 'Filter services to add dummy data to (by name)') 114 | .option('-e, --env [env]', 'Storage environment key') 115 | .option('-u, --target-uptime [targetUptime]', 'targetUptime') 116 | .option('-p, --ping-interval [pingInterval]', 'Ping interval') 117 | .option('-d, --number-days-back [numberDaysBack]', 'Number of days back when populating database') 118 | .option('-s, --number-services [numberServices]', 'Number of services') 119 | 120 | .parse(process.argv); 121 | 122 | run(program, function () { 123 | debug('done!'); 124 | process.exit(0); 125 | }); -------------------------------------------------------------------------------- /webserver/public/js/controllers/service-details.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | 6 | var watchmenControllers = angular.module('watchmenControllers'); 7 | 8 | /** 9 | * Service details 10 | */ 11 | 12 | watchmenControllers.controller('ServiceDetailCtrl', 13 | 14 | function ( 15 | $scope, 16 | $filter, 17 | $stateParams, 18 | Report, 19 | ngTableUtils, 20 | usSpinnerService, 21 | $timeout) { 22 | 23 | function getChartSize() { 24 | return {height: 200, width: $('.chart-container').width()}; 25 | } 26 | 27 | function loading(){ 28 | usSpinnerService.spin('spinner-1'); 29 | $scope.loading = true; 30 | } 31 | 32 | function loaded(){ 33 | usSpinnerService.stop('spinner-1'); 34 | $scope.loading = false; 35 | } 36 | 37 | function errHandler (err){ 38 | console.log(err); 39 | loaded(); 40 | var msg = err.statusText; 41 | if (err.data && err.data.error){ 42 | msg = err.data.error; 43 | } 44 | $scope.errorLoadingService = msg; 45 | } 46 | 47 | loading(); 48 | $scope.serviceDetails = Report.get({id: $stateParams.id}, function (data) { 49 | loaded(); 50 | $scope.latestOutages = data.status.latestOutages; 51 | 52 | // charting 53 | var latencyLastHour = data.status.lastHour.latency; 54 | var latencyLast24Hours = data.status.last24Hours.latency; 55 | var latencyLastWeek = data.status.lastWeek.latency; 56 | 57 | var maxLastHour = _.max(latencyLastHour.list, function (item) { 58 | return item.l; 59 | }); 60 | var maxLast24Hours = _.max(latencyLast24Hours.list, function (item) { 61 | return item.l; 62 | }); 63 | var maxLastWeek = _.max(latencyLastWeek.list, function (item) { 64 | return item.l; 65 | }); 66 | 67 | var max = _.max([maxLastHour.l, maxLast24Hours.l, maxLastWeek.l]); 68 | 69 | var charts = []; 70 | $timeout(function () { 71 | var chartSize = getChartSize(); 72 | if (latencyLastHour.list.length > 0) { // at least one successful ping 73 | $scope.showLastHourChart = true; 74 | charts.push(Charting.render({ 75 | threshold: data.service.warningThreshold, 76 | latency: latencyLastHour.list, 77 | outages: data.status.lastHour.outages, 78 | id: '#chart-last-hour', 79 | size: chartSize, 80 | max: max 81 | })); 82 | } 83 | 84 | if (latencyLast24Hours.list.length > 8) { 85 | $scope.showLast24Chart = true; 86 | charts.push(Charting.render({ 87 | threshold: data.service.warningThreshold, 88 | latency: latencyLast24Hours.list, 89 | outages: data.status.last24Hours.outages, 90 | id: '#chart-last-24-hours', 91 | size: chartSize, 92 | max: max 93 | })); 94 | } 95 | 96 | if (latencyLastWeek.list.length > 1) { 97 | $scope.showLastWeekChart = true; 98 | charts.push(Charting.render({ 99 | threshold: data.service.warningThreshold, 100 | latency: latencyLastWeek.list, 101 | outages: data.status.lastWeek.outages, 102 | id: '#chart-last-week', 103 | size: chartSize, 104 | x_format: '%d/%m', 105 | max: max 106 | })); 107 | } 108 | 109 | function onWindowResize () { 110 | for (var i = 0; i < charts.length; i++) { 111 | charts[i].resize(getChartSize()); 112 | } 113 | } 114 | 115 | $scope.$on('$destroy', function(){ 116 | $(window).off("resize", onWindowResize); 117 | }); 118 | 119 | $(window).resize(onWindowResize); 120 | 121 | }, 0); 122 | }, errHandler); 123 | 124 | $scope.showConfig = false; 125 | $scope.isAdmin = window.isAdmin; 126 | 127 | $scope.services = Report.query(function(){ // for sidebar 128 | $scope.services.sort(function(a, b){ 129 | return a.status.last24Hours.uptime - b.status.last24Hours.uptime; }); 130 | }); 131 | 132 | }); 133 | 134 | })(); 135 | -------------------------------------------------------------------------------- /coverage/lcov-report/webserver/routes/web-route.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for webserver/routes/web-route.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for webserver/routes/web-route.js

17 |

18 | Statements: 94.12% (16 / 17)      19 | Branches: 100% (0 / 0)      20 | Functions: 66.67% (2 / 3)      21 | Lines: 94.12% (16 / 17)      22 | Ignored: none      23 |

24 |
All files » webserver/routes/ » web-route.js
25 |
26 |
27 |

 28 | 
113 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 8 36 | 9 37 | 10 38 | 11 39 | 12 40 | 13 41 | 14 42 | 15 43 | 16 44 | 17 45 | 18 46 | 19 47 | 20 48 | 21 49 | 22 50 | 23 51 | 24 52 | 25 53 | 26 54 | 27 55 | 28 56 | 291 57 | 1 58 |   59 | 1 60 |   61 | 3 62 |   63 | 1 64 |   65 |   66 |   67 |   68 |   69 | 3 70 | 45 71 | 45 72 | 45 73 | 45 74 |   75 |   76 | 3 77 | 3 78 | 3 79 | 3 80 | 3 81 |   82 | 3 83 |   84 |  
var config = require('../../config/web');
 85 | var express = require('express');
 86 |  
 87 | module.exports.getRoutes = function (){
 88 |  
 89 |   var router = express.Router();
 90 |  
 91 |   function serveIndex(req, res){
 92 |     res.render('index.html', {
 93 |       title: 'watchmen'
 94 |     });
 95 |   }
 96 |  
 97 |   router.all('*', function(req, res, next){
 98 |     res.locals.user = req.user;
 99 |     res.locals.baseUrl = config.baseUrl;
100 |     res.locals.ga_analytics_ID = config.ga_analytics_ID;
101 |     next();
102 |   });
103 |  
104 |   router.get('/services', serveIndex);
105 |   router.get('/services/:id/view', serveIndex);
106 |   router.get('/services/:id/edit', serveIndex);
107 |   router.get('/services/add', serveIndex);
108 |   router.get('/', serveIndex);
109 |  
110 |   return router;
111 | };
112 |  
114 | 115 |
116 | 119 | 120 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/utils.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/utils.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/utils.js

17 |

18 | Statements: 83.33% (5 / 6)      19 | Branches: 50% (1 / 2)      20 | Functions: 100% (3 / 3)      21 | Lines: 83.33% (5 / 6)      22 | Ignored: none      23 |

24 |
All files » lib/ » utils.js
25 |
26 |
27 |

 28 | 
119 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 8 36 | 9 37 | 10 38 | 11 39 | 12 40 | 13 41 | 14 42 | 15 43 | 16 44 | 17 45 | 18 46 | 19 47 | 20 48 | 21 49 | 22 50 | 23 51 | 24 52 | 25 53 | 26 54 | 27 55 | 28 56 | 29 57 | 30 58 | 311 59 |   60 | 1 61 |   62 |   63 |   64 |   65 |   66 |   67 |   68 |   69 |   70 | 92 71 |   72 |   73 | 92 74 |   75 |   76 |   77 |   78 |   79 |   80 |   81 |   82 |   83 |   84 | 1 85 |   86 |   87 |   88 |  
exports = module.exports = (function(){
 89 |  
 90 |   return {
 91 |  
 92 |     /**
 93 |      * Round number
 94 |      * @param number
 95 |      * @param decimals
 96 |      * @returns {number}
 97 |      */
 98 |  
 99 |     round: function (number, decimals) {
100 |       Iif (typeof decimals === 'undefined') {
101 |         decimals = 2;
102 |       }
103 |       return Math.round(number * Math.pow(10, decimals)) / Math.pow(10, decimals);
104 |     },
105 |  
106 |     /**
107 |      * Get random integer on the range specified
108 |      * @param min
109 |      * @param max
110 |      * @returns {Number} random integer
111 |      */
112 |  
113 |     getRandomInt: function (min, max) {
114 |       return Math.floor(Math.random() * (max - min)) + min;
115 |     }
116 |   }
117 |  
118 | })();
120 | 121 |
122 | 125 | 126 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /webserver/views/service-list.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | 8 |
9 |
10 |
11 | <% if (user){ %> 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | <% } else {%> 23 | 24 | <% } %> 25 |
26 | 27 | 28 | 29 | 30 | 32 | 33 | <% if ((user && user.isAdmin) || no_auth) { %> 34 | 53 | <% } %> 54 | 55 | 59 | 60 | 65 | 66 | 72 | 73 | 77 | 78 | 84 | 85 | 91 | 92 | 93 |
35 |
36 | 37 | 38 | 39 | 40 | 51 |
52 |
56 | offline 57 | online 58 | 61 | 62 | {{row.status.last24Hours.uptime}}% 63 | 64 | 79 | 80 | 81 | {{row.service.name}} 82 | 83 |
94 | 95 |
96 |
97 | -------------------------------------------------------------------------------- /webserver/routes/api-service-route.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | var express = require('express'); 3 | var debug = require('debug')('service-route'); 4 | var serviceValidator = require('./../../lib/service-validator'); 5 | var accessFilter = require('./../../lib/service-access-filter'); 6 | var config = require('../../config/web'); 7 | 8 | module.exports.getRoutes = function (storage) { 9 | 10 | if (!storage) { 11 | throw new Error('storage needed'); 12 | } 13 | 14 | var router = express.Router(); 15 | 16 | var requireAdmin = function (req, res, next) { 17 | if ((req.user && req.user.isAdmin) || config.no_auth) { 18 | next(); 19 | } else { 20 | return res.status(401).json({error: 'auth required'}); 21 | } 22 | }; 23 | 24 | /** 25 | * Add service 26 | */ 27 | 28 | router.post('/services', requireAdmin, function (req, res) { 29 | var service = req.body; 30 | 31 | var errors = serviceValidator.validate(service); 32 | if (errors.length) { 33 | return res.status(400).json({errors: errors}); 34 | } 35 | storage.addService(service, function (err, id) { 36 | if (err) { 37 | return res.status(500).json({error: err}); 38 | } 39 | return res.status(200).json({id: id}); 40 | }); 41 | }); 42 | 43 | /** 44 | * Delete service 45 | */ 46 | 47 | router.delete('/services/:id', requireAdmin, function (req, res) { 48 | var id = req.params.id; 49 | if (!id) { 50 | return res.status(404).json({error: 'ID parameter not found'}); 51 | } 52 | storage.getService(id, function (err, service) { 53 | if (err) { 54 | return res.status(500).json({error: err}); 55 | } 56 | 57 | if (!service) { 58 | return res.status(404).json({error: 'service not found'}); 59 | } 60 | 61 | storage.deleteService(id, function (err) { 62 | if (err) { 63 | return res.status(500).json({error: err}); 64 | } 65 | return res.json({id: id}); 66 | }); 67 | }); 68 | }); 69 | 70 | /** 71 | * Update service 72 | */ 73 | 74 | router.post('/services/:id', requireAdmin, function (req, res) { 75 | 76 | var id = req.params.id; 77 | if (!id) { 78 | return res.status(404).json({error: 'ID parameter not found'}); 79 | } 80 | 81 | var errors = serviceValidator.validate(req.body); 82 | if (errors.length) { 83 | return res.status(400).json({errors: errors}); 84 | } 85 | 86 | storage.getService(id, function (err, existingService) { 87 | if (err) { 88 | return res.status(500).json({error: err}); 89 | } 90 | 91 | if (!existingService) { 92 | return res.status(404).json({error: 'service not found'}); 93 | } 94 | 95 | storage.updateService(req.body, function (err, service) { 96 | if (err) { 97 | return res.status(500).json({error: err}); 98 | } 99 | return res.json(service); 100 | }); 101 | }); 102 | }); 103 | 104 | 105 | /** 106 | * Reset service data 107 | */ 108 | 109 | router.post('/services/:id/reset', requireAdmin, function (req, res) { 110 | var id = req.params.id; 111 | if (!id) { 112 | return res.status(404).json({error: 'ID parameter not found'}); 113 | } 114 | storage.getService(id, function (err, service) { 115 | if (err) { 116 | return res.status(500).json({error: err}); 117 | } 118 | 119 | if (!service) { 120 | return res.status(404).json({error: 'service not found'}); 121 | } 122 | 123 | storage.resetService(id, function (err) { 124 | if (err) { 125 | return res.status(500).json({error: err}); 126 | } 127 | return res.json({id: id}); 128 | }); 129 | }); 130 | }); 131 | 132 | /** 133 | * Load service 134 | */ 135 | 136 | router.get('/services/:id', requireAdmin, function (req, res) { 137 | if (!req.params.id) { 138 | return res.status(404).json({error: 'ID parameter not found'}); 139 | } 140 | storage.getService(req.params.id, function (err, service) { 141 | if (err) { 142 | console.error(err); 143 | return res.status(500).json({error: err}); 144 | } 145 | 146 | service = accessFilter.filterServices(service, req.user); 147 | 148 | if (!service) { 149 | return res.status(404).json({error: 'Service not found'}); 150 | } 151 | res.json(service); 152 | }); 153 | }); 154 | 155 | /** 156 | * Load services 157 | */ 158 | 159 | router.get('/services', requireAdmin, function (req, res) { 160 | storage.getServices({}, function (err, services) { 161 | if (err) { 162 | console.error(err); 163 | return res.status(500).json({error: err}); 164 | } 165 | // for small number of services (hundreds) we can go away with post database query filtering. 166 | // if you are managing thousands of services you may to index users/services for better performance 167 | res.json(accessFilter.filterServices(services, req.user)); 168 | }); 169 | }); 170 | 171 | return router; 172 | }; 173 | -------------------------------------------------------------------------------- /coverage/lcov-report/lib/storage/base.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for lib/storage/base.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for lib/storage/base.js

17 |

18 | Statements: 76.92% (10 / 13)      19 | Branches: 100% (0 / 0)      20 | Functions: 28.57% (2 / 7)      21 | Lines: 76.92% (10 / 13)      22 | Ignored: none      23 |

24 |
All files » lib/storage/ » base.js
25 |
26 |
27 |

 28 | 
116 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 8 36 | 9 37 | 10 38 | 11 39 | 12 40 | 13 41 | 14 42 | 15 43 | 16 44 | 17 45 | 18 46 | 19 47 | 20 48 | 21 49 | 22 50 | 23 51 | 24 52 | 25 53 | 26 54 | 27 55 | 28 56 | 29 57 | 301 58 |   59 | 1 60 | 67 61 |   62 |   63 | 1 64 |   65 |   66 |   67 | 1 68 | 3 69 |   70 |   71 |   72 |   73 |   74 | 1 75 |   76 |   77 |   78 | 1 79 |   80 |   81 |   82 | 1 83 |   84 |   85 |   86 | 1
function StorageBase (){ }
 87 |  
 88 | StorageBase.prototype._get_key = function (service, callback){
 89 |   return service.host.host + service.host.port + service.url;
 90 | };
 91 |  
 92 | StorageBase.prototype.get_status = function (service, callback){
 93 |   callback(null, null);
 94 | };
 95 |  
 96 | StorageBase.prototype.update_status = function (service, status, callback){
 97 |   callback(null);
 98 | };
 99 |  
100 | //----------------------------------------------
101 | // Reporting
102 | //----------------------------------------------
103 | StorageBase.prototype.report_all = function (callback){
104 |   callback(null, null);
105 | };
106 |  
107 | StorageBase.prototype.report_one = function (service, callback){
108 |   callback(null, null);
109 | };
110 |  
111 | StorageBase.prototype.quit = function (callback){
112 |  
113 | };
114 |  
115 | module.exports = StorageBase;
117 | 118 |
119 | 122 | 123 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /coverage/lcov-report/config/storage.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for config/storage.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for config/storage.js

17 |

18 | Statements: 100% (1 / 1)      19 | Branches: 100% (12 / 12)      20 | Functions: 100% (0 / 0)      21 | Lines: 100% (1 / 1)      22 | Ignored: none      23 |

24 |
All files » config/ » storage.js
25 |
26 |
27 |

 28 | 
134 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 8 36 | 9 37 | 10 38 | 11 39 | 12 40 | 13 41 | 14 42 | 15 43 | 16 44 | 17 45 | 18 46 | 19 47 | 20 48 | 21 49 | 22 50 | 23 51 | 24 52 | 25 53 | 26 54 | 27 55 | 28 56 | 29 57 | 30 58 | 31 59 | 32 60 | 33 61 | 34 62 | 35 63 | 361 64 |   65 |   66 |   67 |   68 |   69 |   70 |   71 |   72 |   73 |   74 |   75 |   76 |   77 |   78 |   79 |   80 |   81 |   82 |   83 |   84 |   85 |   86 |   87 |   88 |   89 |   90 |   91 |   92 |   93 |   94 |   95 |   96 |   97 |   98 |  
exports = module.exports = {
 99 |  
100 |   production: {
101 |     provider : 'redis',
102 |     options : {
103 |       'redis' : {
104 |         port: process.env.WATCHMEN_REDIS_PORT_PRODUCTION || 1216,
105 |         host: '127.0.0.1',
106 |         db: process.env.WATCHMEN_REDIS_DB_PRODUCTION || 1
107 |       }
108 |     }
109 |   },
110 |  
111 |   development: {
112 |     provider : 'redis',
113 |     options : {
114 |       'redis' : {
115 |         port: process.env.WATCHMEN_REDIS_PORT_DEVELOPMENT || 1216,
116 |         host: '127.0.0.1',
117 |         db: process.env.WATCHMEN_REDIS_DB_DEVELOPMENT || 2
118 |       }
119 |     }
120 |   },
121 |  
122 |   test: {
123 |     provider : 'redis',
124 |     options : {
125 |       'redis' : {
126 |         port: process.env.WATCHMEN_REDIS_PORT_TEST || 6666,
127 |         host: '127.0.0.1',
128 |         db: process.env.WATCHMEN_REDIS_DB_TEST || 1
129 |       }
130 |     }
131 |   }
132 |  
133 | };
135 | 136 |
137 | 140 | 141 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /test/test-service-validation.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var serviceValidator = require('../lib/service-validator'); 3 | 4 | var service; 5 | 6 | describe('service validator', function () { 7 | 8 | beforeEach(function () { 9 | service = { 10 | name: 'my service', 11 | interval: 60 * 1000, 12 | failureInterval: 20 * 1000, 13 | url: 'http://apple.com', 14 | port: 443, 15 | timeout: 10000, 16 | warningThreshold: 3000, 17 | pingServiceName: 'http', 18 | restrictedTo: 'user@domain.com, admin@domain.com' 19 | }; 20 | }); 21 | 22 | it('should have an "name" field', function () { 23 | checkNonEmpty('name'); 24 | }); 25 | 26 | it('should have an "url" field', function () { 27 | checkNonEmpty('url'); 28 | }); 29 | 30 | it('should have a "pingServiceName" field', function () { 31 | checkNonEmpty('pingServiceName'); 32 | }); 33 | 34 | describe('"interval" field', function () { 35 | it('should exist', function () { 36 | checkNonEmpty('interval'); 37 | }); 38 | 39 | it('should have a minimum value of 500ms', function () { 40 | service.interval = 400; 41 | var errors = serviceValidator.validate(service); 42 | assert.equal(errors.length, 1); 43 | assert.equal(errors[0].field, 'interval'); 44 | }); 45 | }); 46 | 47 | describe('"failureInterval" field', function () { 48 | it('should exist', function () { 49 | checkNonEmpty('failureInterval'); 50 | }); 51 | 52 | it('should have a minimum value of 500ms', function () { 53 | service.failureInterval = 400; 54 | var errors = serviceValidator.validate(service); 55 | assert.equal(errors.length, 1); 56 | assert.equal(errors[0].field, 'failureInterval'); 57 | }); 58 | }); 59 | 60 | describe('"port" field', function () { 61 | it('should exist', function () { 62 | checkNonEmpty('port'); 63 | }); 64 | 65 | it('should have a numeric value', function () { 66 | checkIntField('3434invalidnumber', 'port'); 67 | }); 68 | 69 | it('should reject negative values', function () { 70 | checkIntField(-1, 'port'); 71 | }); 72 | }); 73 | 74 | describe('"failuresToBeOutage" field', function () { 75 | it('should have a numeric value', function () { 76 | checkIntField('3434invalidnumber', 'failuresToBeOutage'); 77 | }); 78 | 79 | it('should reject negative values', function () { 80 | checkIntField(-2, 'failuresToBeOutage'); 81 | }); 82 | }); 83 | 84 | describe('"timeout" field', function () { 85 | it('should exist', function () { 86 | checkNonEmpty('timeout'); 87 | }); 88 | 89 | it('should have a numeric value', function () { 90 | checkIntField('3434invalidnumber', 'timeout'); 91 | }); 92 | 93 | it('should not be negative', function () { 94 | checkIntField(-1000, 'timeout'); 95 | }); 96 | }); 97 | 98 | describe('"warningThreshold" field', function () { 99 | it('should exist', function () { 100 | checkNonEmpty('warningThreshold'); 101 | }); 102 | 103 | it('should have a numeric value', function () { 104 | checkIntField('3434invalidnumber', 'warningThreshold'); 105 | }); 106 | }); 107 | 108 | describe('"restrictedTo" field', function () { 109 | it('should reject invalid emails', function () { 110 | service.restrictedTo = 'invalidemail, user@domain.com'; 111 | var errors = serviceValidator.validate(service); 112 | assert.equal(errors.length, 1, errors); 113 | assert.equal(errors[0].field, 'restrictedTo'); 114 | }); 115 | 116 | it('should reject empty values', function () { 117 | service.restrictedTo = 'admin@domain.com, , user@domain.com'; 118 | var errors = serviceValidator.validate(service); 119 | assert.equal(errors.length, 1, errors); 120 | assert.equal(errors[0].field, 'restrictedTo'); 121 | }); 122 | 123 | it('should validate if input is correct', function () { 124 | service.restrictedTo = 'admin@domain.com, user@domain.com'; 125 | var errors = serviceValidator.validate(service); 126 | assert.equal(errors.length, 0, errors); 127 | }); 128 | }); 129 | 130 | describe('"alertTo" field', function () { 131 | it('should reject invalid emails', function () { 132 | service.alertTo = 'invalidemail, user@domain.com'; 133 | var errors = serviceValidator.validate(service); 134 | assert.equal(errors.length, 1, errors); 135 | assert.equal(errors[0].field, 'alertTo'); 136 | }); 137 | 138 | it('should reject empty values', function () { 139 | service.alertTo = 'admin@domain.com, , user@domain.com'; 140 | var errors = serviceValidator.validate(service); 141 | assert.equal(errors.length, 1, errors); 142 | assert.equal(errors[0].field, 'alertTo'); 143 | }); 144 | 145 | it('should validate if input is correct', function () { 146 | service.alertTo = 'admin@domain.com, user@domain.com'; 147 | var errors = serviceValidator.validate(service); 148 | assert.equal(errors.length, 0, errors); 149 | }); 150 | }); 151 | 152 | function checkNonEmpty(field) { 153 | delete service[field]; 154 | var errors = serviceValidator.validate(service); 155 | assert.equal(errors.length, 1, errors); 156 | assert.equal(errors[0].field, field); 157 | } 158 | 159 | function checkIntField(val, field) { 160 | service[field] = val; 161 | var errors = serviceValidator.validate(service); 162 | assert.equal(errors.length, 1); 163 | assert.equal(errors[0].field, field); 164 | } 165 | }); 166 | -------------------------------------------------------------------------------- /coverage/lcov-report/config/notifications/notifications.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for config/notifications/notifications.js 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |

Code coverage report for config/notifications/notifications.js

17 |

18 | Statements: 100% (2 / 2)      19 | Branches: 100% (0 / 0)      20 | Functions: 100% (1 / 1)      21 | Lines: 100% (2 / 2)      22 | Ignored: none      23 |

24 |
All files » config/notifications/ » notifications.js
25 |
26 |
27 |

 28 | 
131 | 
1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 7 35 | 8 36 | 9 37 | 10 38 | 11 39 | 12 40 | 13 41 | 14 42 | 15 43 | 16 44 | 17 45 | 18 46 | 19 47 | 20 48 | 21 49 | 22 50 | 23 51 | 24 52 | 25 53 | 26 54 | 27 55 | 28 56 | 29 57 | 30 58 | 31 59 | 32 60 | 33 61 | 34 62 | 351 63 |   64 |   65 |   66 |   67 |   68 | 1 69 |   70 |   71 |   72 |   73 |   74 |   75 |   76 |   77 |   78 |   79 |   80 |   81 |   82 |   83 |   84 |   85 |   86 |   87 |   88 |   89 |   90 |   91 |   92 |   93 |   94 |   95 |   96 |  
exports = module.exports = (function(){
 97 |  
 98 |     /**
 99 |      * Notifications configuration file.
100 |      */
101 |  
102 |     return  {
103 |  
104 |         /**
105 |          * Enable the services you would like to use when an alert is triggered
106 |          *
107 |          * How to create a new notification service:
108 |          * - 1) Create a new service entry in this file
109 |          * - 2) Create the actual service file in the 'path' path
110 |          * - 3) Setup some default configuration file in the 'config' path
111 |          */
112 |  
113 |         services: [
114 |             {
115 |                 enabled: process.env.WATCHMEN_NOTIFICATIONS_AWS_SES_ENABLED,
116 |                 name : 'aws-ses',
117 |                 path: './services/aws-ses/aws-ses',
118 |                 config: '../../config/notifications/services/aws-ses'
119 |             },
120 |             {
121 |                 enabled: process.env.WATCHMEN_NOTIFICATIONS_POSTMARK_ENABLED,
122 |                 name : 'aws-ses',
123 |                 path: './services/postmark/postmark',
124 |                 config: '../../config/notifications/services/postmark'
125 |             }
126 |         ],
127 |  
128 |         alwaysAlertTo: process.env.WATCHMEN_NOTIFICATIONS_ALWAYS_ALERT_TO // comma separated emails
129 |     }
130 | })();
132 | 133 |
134 | 137 | 138 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /coverage/lcov-report/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; padding: 0; 3 | } 4 | body { 5 | font-family: Helvetica Neue, Helvetica,Arial; 6 | font-size: 10pt; 7 | } 8 | div.header, div.footer { 9 | background: #eee; 10 | padding: 1em; 11 | } 12 | div.header { 13 | z-index: 100; 14 | position: fixed; 15 | top: 0; 16 | border-bottom: 1px solid #666; 17 | width: 100%; 18 | } 19 | div.footer { 20 | border-top: 1px solid #666; 21 | } 22 | div.body { 23 | margin-top: 10em; 24 | } 25 | div.meta { 26 | font-size: 90%; 27 | text-align: center; 28 | } 29 | h1, h2, h3 { 30 | font-weight: normal; 31 | } 32 | h1 { 33 | font-size: 12pt; 34 | } 35 | h2 { 36 | font-size: 10pt; 37 | } 38 | pre { 39 | font-family: Consolas, Menlo, Monaco, monospace; 40 | margin: 0; 41 | padding: 0; 42 | line-height: 14px; 43 | font-size: 14px; 44 | -moz-tab-size: 2; 45 | -o-tab-size: 2; 46 | tab-size: 2; 47 | } 48 | 49 | div.path { font-size: 110%; } 50 | div.path a:link, div.path a:visited { color: #000; } 51 | table.coverage { border-collapse: collapse; margin:0; padding: 0 } 52 | 53 | table.coverage td { 54 | margin: 0; 55 | padding: 0; 56 | color: #111; 57 | vertical-align: top; 58 | } 59 | table.coverage td.line-count { 60 | width: 50px; 61 | text-align: right; 62 | padding-right: 5px; 63 | } 64 | table.coverage td.line-coverage { 65 | color: #777 !important; 66 | text-align: right; 67 | border-left: 1px solid #666; 68 | border-right: 1px solid #666; 69 | } 70 | 71 | table.coverage td.text { 72 | } 73 | 74 | table.coverage td span.cline-any { 75 | display: inline-block; 76 | padding: 0 5px; 77 | width: 40px; 78 | } 79 | table.coverage td span.cline-neutral { 80 | background: #eee; 81 | } 82 | table.coverage td span.cline-yes { 83 | background: #b5d592; 84 | color: #999; 85 | } 86 | table.coverage td span.cline-no { 87 | background: #fc8c84; 88 | } 89 | 90 | .cstat-yes { color: #111; } 91 | .cstat-no { background: #fc8c84; color: #111; } 92 | .fstat-no { background: #ffc520; color: #111 !important; } 93 | .cbranch-no { background: yellow !important; color: #111; } 94 | 95 | .cstat-skip { background: #ddd; color: #111; } 96 | .fstat-skip { background: #ddd; color: #111 !important; } 97 | .cbranch-skip { background: #ddd !important; color: #111; } 98 | 99 | .missing-if-branch { 100 | display: inline-block; 101 | margin-right: 10px; 102 | position: relative; 103 | padding: 0 4px; 104 | background: black; 105 | color: yellow; 106 | } 107 | 108 | .skip-if-branch { 109 | display: none; 110 | margin-right: 10px; 111 | position: relative; 112 | padding: 0 4px; 113 | background: #ccc; 114 | color: white; 115 | } 116 | 117 | .missing-if-branch .typ, .skip-if-branch .typ { 118 | color: inherit !important; 119 | } 120 | 121 | .entity, .metric { font-weight: bold; } 122 | .metric { display: inline-block; border: 1px solid #333; padding: 0.3em; background: white; } 123 | .metric small { font-size: 80%; font-weight: normal; color: #666; } 124 | 125 | div.coverage-summary table { border-collapse: collapse; margin: 3em; font-size: 110%; } 126 | div.coverage-summary td, div.coverage-summary table th { margin: 0; padding: 0.25em 1em; border-top: 1px solid #666; border-bottom: 1px solid #666; } 127 | div.coverage-summary th { text-align: left; border: 1px solid #666; background: #eee; font-weight: normal; } 128 | div.coverage-summary th.file { border-right: none !important; } 129 | div.coverage-summary th.pic { border-left: none !important; text-align: right; } 130 | div.coverage-summary th.pct { border-right: none !important; } 131 | div.coverage-summary th.abs { border-left: none !important; text-align: right; } 132 | div.coverage-summary td.pct { text-align: right; border-left: 1px solid #666; } 133 | div.coverage-summary td.abs { text-align: right; font-size: 90%; color: #444; border-right: 1px solid #666; } 134 | div.coverage-summary td.file { border-left: 1px solid #666; white-space: nowrap; } 135 | div.coverage-summary td.pic { min-width: 120px !important; } 136 | div.coverage-summary a:link { text-decoration: none; color: #000; } 137 | div.coverage-summary a:visited { text-decoration: none; color: #777; } 138 | div.coverage-summary a:hover { text-decoration: underline; } 139 | div.coverage-summary tfoot td { border-top: 1px solid #666; } 140 | 141 | div.coverage-summary .sorter { 142 | height: 10px; 143 | width: 7px; 144 | display: inline-block; 145 | margin-left: 0.5em; 146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 | } 148 | div.coverage-summary .sorted .sorter { 149 | background-position: 0 -20px; 150 | } 151 | div.coverage-summary .sorted-desc .sorter { 152 | background-position: 0 -10px; 153 | } 154 | 155 | .high { background: #b5d592 !important; } 156 | .medium { background: #ffe87c !important; } 157 | .low { background: #fc8c84 !important; } 158 | 159 | span.cover-fill, span.cover-empty { 160 | display:inline-block; 161 | border:1px solid #444; 162 | background: white; 163 | height: 12px; 164 | } 165 | span.cover-fill { 166 | background: #ccc; 167 | border-right: 1px solid #444; 168 | } 169 | span.cover-empty { 170 | background: white; 171 | border-left: none; 172 | } 173 | span.cover-full { 174 | border-right: none !important; 175 | } 176 | pre.prettyprint { 177 | border: none !important; 178 | padding: 0 !important; 179 | margin: 0 !important; 180 | } 181 | .com { color: #999 !important; } 182 | .ignore-none { color: #999; font-weight: normal; } 183 | -------------------------------------------------------------------------------- /coverage/lcov-report/sorter.js: -------------------------------------------------------------------------------- 1 | var addSorting = (function () { 2 | "use strict"; 3 | var cols, 4 | currentSort = { 5 | index: 0, 6 | desc: false 7 | }; 8 | 9 | // returns the summary table element 10 | function getTable() { return document.querySelector('.coverage-summary table'); } 11 | // returns the thead element of the summary table 12 | function getTableHeader() { return getTable().querySelector('thead tr'); } 13 | // returns the tbody element of the summary table 14 | function getTableBody() { return getTable().querySelector('tbody'); } 15 | // returns the th element for nth column 16 | function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; } 17 | 18 | // loads all columns 19 | function loadColumns() { 20 | var colNodes = getTableHeader().querySelectorAll('th'), 21 | colNode, 22 | cols = [], 23 | col, 24 | i; 25 | 26 | for (i = 0; i < colNodes.length; i += 1) { 27 | colNode = colNodes[i]; 28 | col = { 29 | key: colNode.getAttribute('data-col'), 30 | sortable: !colNode.getAttribute('data-nosort'), 31 | type: colNode.getAttribute('data-type') || 'string' 32 | }; 33 | cols.push(col); 34 | if (col.sortable) { 35 | col.defaultDescSort = col.type === 'number'; 36 | colNode.innerHTML = colNode.innerHTML + ''; 37 | } 38 | } 39 | return cols; 40 | } 41 | // attaches a data attribute to every tr element with an object 42 | // of data values keyed by column name 43 | function loadRowData(tableRow) { 44 | var tableCols = tableRow.querySelectorAll('td'), 45 | colNode, 46 | col, 47 | data = {}, 48 | i, 49 | val; 50 | for (i = 0; i < tableCols.length; i += 1) { 51 | colNode = tableCols[i]; 52 | col = cols[i]; 53 | val = colNode.getAttribute('data-value'); 54 | if (col.type === 'number') { 55 | val = Number(val); 56 | } 57 | data[col.key] = val; 58 | } 59 | return data; 60 | } 61 | // loads all row data 62 | function loadData() { 63 | var rows = getTableBody().querySelectorAll('tr'), 64 | i; 65 | 66 | for (i = 0; i < rows.length; i += 1) { 67 | rows[i].data = loadRowData(rows[i]); 68 | } 69 | } 70 | // sorts the table using the data for the ith column 71 | function sortByIndex(index, desc) { 72 | var key = cols[index].key, 73 | sorter = function (a, b) { 74 | a = a.data[key]; 75 | b = b.data[key]; 76 | return a < b ? -1 : a > b ? 1 : 0; 77 | }, 78 | finalSorter = sorter, 79 | tableBody = document.querySelector('.coverage-summary tbody'), 80 | rowNodes = tableBody.querySelectorAll('tr'), 81 | rows = [], 82 | i; 83 | 84 | if (desc) { 85 | finalSorter = function (a, b) { 86 | return -1 * sorter(a, b); 87 | }; 88 | } 89 | 90 | for (i = 0; i < rowNodes.length; i += 1) { 91 | rows.push(rowNodes[i]); 92 | tableBody.removeChild(rowNodes[i]); 93 | } 94 | 95 | rows.sort(finalSorter); 96 | 97 | for (i = 0; i < rows.length; i += 1) { 98 | tableBody.appendChild(rows[i]); 99 | } 100 | } 101 | // removes sort indicators for current column being sorted 102 | function removeSortIndicators() { 103 | var col = getNthColumn(currentSort.index), 104 | cls = col.className; 105 | 106 | cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); 107 | col.className = cls; 108 | } 109 | // adds sort indicators for current column being sorted 110 | function addSortIndicators() { 111 | getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted'; 112 | } 113 | // adds event listeners for all sorter widgets 114 | function enableUI() { 115 | var i, 116 | el, 117 | ithSorter = function ithSorter(i) { 118 | var col = cols[i]; 119 | 120 | return function () { 121 | var desc = col.defaultDescSort; 122 | 123 | if (currentSort.index === i) { 124 | desc = !currentSort.desc; 125 | } 126 | sortByIndex(i, desc); 127 | removeSortIndicators(); 128 | currentSort.index = i; 129 | currentSort.desc = desc; 130 | addSortIndicators(); 131 | }; 132 | }; 133 | for (i =0 ; i < cols.length; i += 1) { 134 | if (cols[i].sortable) { 135 | el = getNthColumn(i).querySelector('.sorter'); 136 | if (el.addEventListener) { 137 | el.addEventListener('click', ithSorter(i)); 138 | } else { 139 | el.attachEvent('onclick', ithSorter(i)); 140 | } 141 | } 142 | } 143 | } 144 | // adds sorting functionality to the UI 145 | return function () { 146 | if (!getTable()) { 147 | return; 148 | } 149 | cols = loadColumns(); 150 | loadData(cols); 151 | addSortIndicators(); 152 | enableUI(); 153 | }; 154 | })(); 155 | 156 | window.addEventListener('load', addSorting); 157 | --------------------------------------------------------------------------------