initial commit
This commit is contained in:
commit
ea55fda9e5
5 changed files with 365 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
.idea/
|
||||
*.iml
|
||||
|
||||
node_modules/
|
||||
|
||||
cache.json
|
||||
config.json
|
31
README.md
Normal file
31
README.md
Normal file
|
@ -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://<cachet-url>/api/v1",
|
||||
"token": "<user-token>"
|
||||
}
|
||||
```
|
132
index.js
Normal file
132
index.js
Normal file
|
@ -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(), {});
|
||||
|
180
package-lock.json
generated
Normal file
180
package-lock.json
generated
Normal file
|
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
15
package.json
Normal file
15
package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
Reference in a new issue