Home Manual Reference Source Test API Healthcheck

src/helpers/healthcheck.helper.js

/**
 * @flow
 */
import _ from 'lodash';
import http from 'http';
import CONFIG, { DatabaseConfig, ExternalServiceConfig } from './config.helper.js';
import { MariaDBHelper } from './mariadb.helper.js';
import pack from '../../package.json';
import file from 'fs';
import path from 'path';

/**
 * Healthcheck Response Object containing the healthcheck name, status and if it errored or not
 * @type {HealthcheckResponse}
 */
interface HealthcheckResponse {
  name: ?string;
  status: ?string;
  error?: boolean;
};

interface FullResponse {
  L1: Array<HealthcheckResponse>;
  L2: Array<HealthcheckResponse>;
  L3: Array<HealthcheckResponse>;
}

/**
 * Retrieves the status of a database, based on the config given
 * @param  {DatabaseConfig} config config of database to check
 * @return {HealthcheckResponse}     Response from Database Healtcheck
 */
async function _getDatabaseStatus(config: DatabaseConfig): Promise<HealthcheckResponse> {
  const response: HealthcheckResponse = {
    name: config.serviceName,
    status: undefined
  };
  try {
    const helper: MariaDBHelper = new MariaDBHelper({
      config: _.merge(config, {
        connectionLimit: 1
      })
    });
    const status: string | boolean = await helper.getStatus();
    response.status = JSON.stringify(status);
    helper.shutdown();
  } catch (error) {
    response.error = true;
    response.status = error;
  }

  return response;
}

/**
 * Retrieves the status of an external service via http request to an endpoint, based on config given
 * @param       {ExternalServiceConfig} config config of external service
 * @return      {HealthcheckResponse}     Response from Database Healtcheck
 */
async function _getExternalServiceStatus(config: ExternalServiceConfig): Promise<HealthcheckResponse> {
  const protocol: string = config.protocol;
  const host: string = config.host;
  const port: string = _.get(config.healthcheck, 'port', config.port);
  const apiPath: string = _.get(config.healthcheck, 'path', config.path);

  const endpoint: string = `${protocol}://${host}:${port}/${apiPath}`;

  const promise = new Promise((resolve, reject) => { // eslint-disable-line
    const req = http.get(endpoint, (response) => { // eslint-disable-line
      response.on('data', (data: string) => {
        resolve(data);
      });
    });
    // TODO: Set Timeout length from Config (per service or have default?)
    req.setTimeout(5000, () => {
      req.destroy();
      reject(`Error: Timeout at ${endpoint}`);
    });
    req.on('error', (error: Error) => {
      reject(error);
    });
  });

  const response: HealthcheckResponse = {
    name: config.serviceName,
    status: undefined
  };
  try {
    // $FlowFixMe
    response.status += await promise;
  } catch (error) {
    response.error = true;
    response.status = error;
  }
  return response;
}

/**
 * Returns the current branch of the build (TODO: development vs production?)
 * @return {string} information about the current branch and build
 */
function getBranch(): string {
  const content: string = file.readFileSync(path.join(__dirname, '../../.git/HEAD'), { encoding: 'UTF-8' });

  const split: string[] = content.split('/');
  return split[split.length - 1];
}

/**
 * Class to wrap the methods that are used for server healthcheck
 */
class HealthcheckHelper {
  // Tried these with ES6 Map type but it made it much harder to do easy JSON manipulations
  serviceMap: { [name: string]: ExternalServiceConfig };
  dbMap: { [name: string]: DatabaseConfig };

  /**
   * Creates the Healthcheck helper by loading the configuration file and parsing the database and external
   * service configurations
   */
  constructor() {
    this.serviceMap = {};
    this.dbMap = {};
    _.each(CONFIG.EXTERNAL_SERVICES, (service: ExternalServiceConfig, name: string) => {
      if (service.serviceName === undefined) {
        service.serviceName = name;
      }
      this.serviceMap[name] = service;
    });

    _.each(CONFIG.DB, (service: DatabaseConfig, name: string) => {
      if (service.serviceName === undefined) {
        service.serviceName = name;
      }
      this.dbMap[name] = service;
    });
  }

  /**
   * Returns whether the service passed is running
   * @param  {string}  serviceName name of the service/database to check is running
   * @return {Boolean}             whether or not the service returns it is healthy
   */
  async isRunning(serviceName: string): Promise<HealthcheckResponse | false> {
    const conf: DatabaseConfig | ExternalServiceConfig = this.serviceMap[serviceName];
    // TODO: don't check if it exists, check if it is actually running
    if (conf !== undefined) {
      if (conf instanceof DatabaseConfig) {
        return await _getDatabaseStatus(conf);
      } else {
        return await _getExternalServiceStatus(conf);
      }
    }

    return false;
  }

  /**
   * Returns the status
   * @param {String} level of healthcheck status to view
   *  L1: this server + version
   *  L2: databases and other essential services we manage for the endpoints to work
   *  L3: external services we don't have control over and hopefully we catch errors for to explain to user
   * @returns {any} status of services in that level (or full status of server if no level provided)
   */
  async getStatus(level?: 'L1' | 'L2' | 'L3'): any { // eslint-disable-line flowtype/no-weak-types
    /* eslint-disable id-length */
    const status: FullResponse = {
      L1: [
        {
          // $FlowFixMe
          name: 'SERVER',
          // $FlowFixMe
          status: 'alive',
        },
        {
          // $FlowFixMe
          name: 'VERSION',
          status: pack.version
        },
        {
          // $FlowFixMe
          name: 'BRANCH',
          status: getBranch()
        }
      ],
      L2: await Promise.all(_.map(this.dbMap, _getDatabaseStatus)),
      L3: await Promise.all(_.map(this.serviceMap, _getExternalServiceStatus))
    };
    /* eslint-enable id-length */

    return level === undefined ? status : (status: any)[level]; // eslint-disable-line flowtype/no-weak-types
  }
}

/**
 * Exported Helperto interact with Healcheck Operations
 * @type {Healthcheck}
 */
const healthcheckHelper: HealthcheckHelper = new HealthcheckHelper();
export default healthcheckHelper;