Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
kulak91 committed Oct 26, 2022
1 parent db3e571 commit 0cc53a4
Show file tree
Hide file tree
Showing 13 changed files with 1,030 additions and 1 deletion.
22 changes: 22 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
slack_url=https://hooks.slack.com/services/aaa/aaa/aaa
log=true
error=true
kill=true
exception=false
restart=false
reload=true
delete=false
stop=false
exit=true
start=true
online=true
buffer=true
buffer_seconds=2
queue_max=100
buffer_max_seconds=20
SLACK_SIGNING_SECRET=change
SLACK_BOT_TOKEN=xoxb-aaa-aaa-aaa
SLACK_PORT=6666
SLACK_ADMINS='aaa aaa'
SLACK_APP_TOKEN=xapp-a-aaa-aaa-aaa
LOGS_PATH='/aaa/aaa/aaa/aaa/aaa/aaa/aaa-out.log'
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
.DS_Store
.jshintrc
.env
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2022 DMYTRO KULAKOV

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
102 changes: 101 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,101 @@
# binary-slack
# binary-slack

This is a PM2 Module for sending events & logs from your PM2 processes to Slack.

## Install

To install and setup pm2-slack, run the following commands:

```
pm2 install pm2-slack
pm2 set pm2-slack:slack_url https://slack_url
```

To get the Slack URL, you need to setup an Incoming Webhook. More details on how to set this up can be found here: https://api.slack.com/incoming-webhooks

## Events subscription configuration

The following events can be subscribed to:

- `log` - All standard out logs from your processes. Default: false
- `error` - All error logs from your processes. Default: true
- `kill` - Event fired when PM2 is killed. Default: true
- `exception` - Any exceptions from your processes. Default: true
- `restart` - Event fired when a process is restarted. Default: false
- `reload` - Event fired when a cluster is reloaded. Default: false
- `delete` - Event fired when a process is removed from PM2. Default: false
- `stop` - Event fired when a process is stopped. Default: false
- `restart overlimit` - Event fired when a process is reaches the max amount of times it can restart. Default: true
- `exit` - Event fired when a process is exited. Default: false
- `start` - Event fired when a process is started. Default: false
- `online` - Event fired when a process is online. Default: false

You can simply turn these on and off by setting them to true or false using the PM2 set command.

```
pm2 set pm2-slack:log true
pm2 set pm2-slack:error false
```

## Options

The following options are available:

- `slack_url` (string) - Slack Incomming Webhook URL.
- `servername` / `username` (string) - Set the custom username for Slack messages (visible in message headers). Default: server hostname
- `buffer` (bool) - Enable/Disable buffering of messages. Messages that occur in short time will be concatenated together and posted as a single slack message. Default: true
- `buffer_seconds` (int) - If buffering is enables, all messages are stored for this interval. If no new messages comes in this interval, buffered message(s) are sended to Slack. If new message comes in this interval, the "timer" will be reseted and buffer starts waiting for the new interval for a new next message. *Note: Puspose is reduction of push notifications on Slack clients.* Default: 2
- `buffer_max_seconds` (int) - If time exceed this time, the buffered messages are always sent to Slack, even if new messages are still comming in interval (property `buffer_seconds`). Default: 20
- `queue_max` (int) - Maximum number of messages, that can be send in one Slack message (in one bufferring round). When the queue exceeds this maximum, next messages are suppresesed and replaced with message "*Next XX messages have been suppressed.*". Default: 100

Set these options in the same way as subscribing to events.


###### Example

The following configuration options will enable message buffering, and set the buffer duration to 5 seconds. All messages that occur within maximum 5 seconds delay between two neighboring messages will be concatenated into a single slack message.

```
pm2 set pm2-slack:slack_url https://hooks.slack.com/services/123456789/123456789/aaaaaaa
pm2 set pm2-slack:buffer_seconds 5
```

