UNPKG

openhim-core

Version:

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

362 lines (302 loc) 12.9 kB
config = require "./config/config" config.alerts = config.get('alerts') logger = require "winston" contact = require './contact' moment = require 'moment' Q = require 'q' Channels = require('./model/channels') Channel = Channels.Channel Event = require('./model/events').Event ContactGroup = require('./model/contactGroups').ContactGroup Alert = require('./model/alerts').Alert User = require('./model/users').User authorisation = require('./middleware/authorisation') utils = require './utils' _ = require 'lodash' trxURL = (trx) -> "#{config.alerts.consoleURL}/#/transactions/#{trx.transactionID}" statusTemplate = (transactions, channel, alert) -> plain: -> """ OpenHIM Transactions Alert The following transaction(s) have completed with status #{alert.status} on the OpenHIM instance running on #{config.alerts.himInstance}: Channel - #{channel.name} #{(transactions.map (trx) -> trxURL trx).join '\n'} """ html: -> 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.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> """ sms: -> text = "Alert - " if transactions.length > 1 text += "#{transactions.length} transactions have" else if transactions.length is 1 text += "1 transaction has" else text += "no transactions have" text += " completed with status #{alert.status} on the OpenHIM running on #{config.alerts.himInstance} (#{channel.name})" maxRetriesTemplate = (transactions, channel, alert) -> plain: -> """ OpenHIM Transactions Alert - #{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: -> text = """ <html> <head></head> <body> <h1>OpenHIM Transactions Alert - #{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> """ sms: -> text = "Alert - " if transactions.length > 1 text += "#{transactions.length} transactions have" else if transactions.length is 1 text += "1 transaction has" text += " been retried #{channel.autoRetryMaxAttempts} times but are still failing on the OpenHIM on #{config.alerts.himInstance} (#{channel.name})" getAllChannels = (callback) -> Channel.find {}, callback findGroup = (groupID, callback) -> ContactGroup.findOne _id: groupID, callback findTransactions = (channel, dateFrom, status, callback) -> Event .find { created: $gte: dateFrom channelID: channel._id event: 'end' status: status }, { 'transactionID' } .hint created: 1 .exec callback countTotalTransactionsForChannel = (channel, dateFrom, callback) -> Event.count { created: $gte: dateFrom channelID: channel._id event: 'end' }, callback findOneAlert = (channel, alert, dateFrom, user, alertStatus, callback) -> criteria = { timestamp: { "$gte": dateFrom } channelID: channel._id condition: alert.condition status: if alert.condition is 'auto-retry-max-attempted' then '500' else alert.status alertStatus: alertStatus } criteria.user = user if user Alert .findOne criteria .exec callback findTransactionsMatchingCondition = (channel, alert, dateFrom, callback) -> if not alert.condition or alert.condition is 'status' findTransactionsMatchingStatus channel, alert, dateFrom, callback else if alert.condition is 'auto-retry-max-attempted' findTransactionsMaxRetried channel, alert, dateFrom, callback else callback new Error "Unsupported condition '#{alert.condition}'" findTransactionsMatchingStatus = (channel, alert, dateFrom, callback) -> pat = /\dxx/.exec alert.status if pat statusMatch = "$gte": alert.status[0]*100, "$lt": alert.status[0]*100+100 else statusMatch = alert.status dateToCheck = dateFrom # check last hour when using failureRate dateToCheck = moment().subtract(1, 'hours').toDate() if alert.failureRate? findTransactions channel, dateToCheck, statusMatch, (err, results) -> if not err and results? and alert.failureRate? # Get count of total transactions and work out failure ratio _countStart = new Date() countTotalTransactionsForChannel channel, dateToCheck, (err, count) -> logger.debug ".countTotalTransactionsForChannel: #{new Date()-_countStart} ms" return callback err, null if err failureRatio = results.length/count*100.0 if failureRatio >= alert.failureRate findOneAlert channel, alert, dateToCheck, null, 'Completed', (err, userAlert) -> return callback err, null if err # Has an alert already been sent this last hour? if userAlert? callback err, [] else callback err, utils.uniqArray results else callback err, [] else callback err, results findTransactionsMaxRetried = (channel, alert, dateFrom, callback) -> Event .find { created: $gte: dateFrom channelID: channel._id event: 'end' status: 500 autoRetryAttempt: channel.autoRetryMaxAttempts }, { 'transactionID' } .hint created: 1 .exec (err, transactions) -> return callback err if err callback null, _.uniqWith transactions, (a, b) -> a.transactionID.equals b.transactionID calcDateFromForUser = (user) -> if user.maxAlerts is '1 per hour' dateFrom = moment().subtract(1, 'hours').toDate() else if user.maxAlerts is '1 per day' dateFrom = moment().startOf('day').toDate() else null userAlreadyReceivedAlert = (channel, alert, user, callback) -> if not user.maxAlerts or user.maxAlerts is 'no max' # user gets all alerts callback null, false else dateFrom = calcDateFromForUser user return callback "Unsupported option 'maxAlerts=#{user.maxAlerts}'" if not dateFrom findOneAlert channel, alert, dateFrom, user.user, 'Completed', (err, userAlert) -> callback err ? null, if userAlert then true else false # 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. getTransactionsForAlert = (channel, alert, user, transactions, callback) -> if not user.maxAlerts or user.maxAlerts is 'no max' callback null, transactions else dateFrom = calcDateFromForUser user return callback "Unsupported option 'maxAlerts=#{user.maxAlerts}'" if not dateFrom findTransactionsMatchingCondition channel, alert, dateFrom, callback sendAlert = (channel, alert, user, transactions, contactHandler, done) -> User.findOne { email: user.user }, (err, dbUser) -> return done err if err return done "Cannot send alert: Unknown user '#{user.user}'" if not dbUser userAlreadyReceivedAlert channel, alert, user, (err, received) -> return done err, true if err return done null, true if received logger.info "Sending alert for user '#{user.user}' using method '#{user.method}'" getTransactionsForAlert channel, alert, user, transactions, (err, transactionsForAlert) -> template = statusTemplate transactionsForAlert, channel, alert if alert.condition is 'auto-retry-max-attempted' template = maxRetriesTemplate transactionsForAlert, channel, alert if user.method is 'email' plainMsg = template.plain() htmlMsg = template.html() contactHandler 'email', user.user, 'OpenHIM Alert', plainMsg, htmlMsg, done else if user.method is 'sms' return done "Cannot send alert: MSISDN not specified for user '#{user.user}'" if not dbUser.msisdn smsMsg = template.sms() contactHandler 'sms', dbUser.msisdn, 'OpenHIM Alert', smsMsg, null, done else return done "Unknown method '#{user.method}' specified for user '#{user.user}'" # Actions to take after sending an alert afterSendAlert = (err, channel, alert, user, transactions, skipSave, done) -> logger.error err if err if not skipSave alert = new Alert user: user.user method: user.method channelID: channel._id condition: alert.condition status: if alert.condition is 'auto-retry-max-attempted' then '500' else alert.status alertStatus: if err then 'Failed' else 'Completed' alert.save (err) -> logger.error err if err done() else done() 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. promises = [] _alertStart = new Date() if alert.groups for group in alert.groups groupDefer = Q.defer() findGroup group, (err, result) -> if err logger.error err groupDefer.resolve() else groupUserPromises = [] for user in result.users do (user) -> groupUserDefer = Q.defer() sendAlert channel, alert, user, transactions, contactHandler, (err, skipSave) -> afterSendAlert err, channel, alert, user, transactions, skipSave, -> groupUserDefer.resolve() groupUserPromises.push groupUserDefer.promise (Q.all groupUserPromises).then -> groupDefer.resolve() promises.push groupDefer.promise if alert.users for user in alert.users do (user) -> userDefer = Q.defer() sendAlert channel, alert, user, transactions, contactHandler, (err, skipSave) -> afterSendAlert err, channel, alert, user, transactions, skipSave, -> userDefer.resolve() promises.push userDefer.promise (Q.all promises).then -> logger.debug ".sendAlerts: #{new Date()-_alertStart} ms" done() alertingTask = (job, contactHandler, done) -> job.attrs.data = {} if not job.attrs.data lastAlertDate = job.attrs.data.lastAlertDate ? new Date() _taskStart = new Date() getAllChannels (err, results) -> promises = [] for channel in results if Channels.isChannelEnabled channel for alert in channel.alerts do (channel, alert) -> deferred = Q.defer() _findStart = new Date() findTransactionsMatchingCondition channel, alert, lastAlertDate, (err, results) -> logger.debug ".findTransactionsMatchingStatus: #{new Date()-_findStart} ms" if err logger.error err deferred.resolve() else if results? and results.length>0 sendAlerts channel, alert, results, contactHandler, -> deferred.resolve() else deferred.resolve() promises.push deferred.promise (Q.all promises).then -> job.attrs.data.lastAlertDate = new Date() logger.debug "Alerting task total time: #{new Date()-_taskStart} ms" done() setupAgenda = (agenda) -> agenda.define 'generate transaction alerts', (job, done) -> alertingTask job, contact.contactUser, done agenda.every "#{config.alerts.pollPeriodMinutes} minutes", 'generate transaction alerts' exports.setupAgenda = setupAgenda if process.env.NODE_ENV == "test" exports.findTransactionsMatchingStatus = findTransactionsMatchingStatus exports.findTransactionsMaxRetried = findTransactionsMaxRetried exports.alertingTask = alertingTask