From 5d614eb20b62ba5b616f67aeffddda1bfd5575de Mon Sep 17 00:00:00 2001 From: Phara0h Date: Fri, 30 Oct 2020 15:22:57 -0400 Subject: [PATCH] Added endpoints, Added postman docs, Fixed bugs and more! --- .eslintrc.json | 139 ++++++++++++++++++++ .gitignore | 2 + .prettierrc.js | 4 + README.nbs | 121 +++++++++++++++++- changelog-template.hbs | 2 - package.json | 22 +++- src/alerts.js | 201 ++++++++++++++++++----------- src/config.js | 60 ++++++--- src/health-check.js | 233 ++++++++++++++++++++++------------ src/index.js | 39 +++--- src/routes/v1/alerter.js | 21 +++- src/routes/v1/config.js | 16 +++ src/routes/v1/index.js | 3 +- src/routes/v1/service.js | 26 +++- test/server/test-server.js | 83 ++++++++---- test/sky-puppy-config.json | 252 ++++++++++++++++++------------------- 16 files changed, 853 insertions(+), 371 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .prettierrc.js create mode 100644 src/routes/v1/config.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..97fc4af --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,139 @@ +{ + "parserOptions": { + "ecmaVersion": 9 + }, + "ignorePatterns": ["**/sdk/**", "**/gitlab-ci/**"], + "extends": ["prettier"], + "plugins": ["prettier"], + "env": { + "node": true, + "jest": true + }, + "rules": { + "comma-dangle": ["error", "never"], + "no-cond-assign": 2, + "no-constant-condition": 2, + "no-control-regex": 2, + "no-debugger": 2, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty-character-class": 2, + "no-extra-semi": 2, + "no-func-assign": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-negated-in-lhs": 2, + "no-obj-calls": 2, + "no-unreachable": 2, + "use-isnan": 2, + "no-unexpected-multiline": 2, + "curly": 2, + "dot-location": [2, "property"], + "no-alert": 2, + "no-caller": 1, + "no-eval": 1, + "no-multi-spaces": 2, + "no-unused-vars": [ + 2, + { + "vars": "all", + "args": "none" + } + ], + "array-bracket-spacing": [2, "never"], + "block-spacing": [2, "never"], + "brace-style": [ + 2, + "1tbs", + { + "allowSingleLine": true + } + ], + "camelcase": [ + "error", + { + "properties": "never" + } + ], + "comma-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "comma-style": [2, "last"], + "computed-property-spacing": [2, "never"], + "consistent-this": [2, "self"], + "eol-last": 2, + "func-style": [2, "declaration", { "allowArrowFunctions": true }], + "indent": [ + 2, + 2, + { + "SwitchCase": 1 + } + ], + "key-spacing": [ + 2, + { + "beforeColon": false, + "afterColon": true + } + ], + "keyword-spacing": 2, + "lines-around-comment": [ + 2, + { + "beforeBlockComment": true, + "beforeLineComment": false, + "allowBlockStart": true + } + ], + "linebreak-style": [2, "unix"], + "new-cap": 2, + "new-parens": 2, + "newline-after-var": [2, "always"], + "no-array-constructor": 2, + "no-inline-comments": 2, + "no-lonely-if": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": [ + 2, + { + "max": 1 + } + ], + "no-nested-ternary": 0, + "no-new-object": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "object-curly-spacing": [2, "always"], + "one-var": [2, "never"], + "operator-linebreak": [2, "after"], + "quote-props": [2, "as-needed"], + "quotes": [2, "single", "avoid-escape"], + "semi-spacing": 2, + "semi": [2, "always"], + "sort-vars": 2, + "space-before-blocks": 2, + "space-before-function-paren": [ + 2, + { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + } + ], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [ + 2, + { + "words": true, + "nonwords": false + } + ] + } +} diff --git a/.gitignore b/.gitignore index ba95b20..e4798f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ package-lock.json node_modules .DS_Store +!test/sky-puppy-config.json +sky-puppy-config.json diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..d5fd470 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,4 @@ +module.exports = { + tabWidth: 2, + singleQuote: true, +}; diff --git a/README.nbs b/README.nbs index add33dd..59e30ef 100644 --- a/README.nbs +++ b/README.nbs @@ -1,5 +1,124 @@ # Sky Puppy -A easy to use reliable health checking service with Prometheus export +A easy to use reliable endpoint coordinator / health checking service with Prometheus export +[Sky Puppy Postman Collection](https://documenter.getpostman.com/view/208035/TVYKawgU) + +## Install + +```bash +npm install -g sky-puppy +``` + +## Run + +```bash +sky-puppy +``` + +## Sample Config + +Sky Puppy looks for a file called `sky-puppy-config.json` in the folder it is ran at. + +```json +{ + "alerters": { + "discord_down": { + "uri": "http://discord.com", + "json": true, + "method": "PUT", + "body": { + "embeds": [ + { + "title": "{{service_name}} is {{alert_type}}!", + "description": "This service was healthy for {{last_healthy_total_duration}} seconds!", + "color": 14828098, + "footer": { + "text": "" + }, + "timestamp": "{{timestamp}}" + } + ], + "username": "Sky Puppy", + "avatar_url": "https://i.imgur.com/J5vIVSt.png" + } + }, + "discord_unhealthy": { + "uri": "http://discord.com", + "json": true, + "method": "PUT", + "body": { + "embeds": [ + { + "title": "{{service_name}} is {{alert_type}}!", + "description": "This service was healthy for {{last_healthy_total_duration}} seconds!", + "color": 14852674, + "footer": { + "text": "" + }, + "timestamp": "{{timestamp}}" + } + ], + "username": "Sky Puppy", + "avatar_url": "https://i.imgur.com/J5vIVSt.png" + } + }, + "discord_healthy": { + "uri": "http://discord.com", + "json": true, + "method": "PUT", + "body": { + "embeds": [ + { + "title": "{{service_name}} is {{alert_type}}!", + "description": "Carry on, looks like things are back! We were down for {{last_unhealthy_total_duration}} seconds.", + "color": 6480450, + "footer": { + "text": "" + }, + "timestamp": "{{timestamp}}" + } + ], + "username": "Sky Puppy", + "avatar_url": "https://i.imgur.com/3rfFeOu.png" + } + } + }, + "services": { + "your_service": { + "interval": 3, + "start_delay": 2, + "request": { + "uri": "http://127.0.0.1/a/cool/endpoint", + "timeout": 2, + "json": true, + "method": "PUT", + "body": { + "test": "sweet" + }, + }, + "alerts": [ + { + "type": "down", + "alerter": "discord_down" + }, + { + "type": "unhealthy", + "alerter": "discord_unhealthy" + }, + { + "type": "healthy", + "alerter": "discord_healthy" + } + ] + } + }, + "skypuppy": { + "version": "1.0.0" + } +} + +``` + +## Changelog {{doc1}} diff --git a/changelog-template.hbs b/changelog-template.hbs index 20ff950..26ea3eb 100644 --- a/changelog-template.hbs +++ b/changelog-template.hbs @@ -1,5 +1,3 @@ -### Changelog - All notable changes to this project will be documented in this file. Dates are displayed in UTC. {{#each releases}} diff --git a/package.json b/package.json index b2887a2..3d6ad7e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "type": "git", "url": "git+https://github.com/Phara0h/sky-puppy.git" }, + "bin": { + "sky-puppy": "./src/index.js" + }, "keywords": [ "prometheus", "health", @@ -27,13 +30,20 @@ }, "homepage": "https://github.com/Phara0h/sky-puppy#readme", "dependencies": { - "auto-changelog": "^2.2.0", - "fasquest": "^2.4.0", - "fastify": "^2.15.3", + "fasquest": "^3.0.1", + "fastify": "^3.7.0", + "nbars": "^1.0.1", + "nstats": "^4.1.2" + }, + "devDependencies": { + "eslint": "^7.8.1", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-prettier": "^3.1.4", + "prettier": "^2.0.5", + "prettier-eslint": "^11.0.0", "jsdoc-to-markdown": "^6.0.1", + "auto-changelog": "^2.2.0", "mdsquash": "^1.0.5", - "nbars": "^1.0.1", - "nstats": "^2.1.5", - "postgen": "^4.1.5" + "postgen": "^4.6.0" } } diff --git a/src/alerts.js b/src/alerts.js index a2dff60..0c01b4e 100644 --- a/src/alerts.js +++ b/src/alerts.js @@ -2,8 +2,9 @@ const fs = require('fs'); var fasquest = require('fasquest'); const client = { https: require('https'), - http: require('http'), + http: require('http') }; + fasquest.agent = { http: new client.http.Agent({ keepAlive: false @@ -13,129 +14,179 @@ fasquest.agent = { }) }; class Alerts { - constructor(stats,config,nbars) { - + constructor(stats, config, nbars) { this.nbars = nbars; - this.alerters = {} - this.alerts_status = {} + this.alerters = {}; + this.alerts_status = {}; this.alerts = { down: {}, healthy: {}, unhealthy: {}, unhealthy_status: {}, unhealthy_response_time: {} - } + }; this.stats = stats; this.config = config; var alertersKeys = Object.keys(this.config.alerters); for (var i = 0; i < alertersKeys.length; i++) { - this.addAlerter(alertersKeys[i],{...this.config.getAlerter(alertersKeys[i])}); + this.startAlerter(alertersKeys[i], { + ...this.config.getAlerter(alertersKeys[i]) + }); } - } - addAlerter(name,alerter) { - if(!this.config.getAlerter(name)) { - this.config.addAlerter(name,alerter); - } - - var nAlerter = { - name, - request: { - ...alerter - } + addAlerter(name, alerter) { + if (this.alerters[name]) { + this.editAlerter(name, alerter); + } else { + console.log(`Adding alerter ${name} ...`); + this.config.addAlerter(name, alerter); + this.startAlerter(name, alerter); } + } - nAlerter.request.method = nAlerter.request.method || 'GET'; - nAlerter.request.timeout = Math.round((nAlerter.request.timeout || 60)*1000); - nAlerter.request.resolveWithFullResponse = true; - nAlerter.request.simple = false; + getAlerter(name) { + return this.config.getAlerter(name); + } + deleteAlerter(name) { + if (this.alerters[name]) { + console.log(`Deleting alerter ${name} ...`); - if(!nAlerter.request.headers) { - nAlerter.request.headers = {}; + delete this.alerters[name]; + this.config.deleteAlerter(name); } - nAlerter.request.headers['User-Agent'] = `Sky-Puppy / ${this.config.skypuppy.version} (Health Check Service)` - this.alerters[name] = nAlerter; + return false; } - async _checkStatus(alert,service,type) { - if(service.status.up === type) { + editAlerter(name, alerter) { + console.log(`Updating alerter ${name} ...`); + this.deleteAlerter(name); + this.addAlerter(name, alerter); + } - if(this.alerts_status[service.name] != null && this.alerts_status[service.name] === type) { + startAlerter(name, alerter) { + if (!this.alerters[name]) { + var nAlerter = { + name, + request: { + ...alerter + } + }; + + nAlerter.request.method = nAlerter.request.method || 'GET'; + nAlerter.request.timeout = Math.round( + (nAlerter.request.timeout || 60) * 1000 + ); + nAlerter.request.resolveWithFullResponse = true; + nAlerter.request.simple = false; + + if (!nAlerter.request.headers) { + nAlerter.request.headers = {}; + } + nAlerter.request.headers[ + 'User-Agent' + ] = `Sky-Puppy / ${this.config.skypuppy.version} (Health Check Service)`; + + this.alerters[name] = nAlerter; + } + } + + async _checkStatus(alert, service, type) { + if (service.status.up === type) { + if ( + this.alerts_status[service.name] != null && + this.alerts_status[service.name] === type + ) { return; } // check if status has changed - if(!this.alerts[alert.type][service.name]) { - this.alerts[alert.type][service.name] = {status:{...service.status,count: {...service.status.count}}} + if (!this.alerts[alert.type][service.name]) { + this.alerts[alert.type][service.name] = { + status: { ...service.status, count: { ...service.status.count } } + }; this.alerts[alert.type][service.name].status.count[alert.type]--; } - if((service.status.count[alert.type] - this.alerts[alert.type][service.name].status.count[alert.type]) >= (alert.for || 1) && !this.alerts[alert.type][service.name].alerted) { + if ( + service.status.count[alert.type] - + this.alerts[alert.type][service.name].status.count[alert.type] >= + (alert.for || 1) && + !this.alerts[alert.type][service.name].alerted + ) { this.alerts[alert.type][service.name].alerted = true; - await this._send_alert(alert,service); - return; + await this._send_alert(alert, service); + return; } - } else { this.alerts[alert.type][service.name] = null; } } - - async alert(service) { - if(service.config.alerts) { + if (service.config.alerts) { for (var i = 0; i < service.config.alerts.length; i++) { - switch(service.config.alerts[i].type) { - case "down": - await this._checkStatus(service.config.alerts[i],service,-1); - break; - case "unhealthy": - case "unhealthy_status": - case "unhealthy_response_time": - await this._checkStatus(service.config.alerts[i],service,0); - break; - case "healthy": - await this._checkStatus(service.config.alerts[i],service,1); - break; + switch (service.config.alerts[i].type) { + case 'down': + await this._checkStatus(service.config.alerts[i], service, -1); + break; + case 'unhealthy': + case 'unhealthy_status': + case 'unhealthy_response_time': + await this._checkStatus(service.config.alerts[i], service, 0); + break; + case 'healthy': + await this._checkStatus(service.config.alerts[i], service, 1); + break; } } } } - async _send_alert(alert,service) { - var overrides = alert.overrides || {}; - var request = {...this.alerters[alert.alerter].request, ...overrides} - if(request.body) { - request.body = this.nbars.transform( - request.json ? JSON.stringify(request.body) : request.body, - { - alert_type: alert.type, - service_name: service.name, - timestamp: new Date().toISOString() , - last_unhealthy_total_duration: service.status.last_unhealthy_total_duration || 'Unknown', - last_healthy_total_duration: service.status.last_healthy ? (Number(process.hrtime.bigint() - service.status.last_healthy) / 1000000000).toFixed(2) : 'Unknown' + async _send_alert(alert, service) { + if (this.alerters[alert.alerter]) { + var overrides = alert.overrides || {}; + var request = { ...this.alerters[alert.alerter].request, ...overrides }; + + if (request.body) { + request.body = this.nbars.transform( + request.json ? JSON.stringify(request.body) : request.body, + { + alert_type: alert.type, + service_name: service.name, + timestamp: new Date().toISOString(), + last_unhealthy_total_duration: + service.status.last_unhealthy_total_duration || 'Unknown', + last_healthy_total_duration: service.status.last_healthy + ? ( + Number( + process.hrtime.bigint() - service.status.last_healthy + ) / 1000000000 + ).toFixed(2) + : 'Unknown' + } + ); + + if (request.json) { + request.body = JSON.parse(request.body); } - ) - - if(request.json) { - request.body = JSON.parse(request.body) } - } - //console.log(request.body) - this.alerts_status[service.name] = service.status.up + //console.log(request.body) + this.alerts_status[service.name] = service.status.up; - try { - await fasquest.request(JSON.parse(JSON.stringify(request))); - } catch (e) { - console.log(e) + try { + await fasquest.request(JSON.parse(JSON.stringify(request))); + } catch (e) { + console.log(e); + } + } else { + console.warn( + `WARNING: Alerter [${alert.alerter}] of type [${alert.type}] does not exist, but service [${service.name}] is trying to use it!` + ); } - } - } module.exports = Alerts; diff --git a/src/config.js b/src/config.js index 566073b..4a201c5 100644 --- a/src/config.js +++ b/src/config.js @@ -1,32 +1,38 @@ const fs = require('fs'); var path = require('path'); + class Config { - constructor(config) { - this.version = require(path.dirname(require.main.filename)+'/../package.json').version; - process.title = "Sky Puppy v"+this.version; + constructor() { + this.version = require(path.dirname(require.main.filename) + + '/../package.json').version; + process.title = 'Sky Puppy v' + this.version; try { - this.path = path.resolve(process.env.SKY_PUPPY_CONFIG_PATH || './') + '/sky-puppy-config.json'; + this.path = + path.resolve(process.env.SKY_PUPPY_CONFIG_PATH || './') + + '/sky-puppy-config.json'; this.settings = JSON.parse(fs.readFileSync(this.path)); } catch (e) { this.path = path.dirname('./') + '/sky-puppy-config.json'; this.settings = {}; - console.log('Error loading config. Creating and using default one at ' + this.path); + console.log( + 'Error loading config. Creating and using default one at ' + this.path + ); this.saveConfig(); } - if(!this.alerters) { + if (!this.alerters) { this.settings.alerters = {}; } - if(!this.services) { + if (!this.services) { this.settings.services = {}; } this.settings.skypuppy = this.settings.skypuppy || { version: this.version - } + }; } - addService(name,service) { + addService(name, service) { try { this.services[name] = service; this.saveConfig(); @@ -36,7 +42,24 @@ class Config { } } - addAlerter(name,service) { + deleteService(name) { + if (this.services[name]) { + delete this.services[name]; + this.saveConfig(); + return true; + } else { + throw new Error('No service with that name in config.'); + } + } + + getService(name) { + return this.services[name]; + } + get services() { + return this.settings.services; + } + + addAlerter(name, service) { try { this.alerters[name] = service; this.saveConfig(); @@ -46,17 +69,20 @@ class Config { } } - getService(name) { - return this.services[name]; + deleteAlerter(name) { + if (this.alerters[name]) { + delete this.alerters[name]; + this.saveConfig(); + return true; + } else { + throw new Error('No alerter with that name in config.'); + } } + getAlerter(name) { return this.alerters[name]; } - get services() { - return this.settings.services; - } - get alerters() { return this.settings.alerters; } @@ -65,8 +91,6 @@ class Config { return this.settings.skypuppy; } - - saveConfig() { console.log('Saving Config'); fs.writeFileSync(this.path, JSON.stringify(this.settings, null, 4)); diff --git a/src/health-check.js b/src/health-check.js index ee956b9..69ab367 100644 --- a/src/health-check.js +++ b/src/health-check.js @@ -2,8 +2,9 @@ const fs = require('fs'); var fasquest = require('fasquest'); const client = { https: require('https'), - http: require('http'), + http: require('http') }; + fasquest.agent = { http: new client.http.Agent({ keepAlive: false @@ -18,147 +19,213 @@ var config = new Config(); const Alerts = require('./alerts.js'); class HealthCheck { - constructor(stats,nbars) { - + constructor(stats, nbars) { this.services = {}; - this.alerters = {} + this.alerters = {}; this.stats = stats; - this.alerts = new Alerts(stats,config,nbars); + this.alerts = new Alerts(stats, config, nbars); var servicesKeys = Object.keys(config.services); for (var i = 0; i < servicesKeys.length; i++) { - this.addService(servicesKeys[i],{...config.getService(servicesKeys[i])}); + this.startService(servicesKeys[i], { + ...config.getService(servicesKeys[i]) + }); } - } - - addService(name,service) { - if(!config.getService(name)) { - config.addService(name,service); + addService(name, service) { + if (this.services[name]) { + this.editService(name, service); + } else { + console.log(`Adding service ${name} ...`); + config.addService(name, service); + this.startService(name, service); } + } + getService(name) { + return config.getService(name); + } - var nService = { - name, - started: true, - config: { - ...service, - interval: Math.round((service.interval || 5)*1000), // default 5 seconds - expected_status: service.expected_status || 200, // default 200 OK; - }, - status: { - up: -2, - code: 0, - time: 0, - count: { - healthy: 0, - unhealthy: 0, - unhealthy_status: 0, - unhealthy_response_time: 0, - down: 0 - } + getServiceStatus(name) { + try { + var status = {}; + + switch (this.services[name].status.up) { + case -1: + status.status = 'down'; + break; + case 0: + status.status = 'unhealthy'; + break; + case 1: + status.status = 'healthy'; + break; + default: + status.status = 'unknown'; + break; } - } - - nService.config.request.method = nService.config.request.method || 'GET'; - nService.config.request.timeout = Math.round((nService.config.request.timeout || 60)*1000); - nService.config.request.resolveWithFullResponse = true; - nService.config.request.simple = false; + status.code = this.services[name].status.code; + status.time = this.services[name].status.time; + status.count = this.services[name].status.count; - nService.config.expected_response_time = service.expected_response_time || nService.config.request.timeout; - - if(!nService.config.request.headers) { - nService.config.request.headers = {}; + return status; + } catch (e) { + throw new Error('No service with that name.'); } - nService.config.request.headers['User-Agent'] = `Sky-Puppy / ${config.skypuppy.version} (Health Check Service)` - - this.services[name] = nService; + return null; + } - setTimeout(()=>{ - this._runCheck(this.services[name]); + editService(name, service) { + console.log(`Updating service ${name} ...`); + this.deleteService(name); + this.addService(name, service); + } - },(nService.config.start_delay || 0)*1000) + deleteService(name) { + if (this.services[name]) { + console.log(`Deleting service ${name} ...`); + clearTimeout(this.services[name]._sTimeoutHandler); + delete this.services[name]; + config.deleteService(name); + } + return false; } - editService(service) { - // if(!config.services[service.name]) { - // this. - // } - } + startService(name, service) { + if (!this.services[name]) { + console.log(`Starting service ${name} ...`); + var nService = { + name, + enabled: true, + delete: false, + config: { + ...service, + interval: Math.round((service.interval || 5) * 1000), // default 5 seconds + expected_status: service.expected_status || 200 // default 200 OK; + }, + status: { + up: -2, + code: 0, + time: 0, + count: { + healthy: 0, + unhealthy: 0, + unhealthy_status: 0, + unhealthy_response_time: 0, + down: 0 + } + } + }; - deleteService(service) { + nService.config.request.method = nService.config.request.method || 'GET'; + nService.config.request.timeout = Math.round( + (nService.config.request.timeout || 60) * 1000 + ); + nService.config.request.resolveWithFullResponse = true; + nService.config.request.simple = false; - } + nService.config.expected_response_time = + service.expected_response_time || nService.config.request.timeout; - getStatus(service) { + if (!nService.config.request.headers) { + nService.config.request.headers = {}; + } + nService.config.request.headers[ + 'User-Agent' + ] = `Sky-Puppy / ${config.skypuppy.version} (Health Check Service)`; + + this.services[name] = nService; + this.services[name]._sTimeoutHandler = setTimeout(() => { + this._runCheck(this.services[name]); + }, (nService.config.start_delay || 0) * 1000); + } } - getConfig(service) { + getStatus(service) {} + getConfig() { + return config; } async _runCheck(service) { - if(service.started) { + if (service && service.enabled) { const startTime = process.hrtime.bigint(); const oldStatus = service.status.up; + try { var res = await fasquest.request(service.config.request); - service.status.time = (Number( process.hrtime.bigint() - startTime) / 1000000); + + service.status.time = + Number(process.hrtime.bigint() - startTime) / 1000000; service.status.code = res.statusCode; service.status.up = 1; - if(service.config.expected_status != service.status.code){ + if (service.config.expected_status != service.status.code) { service.status.up = 0; service.status.count.unhealthy_status++; - console.log(service.name,' Unhealthy status: '+service.status.code); + console.log( + service.name, + ' Unhealthy status: ' + service.status.code + ); } - if( service.status.time > service.config.expected_response_time) { + if (service.status.time > service.config.expected_response_time) { service.status.up = 0; service.status.count.unhealthy_response_time++; - console.log(service.name,' Unhealthy response time: '+service.status.time.toFixed(2) +'ms'); + console.log( + service.name, + ' Unhealthy response time: ' + service.status.time.toFixed(2) + 'ms' + ); } - if(service.status.up > 0) { + if (service.status.up > 0) { service.status.count.healthy++; } else { service.status.count.unhealthy++; } } catch (e) { - service.status.time = (Number( process.hrtime.bigint() - startTime) / 1000000); - service.status.count.down++; - service.status.up = -1; - service.status.code = 0; - console.log(service.name,e.err.message); + service.status.time = + Number(process.hrtime.bigint() - startTime) / 1000000; + service.status.count.down++; + service.status.up = -1; + service.status.code = 0; + console.log(service.name, e.err.message); } - - if(service.status.last_status == null) { - service.status.last_status = service.status.up + if (service.status.last_status == null) { + service.status.last_status = service.status.up; } - if(service.status.up > 0) { - if(!service.status.last_healthy) { - service.status.last_healthy = process.hrtime.bigint(); + if (service.status.up > 0) { + if (!service.status.last_healthy) { + service.status.last_healthy = process.hrtime.bigint(); } - if(service.status.last_status < 1 && service.status.last_healthy) { - console.log(service.name,' healthy again!'); - service.status.last_unhealthy_total_duration = (Number(process.hrtime.bigint() - service.status.last_healthy) / 1000000000).toFixed(2) + if (service.status.last_status < 1 && service.status.last_healthy) { + console.log(service.name, ' healthy again!'); + service.status.last_unhealthy_total_duration = ( + Number(process.hrtime.bigint() - service.status.last_healthy) / + 1000000000 + ).toFixed(2); service.status.last_healthy = process.hrtime.bigint(); } } - this.stats.updateService(service.name, service.status) + this.stats.updateService(service.name, service.status); this.alerts.alert(service); - service.status.last_status = service.status.up - const tout = service.config.interval - (Number( process.hrtime.bigint() - startTime) / 1000000); - - setTimeout(async ()=>{ - this._runCheck(service); - },tout > 0 ? tout : 0) + service.status.last_status = service.status.up; + const tout = + service.config.interval - + Number(process.hrtime.bigint() - startTime) / 1000000; + + this.services[service.name]._sTimeoutHandler = setTimeout( + async () => { + this._runCheck(service); + }, + tout > 0 ? tout : 0 + ); } } } diff --git a/src/index.js b/src/index.js index a7c1efb..fae9507 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,19 @@ +#!/usr/bin/env node + const nstats = require('nstats')(); const Stats = require('./misc/stats.js'); const stats = new Stats(); async function start() { - const {NBars} = await import("nbars") + const { NBars } = await import('nbars'); const HealthCheck = require('./health-check.js'); - const healthCheck = new HealthCheck(stats,NBars); + const healthCheck = new HealthCheck(stats, NBars); const app = require('fastify')({ logger: false }); - app.setErrorHandler(function(error, request, reply) { + app.setErrorHandler(function (error, request, reply) { console.error(error); reply.code(500).send( JSON.stringify({ @@ -21,31 +23,16 @@ async function start() { ); }); - app.get('/services/metrics', (req, res) => { - res.code(200).send(stats.toPrometheus()); - }); - app.get('/skypuppy/metrics', (req, res) => { res.code(200).send(nstats.toPrometheus()); }); - app.get('/skypuppy/health', (req, res) => res.code(200).send('All Systems Nominal')); + app.get('/skypuppy/health', (req, res) => + res.code(200).send('All Systems Nominal') + ); // nstats - app.use((req, res, next) => { - if ( - req.url.indexOf('/skypuppy/metrics') == -1 && - req.url.indexOf('/skypuppy/health') == -1 - ) { - if (!nstats.httpServer) { - nstats.httpServer = req.connection.server; - } - var sTime = process.hrtime.bigint(); - - res.on('finish', () => { - nstats.addWeb(req, res, sTime); - }); - } - next(); + app.register(nstats.fastify(), { + ignored_routes: ['/skypuppy/metrics', '/skypuppy/health'] }); app.register(require('./routes/v1'), { @@ -58,7 +45,9 @@ async function start() { console.log(app.printRoutes()); }); - app.listen(process.env.SKY_PUPPY_PORT || 80, process.env.SKY_PUPPY_IP || '0.0.0.0'); - + app.listen( + process.env.SKY_PUPPY_PORT || 8069, + process.env.SKY_PUPPY_IP || '0.0.0.0' + ); } start(); diff --git a/src/routes/v1/alerter.js b/src/routes/v1/alerter.js index 46d23e0..d25aad4 100644 --- a/src/routes/v1/alerter.js +++ b/src/routes/v1/alerter.js @@ -1,14 +1,29 @@ - -module.exports = function(fastify, opts) { +module.exports = function (fastify, opts) { const stats = opts.stats; const healthCheck = opts.healthCheck; fastify.put('/alerter/:name', async (req, res) => { var alterter = req.body; var name = req.params.name; - healthCheck.alerts.addAlerter(name,alterter); + + healthCheck.alerts.addAlerter(name, alterter); res.status(200).send(); }); + fastify.get('/alerter/:name', async (req, res) => { + var name = req.params.name; + + res.status(200).send(healthCheck.alerts.getAlerter(name)); + }); + + fastify.delete('/alerter/:name', async (req, res) => { + var name = req.params.name; + try { + await healthCheck.alerts.deleteAlerter(name); + res.status(200).send(); + } catch (e) { + res.status(400).send(e.message); + } + }); }; diff --git a/src/routes/v1/config.js b/src/routes/v1/config.js new file mode 100644 index 0000000..e9dad31 --- /dev/null +++ b/src/routes/v1/config.js @@ -0,0 +1,16 @@ +module.exports = function (fastify, opts) { + const stats = opts.stats; + const healthCheck = opts.healthCheck; + + fastify.get('/config', async (req, res) => { + healthCheck.getConfig(); + }); + + fastify.put('/config', async (req, res) => { + var service = req.body; + var name = req.params.name; + + healthCheck.addService(name, service); + res.status(200).send(); + }); +}; diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js index f160dde..6f917ba 100644 --- a/src/routes/v1/index.js +++ b/src/routes/v1/index.js @@ -1,5 +1,6 @@ -module.exports = function(fastify, opts, done) { +module.exports = function (fastify, opts, done) { require('./service.js')(fastify, opts); + require('./config.js')(fastify, opts); require('./alerter.js')(fastify, opts); done(); }; diff --git a/src/routes/v1/service.js b/src/routes/v1/service.js index 9a6ba54..3179495 100644 --- a/src/routes/v1/service.js +++ b/src/routes/v1/service.js @@ -1,25 +1,39 @@ - -module.exports = function(fastify, opts) { +module.exports = function (fastify, opts) { const stats = opts.stats; const healthCheck = opts.healthCheck; - fastify.get('/service/:name/config', async (req, res) => { + fastify.get('/service/:name', async (req, res) => { + var name = req.params.name; + res.status(200).send(healthCheck.getService(name)); }); - fastify.put('/service/:name/config', async (req, res) => { + fastify.put('/service/:name', async (req, res) => { var service = req.body; var name = req.params.name; - healthCheck.addService(name,service); + + healthCheck.addService(name, service); res.status(200).send(); }); - fastify.delete('/service/:name/config', async (req, res) => { + fastify.delete('/service/:name', async (req, res) => { + var name = req.params.name; + try { + await healthCheck.deleteService(name); + res.status(200).send(); + } catch (e) { + res.status(400).send(e.message); + } }); fastify.get('/service/:name/status', async (req, res) => { + var name = req.params.name; + res.status(200).send(healthCheck.getServiceStatus(name)); }); + fastify.get('/services/metrics', (req, res) => { + res.code(200).send(stats.toPrometheus()); + }); }; diff --git a/test/server/test-server.js b/test/server/test-server.js index e5560f7..b86cc88 100644 --- a/test/server/test-server.js +++ b/test/server/test-server.js @@ -4,18 +4,23 @@ const url = require('url'); const URL = require('url').URL; var options = { disableRequestLogging: false, - logger: { - level: 'error' - }, + // logger: { + // level: 'error' + // }, https: false, ip: '0.0.0.0', port: 4270, - log: true -} + log: false +}; const app = require('fastify')(options); function response(req, res) { - var urlParsed = new URL(`${options.https ? 'https' : 'http'}://${options.ip}:${options.port}${req.raw.url}`); + var urlParsed = new URL( + `${options.https ? 'https' : 'http'}://${options.ip}:${options.port}${ + req.raw.url + }` + ); + if (options.log) { console.log({ body: req.body, @@ -28,7 +33,7 @@ function response(req, res) { id: req.id, ip: req.ip, ips: req.ips, - hostname: req.hostname, + hostname: req.hostname }); } res.code(200).send({ @@ -42,32 +47,54 @@ function response(req, res) { id: req.id, ip: req.ip, ips: req.ips, - hostname: req.hostname, + hostname: req.hostname }); } - +function log(req, res) { + if (options.log) { + console.log({ + body: req.body, + query: req.query, + params: req.params, + url: req.req.url, + method: req.raw.method, + headers: req.headers, + // raw: req.raw, + id: req.id, + ip: req.ip, + ips: req.ips, + hostname: req.hostname + }); + } +} app.all('/redirect', (req, res) => { + log(req, res); res.redirect(req.query.url); }); app.all('/redirect/loop/:count', (req, res) => { - res.redirect('/redirect/loop/' + (++req.params.count)); + log(req, res); + res.redirect('/redirect/loop/' + ++req.params.count); }); app.all('/redirect/loop/', (req, res) => { - res.redirect('/redirect/loop') + log(req, res); + res.redirect('/redirect/loop'); }); app.all('/econnreset', (req, res) => { + log(req, res); req.req.socket.destroy(); }); var error_flipflop = false; + app.all('/error/flipflop', (req, res) => { + log(req, res); if (!error_flipflop) { res.status(500); error_flipflop = true; - res.send(); + res.send(); } else { error_flipflop = false; res.status(200); @@ -76,30 +103,28 @@ app.all('/error/flipflop', (req, res) => { }); app.all('/error/random', (req, res) => { + log(req, res); res.status(Math.round(Math.random()) > 0 ? 200 : 500); - res.send() + res.send(); }); app.all('/wait/random/:start/:end', async (req, res) => { - var seconds = getRandomInt(Number(req.params.start),Number(req.params.end)) + log(req, res); + var seconds = getRandomInt(Number(req.params.start), Number(req.params.end)); var p = new Promise((resolve, reject) => { setTimeout(() => { resolve(); - }, seconds*1000); + }, seconds * 1000); }); + await p; res.send(); }); - -app.put('/alert/test', (request, res) => { - - console.log(`${request.body.embeds[0].title} : ${request.body.embeds[0].description}`) - res.status(200).send() -}); - var econnreset_flipflop = false; + app.all('/econnreset/flipflop', (req, res) => { + log(req, res); if (!econnreset_flipflop) { req.req.socket.destroy(); econnreset_flipflop = true; @@ -109,9 +134,9 @@ app.all('/econnreset/flipflop', (req, res) => { } }); function getRandomInt(min, max) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; } // app.all('/exit', (req, res) => { // process.exit(1); @@ -121,4 +146,12 @@ function getRandomInt(min, max) { // app.all(['/:param1/:param2'], response); // app.all(['/:param1/:param2/:param3'], response); // app.all(['/:param1/:param2/:param3/:param4'], response); + +app.put('/alert/test', (request, res) => { + console.log( + `${request.body.embeds[0].title} : ${request.body.embeds[0].description}` + ); + res.status(200).send(); +}); + app.listen(options.port, options.ip); diff --git a/test/sky-puppy-config.json b/test/sky-puppy-config.json index 8b38a70..650b5ee 100644 --- a/test/sky-puppy-config.json +++ b/test/sky-puppy-config.json @@ -1,138 +1,138 @@ { - "alerters": { - "discord_down": { - "uri": "http://127.0.0.1:4270/alert/test", - "json": true, - "method": "PUT", - "body": { - "embeds": [ - { - "title": "{{service_name}} is {{alert_type}}!", - "description": "This service was healthy for {{last_healthy_total_duration}} seconds!", - "color": 14828098, - "footer": { - "text": "" - }, - "timestamp": "{{timestamp}}" - } - ], - "username": "Sky Puppy", - "avatar_url": "https://i.imgur.com/J5vIVSt.png" + "alerters": { + "discord_down": { + "uri": "http://127.0.0.1:4270/alert/test", + "json": true, + "method": "PUT", + "body": { + "embeds": [ + { + "title": "{{service_name}} is {{alert_type}}!", + "description": "This service was healthy for {{last_healthy_total_duration}} seconds!", + "color": 14828098, + "footer": { + "text": "" + }, + "timestamp": "{{timestamp}}" } - }, - "discord_unhealthy": { - "uri": "http://127.0.0.1:4270/alert/test", - "json": true, - "method": "PUT", - "body": { - "embeds": [ - { - "title": "{{service_name}} is {{alert_type}}!", - "description": "This service was healthy for {{last_healthy_total_duration}} seconds!", - "color": 14852674, - "footer": { - "text": "" - }, - "timestamp": "{{timestamp}}" - } - ], - "username": "Sky Puppy", - "avatar_url": "https://i.imgur.com/J5vIVSt.png" + ], + "username": "Sky Puppy", + "avatar_url": "https://i.imgur.com/J5vIVSt.png" + } + }, + "discord_unhealthy": { + "uri": "http://127.0.0.1:4270/alert/test", + "json": true, + "method": "PUT", + "body": { + "embeds": [ + { + "title": "{{service_name}} is {{alert_type}}!", + "description": "This service was healthy for {{last_healthy_total_duration}} seconds!", + "color": 14852674, + "footer": { + "text": "" + }, + "timestamp": "{{timestamp}}" } - }, - "discord_healthy": { - "uri": "http://127.0.0.1:4270/alert/test", - "json": true, - "method": "PUT", - "body": { - "embeds": [ - { - "title": "{{service_name}} is {{alert_type}}!", - "description": "Carry on, looks like things are back! We were down for {{last_unhealthy_total_duration}} seconds.", - "color": 6480450, - "footer": { - "text": "" - }, - "timestamp": "{{timestamp}}" - } - ], - "username": "Sky Puppy", - "avatar_url": "https://i.imgur.com/3rfFeOu.png" + ], + "username": "Sky Puppy", + "avatar_url": "https://i.imgur.com/J5vIVSt.png" + } + }, + "discord_healthy": { + "uri": "http://127.0.0.1:4270/alert/test", + "json": true, + "method": "PUT", + "body": { + "embeds": [ + { + "title": "{{service_name}} is {{alert_type}}!", + "description": "Carry on, looks like things are back! We were down for {{last_unhealthy_total_duration}} seconds.", + "color": 6480450, + "footer": { + "text": "" + }, + "timestamp": "{{timestamp}}" } + ], + "username": "Sky Puppy", + "avatar_url": "https://i.imgur.com/3rfFeOu.png" + } + } + }, + "services": { + "timeout-test": { + "interval": 5, + "request": { + "uri": "http://127.0.0.1:4270/wait/random/0/1", + "timeout": 2 + }, + "expected_response_time": 500, + "alerts": [ + { + "type": "down", + "alerter": "discord_down" + }, + { + "type": "unhealthy_response_time", + "for": 1, + "alerter": "discord_unhealthy" + }, + { + "type": "healthy", + "alerter": "discord_healthy" } + ] }, - "services": { - "timeout-test": { - "interval":5, - "request": { - "uri": "http://127.0.0.1:4270/wait/random/0/1", - "timeout": 2 - }, - "expected_response_time": 500, - "alerts": [ - { - "type": "down", - "alerter": "discord_down" - }, - { - "type": "unhealthy_response_time", - "for": 1, - "alerter": "discord_unhealthy" - }, - { - "type": "healthy", - "alerter": "discord_healthy" - } - ] + "status-test": { + "interval": 5, + "request": { + "uri": "http://127.0.0.1:4270/error/random", + "timeout": 2 }, - "status-test": { - "interval":5, - "request": { - "uri": "http://127.0.0.1:4270/error/random", - "timeout": 2 - }, - "start_delay":1, - "alerts": [ - { - "type": "down", - "alerter": "discord_down" - }, - { - "type": "unhealthy_status", - "for": 1, - "alerter": "discord_unhealthy" - }, - { - "type": "healthy", - "alerter": "discord_healthy" - } - ] + "start_delay": 1, + "alerts": [ + { + "type": "down", + "alerter": "discord_down" }, - "status-fast-test": { - "interval":0.25, - "request": { - "uri": "http://127.0.0.1:4271/error/flipflop", - "timeout": 2 - }, - "start_delay":2, - "alerts": [ - { - "type": "down", - "alerter": "discord_down" - }, - { - "type": "unhealthy_status", - "for": 1, - "alerter": "discord_unhealthy" - }, - { - "type": "healthy", - "alerter": "discord_healthy" - } - ] + { + "type": "unhealthy_status", + "for": 4, + "alerter": "discord_unhealthy" + }, + { + "type": "healthy", + "alerter": "discord_healthy" } + ] }, - "skypuppy": { - "version": "1.0.0" + "put-test": { + "interval": 3, + "request": { + "uri": "http://127.0.0.1:4270/error/flipflop", + "timeout": 2 + }, + "start_delay": 2, + "json": true, + "method": "PUT", + "body": { + "test": "sweet" + }, + "alerts": [ + { + "type": "unhealthy", + "alerter": "discord_unhealthy" + }, + { + "type": "healthy", + "alerter": "discord_healthy" + } + ] } + }, + "skypuppy": { + "version": "1.0.0" + } }