Source: index.js

/**
 * @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;