From ea55fda9e5738980a630ad5961a18915c6d5b41b Mon Sep 17 00:00:00 2001 From: samuel-p Date: Wed, 26 Feb 2020 23:56:42 +0100 Subject: [PATCH] initial commit --- .gitignore | 7 ++ README.md | 31 ++++++++ index.js | 132 ++++++++++++++++++++++++++++++++++ package-lock.json | 180 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 15 ++++ 5 files changed, 365 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4983cf8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +*.iml + +node_modules/ + +cache.json +config.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e65976 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# cachet-monitor + +Simple monitor to watch URLs (`HTTP`) or ports (`TCP`, `UDP`) and update Cachet status. + +## Configuration + +Example: + +```json +{ + "services": [ + { + "id": 1, + "type": "HTTP", + "url": "https://sp-codes.de", + "timeout": 60 + }, + { + "id": 2, + "type": "TCP", + "host": "sp-codes.de", + "port": 443, + "timeout": 60 + } + ], + "cron": "0 * * * * *", + "offlineTimeUntilMajor": 300, + "api": "https:///api/v1", + "token": "" +} +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..e59cb45 --- /dev/null +++ b/index.js @@ -0,0 +1,132 @@ +const config = require('./config'); +const fs = require('fs'); +const cron = require('node-cron'); +const fetch = require('node-fetch'); +const abort = require('abort-controller'); +const nmap = require('libnmap'); + +const cache = fs.existsSync("cache.json") ? JSON.parse(fs.readFileSync("cache.json", {encoding: "utf8"})) : {}; + +process.on('SIGINT', () => { + fs.writeFileSync("cache.json", JSON.stringify(cache), {encoding: "utf8"}); + process.exit(0); +}); + +const cachetStatusMapping = { + "ONLINE": 1, + "SLOW": 2, + "OFFLINE": 3, + "INCIDENT": 4 +}; + +const checkHttp = async (url, performanceTimeout, requestTimeout) => { + const controller = new abort.AbortController(); + const timeout = setTimeout(() => controller.abort(), requestTimeout); + try { + const start = new Date().getTime(); + const response = await fetch(url, {signal: controller.signal}); + const stop = new Date().getTime(); + if (response.ok) { + if (stop - start > performanceTimeout) { + return {status: "SLOW", message: response.statusText}; + } + return {status: "ONLINE", message: response.statusText}; + } else { + return {status: "OFFLINE", message: response.statusText}; + } + } catch (e) { + return {status: "OFFLINE", message: e.message}; + } finally { + clearTimeout(timeout); + } +}; + +const checkPort = async (host, port, type, performanceTimeout, requestTimeout) => { + return await new Promise(resolve => { + nmap.scan({ + range: [host], + ports: port.toString(), + timeout: requestTimeout / 1000, + udp: type === 'udp' + }, (error, report) => { + if (error) { + resolve({status: "OFFLINE", message: error}); + } else { + const result = report[host].host[0]; + const time = parseInt(result.item.endtime) - parseInt(result.item.starttime); + const status = result.ports[0].port[0].state[0].item; + if (status.state.includes('open')) { + if (time > performanceTimeout) { + resolve({status: "SLOW", message: status.state}); + } else { + resolve({status: "ONLINE", message: status.state}); + } + } else { + resolve({status: "OFFLINE", message: status.state}); + } + } + }); + }); +}; + +async function checkStatus(service) { + switch (service.type) { + case 'HTTP': + return await checkHttp(service.url, service.timeout * 1000, service.timeout * 2000); + case 'TCP': + return await checkPort(service.host, service.port, 'tcp', service.timeout * 1000, service.timeout * 2000); + case 'UDP': + return await checkPort(service.host, service.port, 'udp', service.timeout * 1000, service.timeout * 2000); + default: + throw new Error('unsupported type "' + type + '"') + } +} + +const checkService = async (service, oldStatus) => { + const newStatus = await checkStatus(service); + newStatus.changed = new Date().getTime(); + if (newStatus.status === "OFFLINE" && ["OFFLINE", "INCIDENT"].includes(oldStatus.status) && + oldStatus.changed + config.offlineTimeUntilMajor * 1000 < newStatus.changed) { + newStatus.status = "INCIDENT"; + } + return newStatus; +}; + +const pushStatusToCachet = async (id, status) => { + try { + let currentCachetStatus = cachetStatusMapping[status]; + const oldState = await fetch(config.api + '/components/' + id).then(r => r.json()); + if (oldState.data.status === currentCachetStatus) { + console.log('state already set'); + return; + } + const update = await fetch(config.api + '/components/' + id, { + method: 'PUT', + body: JSON.stringify({status: currentCachetStatus}), + headers: { + 'Content-Type': 'application/json', + 'X-Cachet-Token': config.token + } + }); + if (!update.ok) { + console.log('failed to update status: ' + update.statusText); + } + } catch (e) { + console.log('failed to update status', e); + } +}; + +const check = async () => { + for (const service of config.services) { + const oldStatus = cache[service.id]; + const newStatus = await checkService(service, oldStatus); + if (oldStatus.status !== newStatus.status) { + console.log(service.id + ' status changed: ' + newStatus.status); + await pushStatusToCachet(service.id, newStatus.status); + cache[service.id] = newStatus; + } + } +}; + +cron.schedule(config.cron, async () => await check(), {}); + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..03091ce --- /dev/null +++ b/package-lock.json @@ -0,0 +1,180 @@ +{ + "name": "cachet-monitor", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "cidr-js": { + "version": "git+https://github.com/jas-/cidr-js.git#8bbf8227780da869d082096847fe3d9a74bf8ef4", + "from": "git+https://github.com/jas-/cidr-js.git#v2.3.2", + "requires": { + "bluebird": "^2.9.21", + "ip-subnet-calculator": "^1.0.2", + "line-by-line": "^0.1.3", + "locutus": "^2.0.9" + } + }, + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "hasbin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/hasbin/-/hasbin-1.2.3.tgz", + "integrity": "sha1-eMWSaJPIAhXCtWiuH9P8q3omlrA=", + "requires": { + "async": "~1.5" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + } + } + }, + "ip-address": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-5.9.4.tgz", + "integrity": "sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw==", + "requires": { + "jsbn": "1.1.0", + "lodash": "^4.17.15", + "sprintf-js": "1.1.2" + } + }, + "ip-subnet-calculator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip-subnet-calculator/-/ip-subnet-calculator-1.1.8.tgz", + "integrity": "sha1-m8AuIz1aQ++uet60X2ppS4K5cP0=" + }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" + }, + "libnmap": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/libnmap/-/libnmap-0.4.19.tgz", + "integrity": "sha512-aMtOq3rsG5mH1UDqYRUf+DSojlSMh6wxV6B9qa8ZOeUHXguSaSCwABqQWMtjbU0o3d5y46nQPAvDZnPT9Thhvw==", + "requires": { + "async": "^2.6.3", + "cidr-js": "git+https://github.com/jas-/cidr-js.git#v2.3.2", + "deepmerge": "^2.2.1", + "hasbin": "^1.2.3", + "ip-address": "^5.9.4", + "netmask": "^1.0.6", + "stack-trace": "0.0.10", + "xml2js": "^0.4.22" + } + }, + "line-by-line": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/line-by-line/-/line-by-line-0.1.6.tgz", + "integrity": "sha512-MmwVPfOyp0lWnEZ3fBA8Ah4pMFvxO6WgWovqZNu7Y4J0TNnGcsV4S1LzECHbdgqk1hoHc2mFP1Axc37YUqwafg==" + }, + "locutus": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.11.tgz", + "integrity": "sha512-C0q1L38lK5q1t+wE0KY21/9szrBHxye6o2z5EJzU+5B79tubNOC+nLAEzTTn1vPUGoUuehKh8kYKqiVUTWRyaQ==", + "requires": { + "es6-promise": "^4.2.5" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "netmask": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", + "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" + }, + "node-cron": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-2.0.3.tgz", + "integrity": "sha512-eJI+QitXlwcgiZwNNSRbqsjeZMp5shyajMR81RZCqeW0ZDEj4zU9tpd4nTh/1JsBiKbF8d08FCewiipDmVIYjg==", + "requires": { + "opencollective-postinstall": "^2.0.0", + "tz-offset": "0.0.1" + } + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, + "opencollective-postinstall": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", + "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "tz-offset": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tz-offset/-/tz-offset-0.0.1.tgz", + "integrity": "sha512-kMBmblijHJXyOpKzgDhKx9INYU4u4E1RPMB0HqmKSgWG8vEcf3exEfLh4FFfzd3xdQOw9EuIy/cP0akY6rHopQ==" + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c268f0 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "cachet-monitor", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "node index.js" + }, + "author": "codes@samuel-philipp.de", + "dependencies": { + "abort-controller": "^3.0.0", + "libnmap": "^0.4.19", + "node-cron": "^2.0.3", + "node-fetch": "^2.6.0" + } +}