rpgeeganage/ifto

View on GitHub
lib/ifto.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { Context } from 'aws-lambda';
import { Spy } from './spy_modules';
import { formatedDate } from './util';

/**
 * Function signature of the expected output function
 */
export type OutputFunction = (...arg: any[]) => void;

/**
 * Environment variable to indicate monitoring should start
 */
export const ENV_VAR_MONITORING_START_NAME = 'ifto_start';

/**
 * Environment variable to enable to disable monitoring
 */
export const ENV_VAR_MONITORING_START_VALUE = 'true';

/**
 * Environment variable to configure "flushLogsWhenDifferenceLessThanMilliseconds"
 */
export const ENV_VAR_FLUSH_LOGS_WHEN_DIFFERENCE_LESS_THAN_MILLISECONDS =
  'ifto_flush_when';

/**
 * Extended Global variable.
 * Holds the Ifto instance
 * @export
 * @interface ExtendedNodeJsGlobal
 * @extends {NodeJS.Global}
 */
export interface ExtendedNodeJsGlobal extends NodeJS.Global {
  Ifto?: Ifto;
}

/**
 * Option interface
 *
 * @interface Options
 */
interface Options {
  flushLogsWhenDifferenceLessThanMilliseconds: number;
  output: OutputFunction;
}

/**
 * Default options
 */
export const defaults: Options = {
  flushLogsWhenDifferenceLessThanMilliseconds: 50,
  output: console.log
};

/**
 * If lambda Timeout (Ifto) main class.
 *
 * @export
 * @class Ifto
 */
export class Ifto {
  /**
   * flag indicates monitoring should enable or not
   *
   * @private
   * @memberof Ifto
   */
  private allowedToMonitor = false;

  /**
   * Holds the log entries
   *
   * @private
   * @type {string[]}
   * @memberof Ifto
   */
  private logEntries: string[] = [];

  /**
   * This holds minimum time difference (in milliseconds) value to flush the logs.
   *
   * Lambda context contains 'getRemainingTimeInMillis()' function which give how many milliseconds
   * left for execution. flushLogsWhenDifferenceLessThanMilliseconds defines minimum milliseconds left,
   * before flushing logs.
   * Eg:
   * if (context.getRemainingTimeInMillis() <= flushLogsWhenDifferenceLessThanMilliseconds) {
   *  flush the logs
   * }
   *
   * @private
   * @type {number}
   * @memberof Ifto
   */
  private flushLogsWhenDifferenceLessThanMilliseconds: number;

  /**
   * Holds the output function
   *
   * @private
   * @type {OutputFunction}
   * @memberof Ifto
   */
  private output: OutputFunction;

  /**
   * Holds the lambda Context object
   *
   * @private
   * @type {Context}
   * @memberof Ifto
   */
  private lambdaContext?: Context;

  /**
   * Creates an instance of Ifto.
   * @memberof Ifto
   */
  constructor() {
    this.flushLogsWhenDifferenceLessThanMilliseconds =
      defaults.flushLogsWhenDifferenceLessThanMilliseconds;
    this.output = defaults.output;
  }

  /**
   * Get instance of Ifto
   *
   * @static
   * @returns {Ifto}
   * @memberof Ifto
   */
  static getInstance(): Ifto {
    return new Ifto();
  }

  /**
   * Add Lambda function context
   *
   * @param {Context} lambdaContext
   * @returns
   * @memberof Ifto
   */
  addLambdaContext(lambdaContext: Context) {
    this.lambdaContext = lambdaContext;

    return this;
  }

  /**
   * Return lambda Context
   *
   * @returns {Context}
   * @memberof Ifto
   */
  getLambdaContext() {
    return this.lambdaContext;
  }

  /**
   * Update output function
   *
   * @param {OutputFunction} output
   * @returns
   * @memberof Ifto
   */
  updateOutputFunction(output: OutputFunction) {
    this.output = output;

    return this;
  }

  /**
   * Get output function
   *
   * @returns {OutputFunction}
   * @memberof Ifto
   */
  getOutputFunction() {
    return this.output;
  }

  /**
   * Get log entries
   *
   * @returns
   * @memberof Ifto
   */
  getLogEntries() {
    return this.logEntries;
  }

