openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
362 lines (302 loc) • 12.9 kB
text/coffeescript
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