Note: In this example, the maximum total delay for messages is still 20 seconds (default value for `buffer_max_seconds`). After this time, the buffer will be flushed
everytime and all messages will be sent.

### Process based custom options

By default, all options are defined for all processes globally.
But you can separately define custom options to each PM2 process.
Use format `pm2-slack:optionName-processName` to process based custom options.

If no custom options is defined, the global `pm2-slack:propertyName` will be used.

Note: By this way, all custom options can be used for specific process, but events subsciptions configuration is always global only.

###### Example

We have many processes, includes process `foo` and process `bar`.
For this two processes will have to define separate Slack URL channel and separate server name.
Same buffer options will be used for all processed.

```
# Define global options for all processes.
pm2 set pm2-slack:buffer_seconds 5
# Define global options for all processes.
# (for process `foo` and `bar` the values will be overridden below).
pm2 set pm2-slack:slack_url https://hooks.slack.com/services/123456789/123456789/aaaaaaa
pm2 set pm2-slack:servername Orion
# Define custom Slack Incomming Webhoook for `foo` process.
pm2 set pm2-slack:slack_url-foo https://hooks.slack.com/services/123456789/123456789/bbbbbbb
pm2 set pm2-slack:servername-foo Foo-server
# Note: The `pm2-slack:buffer_seconds`=5 will be used from global options for this process.
# Define custom Slack Incomming Webhoook for `bar` process
pm2 set pm2-slack:slack_url-bar https://hooks.slack.com/services/123456789/123456789/ccccccc
pm2 set pm2-slack:servername-foo Bar-server
# Note: The `pm2-slack:buffer_seconds`=5 will be used from global options for this process.
```

181 changes: 181 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
'use strict';

// Dependency
// for local tests:
require('dotenv').config();
const pm2 = require('pm2');
const pmx = require('pmx').initModule();
const app = require('./slack-receiver');
const MessageQueue = require('./message-queue');
const { parseIncommingLog, parseProcessName } = require('./utils');

/**
* Get the configuration from PM2
*
* @type {Object}
* @property {boolean} exception
*/



// const process.env = pmx.initModule();

const slackUrlRouter = {
/**
* Keys are slackUrls, values are instances of MessageQueue
*
* @typedef {Object.<string, MessageQueue>}
*/
messageQueues: {},


/**
* Add the message to appropriate message queue (each Slack URL has own independent message enqueing).
*
* @param {Message} message
*/
addMessage: function (message) {
const processName = message.name;
const slackUrl = process.env['slack_url-' + processName] || process.env['slack_url'];

if (!slackUrl) {
return;
}

if (!this.messageQueues[slackUrl]) {
// Init new messageQueue to different Slack URL.

// Resolve configuration parameters.
const configProperties = ['username', 'servername', 'buffer', 'slack_url', 'buffer_seconds', 'buffer_max_seconds', 'queue_max'];
const config = {};
configProperties.map((configPropertyName) => {
// Use process based custom configuration values if exist, else use the global configuration values.
config[configPropertyName] = process.env[configPropertyName + '-' + processName] || process.env[configPropertyName];
});

this.messageQueues[slackUrl] = new MessageQueue(config);
}

this.messageQueues[slackUrl].addMessageToQueue(message);

}
};


// ----- APP INITIALIZATION -----

