openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
455 lines (349 loc) • 17.9 kB
text/coffeescript
transactions = require '../model/transactions'
events = require '../middleware/events'
Channel = require('../model/channels').Channel
Client = require('../model/clients').Client
autoRetryUtils = require '../autoRetry'
Q = require 'q'
logger = require 'winston'
authorisation = require './authorisation'
utils = require "../utils"
config = require '../config/config'
statsd_client = require "statsd-client"
statsd_server = config.get 'statsd'
sdc = new statsd_client statsd_server
application = config.get 'application'
apiConf = config.get 'api'
os = require "os"
timer = new Date()
domain = os.hostname() + '.' + application.name
hasError = (updates) ->
if updates.error? then return true
if updates.routes?
error = false
updates.routes.forEach (route) ->
if route.error then error = true
if error then return true
if updates.$push?.routes?.error? then return true
return false
getChannelIDsArray = (channels) ->
channelIDs = []
for channel in channels
channelIDs.push channel._id.toString()
return channelIDs
# function to construct projection object
getProjectionObject = (filterRepresentation) ->
switch filterRepresentation
when "simpledetails"
# view minimum required data for transaction details view
return { "request.body": 0, "response.body": 0, "routes.request.body": 0, "routes.response.body": 0, "orchestrations.request.body": 0, "orchestrations.response.body": 0 }
when "full"
# view all transaction data
return {}
when "fulltruncate"
# same as full
return {}
when "bulkrerun"
# view only 'bulkrerun' properties
return { "_id": 1, "childIDs": 1, "canRerun": 1, "channelID": 1 }
else
# no filterRepresentation supplied - simple view
# view minimum required data for transactions
return { "request.body": 0, "request.headers": 0, "response.body": 0, "response.headers": 0, orchestrations: 0, routes: 0 }
truncateTransactionDetails = (trx) ->
truncateSize = apiConf.truncateSize ? 15000
truncateAppend = apiConf.truncateAppend ? "\n[truncated ...]"
trunc = (t) ->
if t.request?.body? and t.request.body.length > truncateSize
t.request.body = t.request.body[...truncateSize] + truncateAppend
if t.response?.body? and t.response.body.length > truncateSize
t.response.body = t.response.body[...truncateSize] + truncateAppend
trunc trx
if trx.routes?
trunc r for r in trx.routes
if trx.orchestrations?
trunc o for o in trx.orchestrations
###
# Retrieves the list of transactions
###
exports.getTransactions = ->
try
filtersObject = this.request.query
#get limit and page values
filterLimit = filtersObject.filterLimit
filterPage = filtersObject.filterPage
filterRepresentation = filtersObject.filterRepresentation
#remove limit/page/filterRepresentation values from filtersObject (Not apart of filtering and will break filter if present)
delete filtersObject.filterLimit
delete filtersObject.filterPage
delete filtersObject.filterRepresentation
#determine skip amount
filterSkip = filterPage*filterLimit
# get filters object
filters = if filtersObject.filters? then JSON.parse filtersObject.filters else {}
# Test if the user is authorised
if not authorisation.inGroup 'admin', this.authenticated
# if not an admin, restrict by transactions that this user can view
channels = yield authorisation.getUserViewableChannels this.authenticated
if not filtersObject.channelID
filters.channelID = $in: getChannelIDsArray channels
else if filtersObject.channelID not in getChannelIDsArray channels
return utils.logAndSetResponse this, 403, "Forbidden: Unauthorized channel #{filtersObject.channelID}", 'info'
# set 'filterRepresentation' to default if user isnt admin
filterRepresentation = ''
# get projection object
projectionFiltersObject = getProjectionObject filterRepresentation
if filtersObject.channelID
filters.channelID = filtersObject.channelID
# parse date to get it into the correct format for querying
if filters['request.timestamp']
filters['request.timestamp'] = JSON.parse filters['request.timestamp']
### Transaction Filters ###
# build RegExp for transaction request path filter
if filters['request.path']
filters['request.path'] = new RegExp filters['request.path'], "i"
# build RegExp for transaction request querystring filter
if filters['request.querystring']
filters['request.querystring'] = new RegExp filters['request.querystring'], "i"
# response status pattern match checking
if filters['response.status'] && utils.statusCodePatternMatch( filters['response.status'] )
filters['response.status'] = "$gte": filters['response.status'][0]*100, "$lt": filters['response.status'][0]*100+100
# check if properties exist
if filters['properties']
# we need to source the property key and re-construct filter
key = Object.keys(filters['properties'])[0]
filters['properties.'+key] = filters['properties'][key]
# if property has no value then check if property exists instead
if filters['properties'][key] is null
filters['properties.'+key] = { '$exists': true }
# delete the old properties filter as its not needed
delete filters['properties']
# parse childIDs.0 query to get it into the correct format for querying
# .0 is first index of array - used to validate if empty or not
if filters['childIDs.0']
filters['childIDs.0'] = JSON.parse filters['childIDs.0']
### Route Filters ###
# build RegExp for route request path filter
if filters['routes.request.path']
filters['routes.request.path'] = new RegExp filters['routes.request.path'], "i"
# build RegExp for transaction request querystring filter
if filters['routes.request.querystring']
filters['routes.request.querystring'] = new RegExp filters['routes.request.querystring'], "i"
# route response status pattern match checking
if filters['routes.response.status'] && utils.statusCodePatternMatch( filters['routes.response.status'] )
filters['routes.response.status'] = "$gte": filters['routes.response.status'][0]*100, "$lt": filters['routes.response.status'][0]*100+100
### orchestration Filters ###
# build RegExp for orchestration request path filter
if filters['orchestrations.request.path']
filters['orchestrations.request.path'] = new RegExp filters['orchestrations.request.path'], "i"
# build RegExp for transaction request querystring filter
if filters['orchestrations.request.querystring']
filters['orchestrations.request.querystring'] = new RegExp filters['orchestrations.request.querystring'], "i"
# orchestration response status pattern match checking
if filters['orchestrations.response.status'] && utils.statusCodePatternMatch( filters['orchestrations.response.status'] )
filters['orchestrations.response.status'] = "$gte": filters['orchestrations.response.status'][0]*100, "$lt": filters['orchestrations.response.status'][0]*100+100
# execute the query
this.body = yield transactions.Transaction
.find filters, projectionFiltersObject
.skip filterSkip
.limit parseInt filterLimit
.sort 'request.timestamp': -1
.exec()
if filterRepresentation is 'fulltruncate'
truncateTransactionDetails trx for trx in this.body
catch e
utils.logAndSetResponse this, 500, "Could not retrieve transactions via the API: #{e}", 'error'
###
# Adds an transaction
###
exports.addTransaction = ->
# Test if the user is authorised
if not authorisation.inGroup 'admin', this.authenticated
utils.logAndSetResponse this, 403, "User #{this.authenticated.email} is not an admin, API access to addTransaction denied.", 'info'
return
# Get the values to use
transactionData = this.request.body
tx = new transactions.Transaction transactionData
try
# Try to add the new transaction (Call the function that emits a promise and Koa will wait for the function to complete)
yield Q.ninvoke tx, "save"
this.status = 201
logger.info "User #{this.authenticated.email} created transaction with id #{tx.id}"
generateEvents tx, tx.channelID
catch e
utils.logAndSetResponse this, 500, "Could not add a transaction via the API: #{e}", 'error'
###
# Retrieves the details for a specific transaction
###
exports.getTransactionById = (transactionId) ->
# Get the values to use
transactionId = unescape transactionId
try
filtersObject = this.request.query
filterRepresentation = filtersObject.filterRepresentation
#remove filterRepresentation values from filtersObject (Not apart of filtering and will break filter if present)
delete filtersObject.filterRepresentation
# set filterRepresentation to 'full' if not supplied
if not filterRepresentation then filterRepresentation = 'full'
# --------------Check if user has permission to view full content----------------- #
# if user NOT admin, determine their representation privileges.
if not authorisation.inGroup 'admin', this.authenticated
# retrieve transaction channelID
txChannelID = yield transactions.Transaction.findById(transactionId, channelID: 1, _id: 0).exec()
if txChannelID?.length is 0
this.body = "Could not find transaction with ID: #{transactionId}"
this.status = 404
return
else
# assume user is not allowed to view all content - show only 'simpledetails'
filterRepresentation = 'simpledetails'
# get channel.txViewFullAcl information by channelID
channel = yield Channel.findById(txChannelID.channelID, txViewFullAcl: 1, _id: 0).exec()
# loop through user groups
for group in this.authenticated.groups
# if user role found in channel txViewFullAcl - user has access to view all content
if channel.txViewFullAcl.indexOf(group) >= 0
# update filterRepresentation object to be 'full' and allow all content
filterRepresentation = 'full'
break
# --------------Check if user has permission to view full content----------------- #
# get projection object
projectionFiltersObject = getProjectionObject filterRepresentation
result = yield transactions.Transaction.findById(transactionId, projectionFiltersObject).exec()
if result and filterRepresentation is 'fulltruncate'
truncateTransactionDetails result
# Test if the result if valid
if not result
this.body = "Could not find transaction with ID: #{transactionId}"
this.status = 404
# Test if the user is authorised
else if not authorisation.inGroup 'admin', this.authenticated
channels = yield authorisation.getUserViewableChannels this.authenticated
if getChannelIDsArray(channels).indexOf(result.channelID.toString()) >= 0
this.body = result
else
utils.logAndSetResponse this, 403, "User #{this.authenticated.email} is not authenticated to retrieve transaction #{transactionId}", 'info'
else
this.body = result
catch e
utils.logAndSetResponse this, 500, "Could not get transaction by ID via the API: #{e}", 'error'
###
# Retrieves all transactions specified by clientId
###
exports.findTransactionByClientId = (clientId) ->
clientId = unescape clientId
try
filtersObject = this.request.query
filterRepresentation = filtersObject.filterRepresentation
# get projection object
projectionFiltersObject = getProjectionObject filterRepresentation
filtersObject = {}
filtersObject.clientID = clientId
# Test if the user is authorised
if not authorisation.inGroup 'admin', this.authenticated
# if not an admin, restrict by transactions that this user can view
channels = yield authorisation.getUserViewableChannels this.authenticated
filtersObject.channelID = $in: getChannelIDsArray channels
# set 'filterRepresentation' to default if user isnt admin
filterRepresentation = ''
# execute the query
this.body = yield transactions.Transaction
.find filtersObject, projectionFiltersObject
.sort 'request.timestamp': -1
.exec()
catch e
utils.logAndSetResponse this, 500, "Could not get transaction by clientID via the API: #{e}", 'error'
generateEvents = (transaction, channelID) ->
Channel.findById channelID, (err, channel) ->
logger.debug "Storing events for transaction: #{transaction._id}"
trxEvents = []
done = (err) -> logger.error err if err
events.createTransactionEvents trxEvents, transaction, channel
if trxEvents.length > 0
events.saveEvents trxEvents, done
updateTransactionMetrics = (updates, doc) ->
if updates['$push']?.routes?
for k, route of updates['$push']
do (route) ->
if route.metrics?
for metric in route.metrics
if metric.type == 'counter'
logger.debug "incrementing mediator counter #{metric.name}"
sdc.increment "#{domain}.channels.#{doc.channelID}.#{route.name}.mediator_metrics.#{metric.name}"
if metric.type == 'timer'
logger.debug "incrementing mediator timer #{metric.name}"
sdc.timing "#{domain}.channels.#{doc.channelID}.#{route.name}.mediator_metrics.#{metric.name}", metric.value
if metric.type == 'gauge'
logger.debug "incrementing mediator gauge #{metric.name}"
sdc.gauge "#{domain}.channels.#{doc.channelID}.#{route.name}.mediator_metrics.#{metric.name}", metric.value
for orchestration in route.orchestrations
do (orchestration) ->
orchestrationDuration = orchestration.response.timestamp - orchestration.request.timestamp
orchestrationStatus = orchestration.response.status
orchestrationName = orchestration.name
if orchestration.group
orchestrationName = "#{orchestration.group}.#{orchestration.name}" #Namespace it by group
###
# Update timers
###
logger.debug 'updating async route timers'
sdc.timing "#{domain}.channels.#{doc.channelID}.#{route.name}.orchestrations.#{orchestrationName}", orchestrationDuration
sdc.timing "#{domain}.channels.#{doc.channelID}.#{route.name}.orchestrations.#{orchestrationName}.statusCodes.#{orchestrationStatus}" , orchestrationDuration
###
# Update counters
###
logger.debug 'updating async route counters'
sdc.increment "#{domain}.channels.#{doc.channelID}.#{route.name}.orchestrations.#{orchestrationName}"
sdc.increment "#{domain}.channels.#{doc.channelID}.#{route.name}.orchestrations.#{orchestrationName}.statusCodes.#{orchestrationStatus}"
if orchestration.metrics?
for metric in orchestration.metrics
if metric.type == 'counter'
logger.debug "incrementing #{route.name} orchestration counter #{metric.name}"
sdc.increment "#{domain}.channels.#{doc.channelID}.#{route.name}.orchestrations.#{orchestrationName}.#{metric.name}", metric.value
if metric.type == 'timer'
logger.debug "incrementing #{route.name} orchestration timer #{metric.name}"
sdc.timing "#{domain}.channels.#{doc.channelID}.#{route.name}.orchestrations.#{orchestrationName}.#{metric.name}", metric.value
if metric.type == 'gauge'
logger.debug "incrementing #{route.name} orchestration gauge #{metric.name}"
sdc.gauge "#{domain}.channels.#{doc.channelID}.#{route.name}.orchestrations.#{orchestrationName}.#{metric.name}", metric.value
###
# Updates a transaction record specified by transactionId
###
exports.updateTransaction = (transactionId) ->
# Test if the user is authorised
if not authorisation.inGroup 'admin', this.authenticated
utils.logAndSetResponse this, 403, "User #{this.authenticated.email} is not an admin, API access to updateTransaction denied.", 'info'
return
transactionId = unescape transactionId
updates = this.request.body
try
if hasError updates
tx = yield transactions.Transaction.findById(transactionId).exec()
channel = yield Channel.findById(tx.channelID).exec()
if not autoRetryUtils.reachedMaxAttempts tx, channel
updates.autoRetry = true
autoRetryUtils.queueForRetry tx
tx = yield transactions.Transaction.findByIdAndUpdate(transactionId, updates, new: true).exec()
this.body = "Transaction with ID: #{transactionId} successfully updated"
this.status = 200
logger.info "User #{this.authenticated.email} updated transaction with id #{transactionId}"
generateEvents updates, tx.channelID
updateTransactionMetrics updates, tx
catch e
utils.logAndSetResponse this, 500, "Could not update transaction via the API: #{e}", 'error'
###
# Removes a transaction
###
exports.removeTransaction = (transactionId) ->
# Test if the user is authorised
if not authorisation.inGroup 'admin', this.authenticated
utils.logAndSetResponse this, 403, "User #{this.authenticated.email} is not an admin, API access to removeTransaction denied.", 'info'
return
# Get the values to use
transactionId = unescape transactionId
try
yield transactions.Transaction.findByIdAndRemove(transactionId).exec()
this.body = 'Transaction successfully deleted'
this.status = 200
logger.info "User #{this.authenticated.email} removed transaction with id #{transactionId}"
catch e
utils.logAndSetResponse this, 500, "Could not remove transaction via the API: #{e}", 'error'