UNPKG

openhim-core

Version:

The OpenHIM core application that provides logging and routing of http requests

516 lines (408 loc) 16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setupAgenda = setupAgenda; var _winston = _interopRequireDefault(require("winston")); var _moment = _interopRequireDefault(require("moment")); var _lodash = _interopRequireDefault(require("lodash")); var contact = _interopRequireWildcard(require("./contact")); var _config = require("./config"); var _events = require("./model/events"); var _contactGroups = require("./model/contactGroups"); var _alerts = require("./model/alerts"); var _users = require("./model/users"); var utils = _interopRequireWildcard(require("./utils")); var Channels = _interopRequireWildcard(require("./model/channels")); function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } _config.config.alerts = _config.config.get('alerts'); const { ChannelModel } = Channels; const trxURL = trx => `${_config.config.alerts.consoleURL}/#!/transactions/${trx.transactionID}`; const statusTemplate = (transactions, channel, alert) => ({ plain() { return `\ OpenHIM Transactions Alert The following transaction(s) have completed with status ${alert.status} on the OpenHIM instance running on ${_config.config.alerts.himInstance}: Channel - ${channel.name} ${transactions.map(trx => trxURL(trx)).join('\n')} \ `; }, html() { let text = `\ <html> <head></head> <body> <h1>OpenHIM Transactions Alert</h1> <div> <p>The following transaction(s) have completed with status <b>${alert.status}</b> on the OpenHIM instance running on <b>${_config.config.alerts.himInstance}</b>:</p> <table> <tr><td>Channel - <b>${channel.name}</b></td></td>\n\ `; text += transactions.map(trx => ` <tr><td><a href='${trxURL(trx)}'>${trxURL(trx)}</a></td></tr>`).join('\n'); text += '\n'; text += `\ </table> </div> </body> </html>\ `; return text; }, sms() { let text = 'Alert - '; if (transactions.length > 1) { text += `${transactions.length} transactions have`; } else if (transactions.length === 1) { text += '1 transaction has'; } else { text += 'no transactions have'; } text += ` completed with status ${alert.status} on the OpenHIM running on ${_config.config.alerts.himInstance} (${channel.name})`; return text; } }); const maxRetriesTemplate = (transactions, channel) => ({ plain() { return `\ OpenHIM Transactions Alert - ${_config.config.alerts.himInstance} The following transaction(s) have been retried ${channel.autoRetryMaxAttempts} times, but are still failing: Channel - ${channel.name} ${transactions.map(trx => trxURL(trx)).join('\n')} Please note that they will not be retried any further by the OpenHIM automatically.\ `; }, html() { let text = `\ <html> <head></head> <body> <h1>OpenHIM Transactions Alert - ${_config.config.alerts.himInstance}</h1> <div> <p>The following transaction(s) have been retried <b>${channel.autoRetryMaxAttempts}</b> times, but are still failing:</p> <table> <tr><td>Channel - <b>${channel.name}</b></td></td>\n\ `; text += transactions.map(trx => ` <tr><td><a href='${trxURL(trx)}'>${trxURL(trx)}</a></td></tr>`).join('\n'); text += '\n'; text += `\ </table> <p>Please note that they will not be retried any further by the OpenHIM automatically.</p> </div> </body> </html>\ `; return text; }, sms() { let text = 'Alert - '; if (transactions.length > 1) { text += `${transactions.length} transactions have`; } else if (transactions.length === 1) { text += '1 transaction has'; } text += ` been retried ${channel.autoRetryMaxAttempts} times but are still failing on the OpenHIM on ${_config.config.alerts.himInstance} (${channel.name})`; return text; } }); const getAllChannels = callback => ChannelModel.find({}, callback); const findGroup = (groupID, callback) => _contactGroups.ContactGroupModel.findOne({ _id: groupID }, callback); const findTransactions = (channel, dateFrom, status, callback) => _events.EventModel.find({ created: { $gte: dateFrom }, channelID: channel._id, event: 'end', status, type: 'channel' }, { transactionID: 'transactionID' }).hint({ created: 1 }).exec(callback); const countTotalTransactionsForChannel = (channel, dateFrom, callback) => _events.EventModel.countDocuments({ created: { $gte: dateFrom }, channelID: channel._id, type: 'channel', event: 'end' }, callback); function findOneAlert(channel, alert, dateFrom, user, alertStatus, callback) { const criteria = { timestamp: { $gte: dateFrom }, channelID: channel._id, condition: alert.condition, status: alert.condition === 'auto-retry-max-attempted' ? '500' : alert.status, alertStatus }; if (user) { criteria.user = user; } return _alerts.AlertModel.findOne(criteria).exec(callback); } function findTransactionsMatchingStatus(channel, alert, dateFrom, callback) { let statusMatch; const pat = /\dxx/.exec(alert.status); if (pat) { statusMatch = { $gte: alert.status[0] * 100, $lt: alert.status[0] * 100 + 100 }; } else { statusMatch = alert.status; } let dateToCheck = dateFrom; // check last hour when using failureRate if (alert.failureRate != null) { dateToCheck = (0, _moment.default)().subtract(1, 'hours').toDate(); } return findTransactions(channel, dateToCheck, statusMatch, (err, results) => { if (!err && results != null && alert.failureRate != null) { // Get count of total transactions and work out failure ratio const _countStart = new Date(); return countTotalTransactionsForChannel(channel, dateToCheck, (err, count) => { _winston.default.debug(`.countTotalTransactionsForChannel: ${new Date() - _countStart} ms`); if (err) { return callback(err, null); } const failureRatio = results.length / count * 100.0; if (failureRatio >= alert.failureRate) { return findOneAlert(channel, alert, dateToCheck, null, 'Completed', (err, userAlert) => { if (err) { return callback(err, null); } // Has an alert already been sent this last hour? if (userAlert != null) { return callback(err, []); } return callback(err, utils.uniqArray(results)); }); } return callback(err, []); }); } return callback(err, results); }); } const findTransactionsMaxRetried = (channel, alert, dateFrom, callback) => _events.EventModel.find({ created: { $gte: dateFrom }, channelID: channel._id, event: 'end', type: 'channel', status: 500, autoRetryAttempt: channel.autoRetryMaxAttempts }, { transactionID: 'transactionID' }) // .hint({created: 1}) .exec((err, transactions) => { if (err) { return callback(err); } return callback(null, _lodash.default.uniqWith(transactions, (a, b) => a.transactionID.equals(b.transactionID))); }); function findTransactionsMatchingCondition(channel, alert, dateFrom, callback) { if (!alert.condition || alert.condition === 'status') { return findTransactionsMatchingStatus(channel, alert, dateFrom, callback); } else if (alert.condition === 'auto-retry-max-attempted') { return findTransactionsMaxRetried(channel, alert, dateFrom, callback); } return callback(new Error(`Unsupported condition '${alert.condition}'`)); } function calcDateFromForUser(user) { if (user.maxAlerts === '1 per hour') { return (0, _moment.default)().subtract(1, 'hours').toDate(); } else if (user.maxAlerts === '1 per day') { return (0, _moment.default)().startOf('day').toDate(); } return null; } function userAlreadyReceivedAlert(channel, alert, user, callback) { if (!user.maxAlerts || user.maxAlerts === 'no max') { // user gets all alerts return callback(null, false); } const dateFrom = calcDateFromForUser(user); if (!dateFrom) { return callback(new Error(`Unsupported option 'maxAlerts=${user.maxAlerts}'`)); } return findOneAlert(channel, alert, dateFrom, user.user, 'Completed', (err, userAlert) => callback(err != null ? err : null, !!userAlert)); } // Setup the list of transactions for alerting. // // Fetch earlier transactions if a user is setup with maxAlerts. // If the user has no maxAlerts limit, then the transactions object is returned as is. function getTransactionsForAlert(channel, alert, user, transactions, callback) { if (!user.maxAlerts || user.maxAlerts === 'no max') { return callback(null, transactions); } const dateFrom = calcDateFromForUser(user); if (!dateFrom) { return callback(new Error(`Unsupported option 'maxAlerts=${user.maxAlerts}'`)); } return findTransactionsMatchingCondition(channel, alert, dateFrom, callback); } const sendAlert = (channel, alert, user, transactions, contactHandler, done) => _users.UserModel.findOne({ email: user.user }, (err, dbUser) => { if (err) { return done(err); } if (!dbUser) { return done(`Cannot send alert: Unknown user '${user.user}'`); } return userAlreadyReceivedAlert(channel, alert, user, (err, received) => { if (err) { return done(err, true); } if (received) { return done(null, true); } _winston.default.info(`Sending alert for user '${user.user}' using method '${user.method}'`); return getTransactionsForAlert(channel, alert, user, transactions, (err, transactionsForAlert) => { if (err) { done(err); } let template = statusTemplate(transactionsForAlert, channel, alert); if (alert.condition === 'auto-retry-max-attempted') { template = maxRetriesTemplate(transactionsForAlert, channel, alert); } if (user.method === 'email') { const plainMsg = template.plain(); const htmlMsg = template.html(); return contactHandler('email', user.user, 'OpenHIM Alert', plainMsg, htmlMsg, done); } else if (user.method === 'sms') { if (!dbUser.msisdn) { return done(`Cannot send alert: MSISDN not specified for user '${user.user}'`); } const smsMsg = template.sms(); return contactHandler('sms', dbUser.msisdn, 'OpenHIM Alert', smsMsg, null, done); } return done(`Unknown method '${user.method}' specified for user '${user.user}'`); }); }); }); // Actions to take after sending an alert function afterSendAlert(err, channel, alert, user, transactions, skipSave, done) { if (err) { _winston.default.error(err); } if (!skipSave) { alert = new _alerts.AlertModel({ user: user.user, method: user.method, channelID: channel._id, condition: alert.condition, status: alert.condition === 'auto-retry-max-attempted' ? '500' : alert.status, alertStatus: err ? 'Failed' : 'Completed' }); return alert.save(err => { if (err) { _winston.default.error(err); } return done(); }); } return done(); } function sendAlerts(channel, alert, transactions, contactHandler, done) { // Each group check creates one promise that needs to be resolved. // For each group, the promise is only resolved when an alert is sent and stored // for each user in that group. This resolution is managed by a promise set for that group. // // For individual users in the alert object (not part of a group), // a promise is resolved per user when the alert is both sent and stored. const promises = []; const _alertStart = new Date(); if (alert.groups) { for (const group of Array.from(alert.groups)) { const groupDefer = new Promise((resolve, reject) => { findGroup(group, (err, result) => { if (err) { _winston.default.error(err); return resolve(); } const groupUserPromises = Array.from(result.users).map(user => { return new Promise(resolve => { sendAlert(channel, alert, user, transactions, contactHandler, (err, skipSave) => { afterSendAlert(err, channel, alert, user, transactions, skipSave, () => resolve()); }); }); }); return Promise.all(groupUserPromises).then(() => resolve()); }); }); promises.push(groupDefer); } } if (alert.users) { Array.from(alert.users).forEach(user => { const userDefer = new Promise(resolve => { sendAlert(channel, alert, user, transactions, contactHandler, (err, skipSave) => { afterSendAlert(err, channel, alert, user, transactions, skipSave, () => resolve()); }); }); promises.push(userDefer); }); } return Promise.all(promises).then(() => { _winston.default.debug(`.sendAlerts: ${new Date() - _alertStart} ms`); return done(); }); } function alertingTask(job, contactHandler, done) { if (!job.attrs.data) { job.attrs.data = {}; } const lastAlertDate = job.attrs.data.lastAlertDate != null ? job.attrs.data.lastAlertDate : new Date(); const _taskStart = new Date(); return getAllChannels((err, results) => { if (err) { return done(err); } const promises = []; for (const channel of Array.from(results)) { if (Channels.isChannelEnabled(channel)) { for (const alert of Array.from(channel.alerts)) { (function (channel, alert) { const deferred = new Promise(resolve => { const _findStart = new Date(); findTransactionsMatchingCondition(channel, alert, lastAlertDate, (err, results) => { _winston.default.debug(`.findTransactionsMatchingStatus: ${new Date() - _findStart} ms`); if (err) { _winston.default.error(err); return resolve(); } else if (results != null && results.length > 0) { return sendAlerts(channel, alert, results, contactHandler, () => resolve()); } return resolve(); }); }); return promises.push(deferred); })(channel, alert); } } } return Promise.all(promises).then(() => { job.attrs.data.lastAlertDate = new Date(); _winston.default.debug(`Alerting task total time: ${new Date() - _taskStart} ms`); return done(); }); }); } function setupAgenda(agenda) { agenda.define('generate transaction alerts', (job, done) => alertingTask(job, contact.contactUser, done)); return agenda.every(`${_config.config.alerts.pollPeriodMinutes} minutes`, 'generate transaction alerts'); } if (process.env.NODE_ENV === 'test') { exports.findTransactionsMatchingStatus = findTransactionsMatchingStatus; exports.findTransactionsMaxRetried = findTransactionsMaxRetried; exports.alertingTask = alertingTask; } //# sourceMappingURL=alerts.js.map