UNPKG

@kulakovdmitr/pm2-slack

Version:

A PM2 module to emit events to Slack and configure settings

210 lines (174 loc) 6.9 kB
'use strict'; // Dependency const pm2 = require('pm2'); const pmx = require('pmx'); const MessageQueue = require('./message-queue'); /** * Get the configuration from PM2 * * @type {Object} * @property {boolean} exception */ const moduleConfig = pmx.initModule(); /** * New PM2 is storing log messages with date in format "YYYY-MM-DD hh:mm:ss +-zz:zz" * Parses this date from begin of message * * @param {string} logMessage * @returns {{description:string|null, timestamp:number|null}} */ function parseIncommingLog(logMessage) { let description = null; let timestamp = null; if (typeof logMessage === "string") { // Parse date on begin (if exists) const dateRegex = /([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{1,2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})? [+\-]?[0-9]{1,2}:[0-9]{2}(\.[0-9]{3})?)[:\-\s]+/; const parsedDescription = dateRegex.exec(logMessage); // Note: The `parsedDescription[0]` is datetime with separator(s) on the end. // The `parsedDescription[1]` is datetime only (without separators). // The `parsedDescription[2]` are ".microseconds" if (parsedDescription && parsedDescription.length >= 2) { // Use timestamp from message timestamp = Math.floor(Date.parse(parsedDescription[1]) / 1000); // Use message without date description = logMessage.replace(parsedDescription[0], ""); } else { // Use whole original message description = logMessage; } } return { description: description, timestamp: timestamp } } 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 = moduleConfig['slack_url-' + processName] || moduleConfig['slack_url']; if (!slackUrl) { return; // No Slack URL defined for this process and no global Slack URL exists. } 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] = moduleConfig[configPropertyName + '-' + processName] || moduleConfig[configPropertyName]; }); this.messageQueues[slackUrl] = new MessageQueue(config); } this.messageQueues[slackUrl].addMessageToQueue(message); } }; /** * Get pm2 app display name. * If the app is running in cluster mode, id will append [pm_id] as the suffix. * * @param {object} process * @returns {string} name */ function parseProcessName(process) { return process.name + (process.exec_mode === 'cluster_mode' && process.instances > 1 ? `[${process.pm_id}]` : ''); } // ----- APP INITIALIZATION ----- // Start listening on the PM2 BUS pm2.launchBus(function(err, bus) { // Listen for process logs if (moduleConfig.log) { bus.on('log:out', function(data) { if (data.process.name === '@kulakovdmitr/pm2-slack') { 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 (moduleConfig.error) { bus.on('log:err', function(data) { if (data.process.name === '@kulakovdmitr/pm2-slack') { 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 (moduleConfig.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 (moduleConfig.exception) { bus.on('process:exception', function(data) { if (data.process.name === '@kulakovdmitr/pm2-slack') { 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 (!moduleConfig[data.event]) { return; } // This event type is disabled by configuration. if (data.process.name === '@kulakovdmitr/pm2-slack') { return; } // Ignore messages of own module. let description = null; switch (data.event) { case 'start': description = 'My own App Has Been Started!!'; break; case 'stop': case 'restart': description = null; 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, timestamp: Math.floor(Date.now() / 1000), }); }); }); /** * @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 */