// Start listening on the PM2 BUS
pm2.launchBus(function (err, bus) {

// Listen for process logs
if (process.env.log) {
bus.on('log:out', function (data) {
// if (data.process.name === 'pm2-slack-plus') { return; } // Ignore messages of own module.
const parsedLog = parseIncommingLog(data.data);
slackUrlRouter.addMessage({
name: parseProcessName(data.process),
event: 'log',
description: parsedLog.description,
timestamp: parsedLog.timestamp,
});
});
}

// Listen for process errors
if (process.env.error) {
bus.on('log:err', function (data) {


if (data.data.includes('DeprecationWarning')) return;
// if (data.process.name === 'pm2-slack-plus') { return; } // Ignore messages of own module.
const parsedLog = parseIncommingLog(data.data);
slackUrlRouter.addMessage({
name: parseProcessName(data.process),
event: 'error',
description: parsedLog.description,
timestamp: parsedLog.timestamp,
});
});
}

// Listen for PM2 kill
if (process.env.kill) {
bus.on('pm2:kill', function (data) {
slackUrlRouter.addMessage({
name: 'PM2',
event: 'kill',
description: data.msg,
timestamp: Math.floor(Date.now() / 1000),
});
});
}

// Listen for process exceptions
if (process.env.exception) {
bus.on('process:exception', function (data) {
if (data.process.name === 'pm2-slack-plus') { return; } // Ignore messages of own module.

// If it is instance of Error, use it. If type is unknown, stringify it.
const description = (data.data && data.data.message) ? (data.data.code || '') + data.data.message : JSON.stringify(data.data);
slackUrlRouter.addMessage({
name: parseProcessName(data.process),
event: 'exception',
description: description,
timestamp: Math.floor(Date.now() / 1000),
});
});
}

// Listen for PM2 events
bus.on('process:event', function (data) {
if (!process.env[data.event] && data.event !== 'restart overlimit') { return; } // This event type is disabled by configuration.
if (data.process.name === 'pm2-slack-plus') { return; } // Ignore messages of own module.

let description = null;
let interactive = [];
switch (data.event) {
case 'start':
case 'online':
description = `${data.process.name} started`;
break;
case 'stop':
description = 'App stopped.';
break;
case 'restart':
description = 'App restarted.';
break;
case 'exit':
description = 'App closed.';
break;
case 'restart overlimit':
description = 'Process has been stopped. Check and fix the issue.';
break;

}
slackUrlRouter.addMessage({
name: parseProcessName(data.process),
event: data.event,
description: description,
interactive: interactive,
timestamp: Math.floor(Date.now() / 1000),
});
});
});

if (process.env["SLACK_SIGNING_SECRET"] && process.env["SLACK_BOT_TOKEN"] && process.env["SLACK_PORT"]) {
(async () => {

await app.start();

console.log(`Slack bot is ready. Listening on ${process.env["SLACK_PORT"]} port.`);
})();
}

/**
* @typedef {Object} Message
*
* @property {string} name - Process name
* @property {string} event - `start`|`stop`|`restart`|`error`|`exception`|`restart overlimit`| ...
* @property {string} description
* @property {number} timestamp - Linux timestamp format
*/
51 changes: 51 additions & 0 deletions message-queue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use strict";

// Dependency
const Scheduler = require('./scheduler');
const slackSender = require('./slack-sender');

/**
*
* @param {Object} config
* @param {boolean} config.buffer
* @param {number} config.buffer_seconds
* @param {number} config.buffer_max_seconds
* @param {number} config.queue_max
* @param {number} config.slack_url
* @constructor
*/
function MessageQueue(config) {
this.config = config;
this.messageQueue = [];
this.scheduler = new Scheduler(config);
}


/**
* Sends the message to Slack's Incoming Webhook.
* If buffer is enabled, the message is added to queue and sending is postponed for couple of seconds.
*
* @param {Message} message
*/
MessageQueue.prototype.addMessageToQueue = function(message) {
const self = this;

if (!this.config.buffer || !(this.config.buffer_seconds > 0)) {
// No sending buffer defined. Send directly to Slack.
slackSender.sendToSlack([message], self.config);
} else {
// Add message to buffer
this.messageQueue.push(message);
// Plan send the enqueued messages
this.scheduler.schedule(async function() {
// Remove waiting messages from global queue
const messagesToSend = self.messageQueue.splice(0, self.messageQueue.length);

slackSender.sendToSlack(messagesToSend, self.config);
});
}

}


module.exports = MessageQueue;
Loading

0 comments on commit 0cc53a4

Please sign in to comment.