/**
* @file express middleware to handle health checks
* @author Dominik Riedel <d.riedel@reply.de>
* @copyright Dominik Riedel 2018, MIT License
*/
'use strict';
const responseWrapper = require('./responseWrapper');
let errorCalls = 0;
/**
* @typedef HealthCheckConfiguration
* @type {Object}
* @property {string} serviceName - name of the service that will be appended to the trace id string
* @property {string} checkInternalErrors - whether or not to check for internal server errors on all endpoints. (default: true)
* @property {string} errorThreshold - the amount of internal server errors that may happen before health of service is considered unhealthy (default: 10)
* @property {string} routesToCheck - the actual routes that are supposed to be observed for ongoing error behavior (default: *)
* @property {string} responseCodesToCheck - the HTTP response codes that indicate an error behavior (default: 5XX)
*/
/**
* @callback RequestMiddleware
* @param {Object} req the express request object (see http://expressjs.com/en/4x/api.html#req)
* @param {Object} res the express response object (see http://expressjs.com/en/4x/api.html#res)
* @param {Object} next the next middleware object to be executed from the express middleware list
*/
/**
* Enables GET /health endpoint to retrieve the health of the service.
*
* This middleware also intercepts the response of every request made to the service to check if there is an ongoing problem
* with a steady response status of HTTP 500 (internal server error). If so the service health will switch to `unhealthy`.
*
* By default all API endpints (except the GET /health) endpoint will be monitored.
* By default the service will switch to and `unhealthy` state if the service will responde with HTTP status 500 for 10 sequential requests.
*
* @param {RequestIdConfiguration} options - configuration options for this middleware
* @returns {RequestMiddleware}
* @throws TypeError in case that the middleware has been passed incorrectly to express
* @throws TypeError if not passed an options object
* @throws TypeError if not given a serviceName with the options
*/
function healthCheck(options) {
// ensure that the middleware has not been passed as a function reference
if (arguments.length === 3) {
throw new TypeError('the requestId middleware has to be passed via app.use(healthCheck()); and not as a reference e.g. app.use(healthCheck);');
}
if (typeof options !== 'object') {
throw new TypeError('the healthCheck middleware expects to be passed an options object');
}
if (typeof options.serviceName !== 'string') {
throw new TypeError('you need to pass a serviceName to the healthCheck middleware');
}
if (typeof options.serviceVersion !== 'string') {
throw new TypeError('you need to pass a serviceVersion to the healthCheck middleware');
}
// overwrite default values (if any)
const {
serviceName,
serviceVersion,
checkInternalErrors,
errorThreshold
} = Object.assign({
checkInternalErrors: true,
errorThreshold: 10,
responseCodesToCheck: [500]
}, options);
const checkNameAndVersion = (query = {}) => {
const { name, version } = query;
// check service name
if (name && name !== serviceName) {
return false;
}
// check service version
if (version && version !== serviceVersion) {
return false;
}
return true;
};
return async (req, res, next) => {
// intercept on /health endpoint and return health result
if (req.path.toLowerCase() === '/health' && ['get', 'head'].indexOf(req.method.toLowerCase()) >= 0) {
if (!checkNameAndVersion(req.query) || (checkInternalErrors && errorCalls > errorThreshold)) {
return res.status(500).json({
service: serviceName,
status: 'unhealthy'
});
}
return res.status(200).json({
service: serviceName,
status: 'healthy'
});
}
// Circuit Beaker to check API reponses and set errorCalls
responseWrapper.wrapResponse(req, res, (wrappedRes) => {
const { statusCode } = wrappedRes;
if (req.path !== '/health' && statusCode === 500) errorCalls += 1;
if (req.path !== '/health' && statusCode !== 500) errorCalls = 0;
});
return next();
};
}
module.exports = healthCheck;