  /**
   * Log data
   *
   * @param {string} entry
   * @returns
   * @memberof Ifto
   */
  log(entry: string) {
    this.logEntries.push(this.getLogEntry(this.logEntries, entry));

    return this;
  }

  /**
   * Get value for flushLogsWhenDifferenceLessThanMilliseconds
   *
   * @returns
   * @memberof Ifto
   */
  getFlushLogsWhenDifferenceLessThanMilliseconds() {
    return this.flushLogsWhenDifferenceLessThanMilliseconds;
  }

  /**
   * Attach to Global object
   *
   * @param {ExtendedNodeJsGlobal} globalObject
   * @returns
   * @memberof Ifto
   */
  attach(globalObject: ExtendedNodeJsGlobal) {
    if (!globalObject.Ifto) {
      globalObject.Ifto = this;
    }

    return this;
  }

  /**
   * Indicates whether the monitoring should start or not
   *
   * @returns
   * @memberof Ifto
   */
  isAllowedToMonitor() {
    return this.allowedToMonitor;
  }

  /**
   * Initialize the Ifto process
   *
   * @param {NodeJS.Process} currentProcess
   * @returns
   * @memberof Ifto
   */
  init(params: NodeJS.ProcessEnv) {
    // Initializing the monitoring flag
    const envValue = this.getValueFromProcessEnv(
      params,
      ENV_VAR_MONITORING_START_NAME
    );

    this.allowedToMonitor = !!(
      envValue &&
      envValue.toLocaleLowerCase() === ENV_VAR_MONITORING_START_VALUE
    );

    // Initializing flush time value
    const flushTimeValue = this.getValueFromProcessEnv(
      params,
      ENV_VAR_FLUSH_LOGS_WHEN_DIFFERENCE_LESS_THAN_MILLISECONDS
    );

    if (flushTimeValue) {
      const timeout = parseInt(flushTimeValue as string, 10);
      if (!isNaN(timeout)) {
        this.flushLogsWhenDifferenceLessThanMilliseconds = timeout;
      }
    }

    return this;
  }

  /**
   * Start monitoring
   *
   * @param {Promise<any>} executor
   * @returns
   * @memberof Ifto
   */
  async monitor(handler: Promise<any>): Promise<any> {
    let interval: NodeJS.Timeout | undefined;
    if (this.allowedToMonitor && this.lambdaContext) {
      const spy = Spy.getInstance(10);
      spy.start();
      interval = setInterval(() => {
        spy.stop();
        this.output(
          `${this.getWarningString()}\n${this.logEntries.join(
            '\n'
          )}\n${spy.printEntries()}`
        );
        this.clearMonitoringInterval(interval);
      }, this.lambdaContext.getRemainingTimeInMillis() - this.flushLogsWhenDifferenceLessThanMilliseconds);
    }

    try {
      const result = await handler;
      this.clearMonitoringInterval(interval);

      return result;
    } catch (error) {
      this.clearMonitoringInterval(interval);

      throw error;
    }
  }

  /**
   * Clear
   *
   * @private
   * @param {NodeJS.Timeout} [interval]
   * @memberof Ifto
   */
  private clearMonitoringInterval(interval?: NodeJS.Timeout) {
    if (interval) {
      clearInterval(interval);
    }
  }

  /**
   * Get formated log entry
   *
   * @private
   * @param {string[]} logArray
   * @param {string} entry
   * @returns
   * @memberof Ifto
   */
  private getLogEntry(logArray: string[], entry: string) {
    return `${formatedDate()} ${logArray.length}: ${entry}`;
  }

  /**
   * Get the warning header
   *
   * @private
   * @returns
   * @memberof Ifto
   */
  private getWarningString() {
    return `
Expecting a possible lambda timeout.
Only ${this.flushLogsWhenDifferenceLessThanMilliseconds} milliseconds remaining.
(If this is a false positive error change the value by setting up the environment variable "${ENV_VAR_FLUSH_LOGS_WHEN_DIFFERENCE_LESS_THAN_MILLISECONDS}").
Current log:`;
  }

  /**
   * try to extract lower case or upper case values
   *
   * @private
   * @param {NodeJS.ProcessEnv} params
   * @param {string} valueKey
   * @returns
   * @memberof Ifto
   */
  private getValueFromProcessEnv(params: NodeJS.ProcessEnv, valueKey: string) {
    return params[valueKey] || params[valueKey.toUpperCase()];
  }
}