Skip to content

Commit

Permalink
Updated packages, config reading and adding command execution on failure
Browse files Browse the repository at this point in the history
  • Loading branch information
cyberinferno committed Jul 18, 2020
1 parent 16c3c90 commit c1923e1
Show file tree
Hide file tree
Showing 8 changed files with 1,249 additions and 57 deletions.
13 changes: 13 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "strongloop",
"rules": {
"max-len": [2, 120, 8]
},
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2018
}
}
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Simple remote port monitor tool

A simple configurable tool to monitor remote ports and notify if connectivity fails. **Discord channel** as well as **Email** notifications are currently available.
A simple configurable tool to monitor remote ports and notify if connectivity fails. If the application is running in same server as the service, a command can be configured to be executed on connectivity check failure.

**Discord channel** as well as **Email** notifications are currently available.

## Requirement

Expand All @@ -13,7 +15,7 @@ A simple configurable tool to monitor remote ports and notify if connectivity fa
- Install latest LTS version NodeJS (Refer https://nodejs.org).
- Clone this repository.
- Run `npm install` command in the project directory to install all dependencies.
- Create the file `config.json` and copy all contents from `config_example.json`. Update configuration as per your requirements.
- Create the file `development.json` or `production.json` based upon environment to override default config.
- Run `node app.js` command in the project directory to start the monitoring for testing purpose.
- Install PM2 (Refer https://pm2.keymetrics.io/) and run the command `pm2 start app.js` in the project directory to run it as a background service in your server.

Expand All @@ -27,10 +29,11 @@ A simple configurable tool to monitor remote ports and notify if connectivity fa
| discord_webhooks | Array of Discord webhooks to which notification will be sent |
| notify_emails | Array of emails to which notification will be sent |
| smtp_transport | SMTP server details using which tool will send emails |
| message_strings | Holds text that is sent when a service is down |

### Example Configuration

This is a full config example with discord webhook and SMTP configurations included for reference purpose
This is a full config example with restart command, discord webhook and SMTP configurations included for reference purpose

```
{
Expand All @@ -40,7 +43,8 @@ This is a full config example with discord webhook and SMTP configurations inclu
"services": [{
"name": "My local web server",
"host": "127.0.0.1",
"port": 80
"port": 80,
"command": "sudo service apache2 restart"
}],
"discord_webhooks": [
"https://discordapp.com/api/webhooks/{id}/{token}"
Expand Down
115 changes: 87 additions & 28 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
'use strict';
/**
* Simple port monitor and restart tool
*/

const axios = require('axios');
const config = require('./config.json');
const config = require('config');
const net = require('net');
const nodemailer = require('nodemailer');
const { exec } = require('child_process');


const MILISECONDS_TO_SECONDS_FACTOR = 1000;
const DEFAULT_TIMEOUT_IN_SECONDS = 10000;
const MILLISECONDS_TO_SECONDS_FACTOR = 1000;
const DEFAULT_TIMEOUT_IN_SECONDS = 10;

var notificationLog = {};
try {
Expand All @@ -29,82 +31,99 @@ try {
console.log(e);
}

// TODO: Make sure config is valid

// Start monitoring process
function startMonitoring() {
console.log('Starting monitoring service...');
if (!config['services'] || !config['services'].length) {
console.log('No services have been configured to be monitored!');
} else {
checkAndNotifyAllServices();
setInterval(checkAndNotifyAllServices, config['monitor_interval'] * MILISECONDS_TO_SECONDS_FACTOR);
setInterval(checkAndNotifyAllServices, config['monitor_interval'] * MILLISECONDS_TO_SECONDS_FACTOR);
}
}

// Function thats called at given interval which checks services and notifies if down
function checkAndNotifyAllServices() {
config['services'].forEach(service => {
if (service.host && service.port && service.name) {
console.log(`Checking ${service.name} connectivity...`);
checkConnection(service.name, service.host, service.port)
checkConnection(service)
.then(result => {
console.log(`${result.name} connectivity check succeeded!`);
if (notificationLog[result.name]) {
delete notificationLog[result.name];
}
}).catch(result => {
console.log(`${result.name} connectivity check failed!`);
if (result.command) {
executeCommand(result.command);
}
notifyFailure(result);
});
}
});
}

function notifyFailure(data) {
// Notify failure
function notifyFailure({name, host, port, command}) {
let currentTime = process.hrtime();
// Do not notify if interval has not reached
const notification = notificationLog[data.name];
const notification = notificationLog[name];
if (notification && currentTime[0] - notification < config['notify_interval']) {
return;
}
notificationLog[data.name] = currentTime[0];

const notificationMsg = `@here ${data.name} service with host ${data.host}\
and port ${data.port} has gone down!`;
// Set current time as last notification time
notificationLog[name] = currentTime[0];

// Generate failure message
let notificationMsg;
if (command) {
notificationMsg = renderTemplate(config['message_strings']['failure_with_command'], {name, host, port, command});
} else {
notificationMsg = renderTemplate(config['message_strings']['failure'], {name, host, port});
}

// Discord notification
if (config['discord_webhooks'] && config['discord_webhooks'].length) {
config['discord_webhooks'].forEach(webhook => discordNotification(webhook, notificationMsg));
}

// Email notification
if (config['notify_emails'] && config['notify_emails'].length) {
config['notify_emails'].forEach(email => emailNotification(email, notificationMsg, notificationMsg));
}
}

async function discordNotification(webhookUrl, content) {
// Discord webhook notification
function discordNotification(webhookUrl, content) {
try {
await axios.post(webhookUrl, {
content: content
axios.post(webhookUrl, {
content: content,
});
} catch (err) {
console.log(err);
}
}

// SMTP email notification
function emailNotification(toAddress, subject, body) {
if (config['smtp_transport'] && config['smtp_transport']['host']) {
let transporter = nodemailer.createTransport(config['smtp_transport']);
transporter.sendMail({
from: config['smtp_transport']['auth']['user'],
to: toAddress,
subject: subject,
text: body
text: body,
});
}
}

function checkConnection(serviceName, host, port) {
// Checks whether the service port is open by trying to connect to it
function checkConnection({name, host, port, command}) {
return new Promise((resolve, reject) => {
const connectionTimeout = config['connection_timeout'];
const timeout = connectionTimeout ? connectionTimeout * MILISECONDS_TO_SECONDS_FACTOR :
const timeout = connectionTimeout ? connectionTimeout * MILLISECONDS_TO_SECONDS_FACTOR :
DEFAULT_TIMEOUT_IN_SECONDS;
const timer = setTimeout(() => {
reject('timeout');
Expand All @@ -113,20 +132,60 @@ function checkConnection(serviceName, host, port) {
const socket = net.createConnection(port, host, () => {
clearTimeout(timer);
resolve({
name: serviceName,
host: host,
port: port
name,
host,
port,
});
socket.end();
});
socket.on('error', (err) => {
socket.on('error', (error) => {
clearTimeout(timer);
reject({
name: serviceName,
host: host,
port: port,
error: err
name,
host,
port,
error,
command,
});
});
});
}

// Executes failure command
function executeCommand(command) {
exec(command, (error, stdout, stderr) => {
if (error) {
console.log(`${command} error: ${error.message}`);
return;
}
if (stderr) {
console.log(`${command} stderr: ${stderr}`);
return;
}
console.log(`${command} stdout: ${stdout}`);
});
}

// Ref: https://stackoverflow.com/questions/29182244/convert-a-string-to-a-template-string
function Prop(obj, is, value) {
if (typeof is === 'string')
is = is.split('.');
if (is.length === 1 && value !== undefined)
// eslint-disable-next-line no-return-assign
return obj[is[0]] = value;
else if (is.length === 0)
return obj;
else {
var prop = is.shift();
// Forge a path of nested objects if there is a value to set
if (value !== undefined && obj[prop] === undefined) obj[prop] = {};
return Prop(obj[prop], is, value);
}
}

// Ref: https://stackoverflow.com/questions/29182244/convert-a-string-to-a-template-string
function renderTemplate(str, obj) {
return str.replace(/\$\{(.+?)\}/g, (match, p1) => {
return Prop(obj, p1);
});
}
3 changes: 3 additions & 0 deletions config/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*
!.gitignore
!default.json
13 changes: 13 additions & 0 deletions config/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"notify_interval": 900,
"monitor_interval": 60,
"connection_timeout": 30,
"services": {},
"discord_webhooks": [],
"notify_emails": [],
"smtp_transport": {},
"message_strings": {
"failure": "**${name}** service with host **${host}** and port **${port}** has gone down!",
"failure_with_command": "**${name}** service with host **${host}** and port **${port}** has gone down. **${command}** was executed!"
}
}
9 changes: 0 additions & 9 deletions config_example.json

This file was deleted.

Loading

0 comments on commit c1923e1

Please sign in to comment.