Source: reporters/Reporter.js

const consoleLogLevel = require('console-log-level');
const Optional = require('optional-js');
const { validateReporterParameters } = require('../validators/inputValidators');

const DEFAULT_REPORTING_INTERVAL_IN_SECONDS = 10;

function prefix() {
  return `${new Date().toISOString()}: `;
}

/**
 * The abstract reporter that specific implementations can extend to create a Self Reporting Metrics Registry Reporter.
 *
 * {@link SelfReportingMetricsRegistry}
 *
 * @example
 * const os = require('os');
 * const process = require('process');
 * const { SelfReportingMetricsRegistry, Reporter } = require('measured-reporting');
 *
 * // Create a self reporting registry with a named anonymous reporter instance;
 * const registry = new SelfReportingMetricsRegistry(
 *   new class ConsoleReporter extends Reporter {
 *     constructor() {
 *       super({
 *         defaultDimensions: {
 *           hostname: os.hostname(),
 *           env: process.env['NODE_ENV'] ? process.env['NODE_ENV'] : 'unset'
 *         }
 *        })
 *     }
 *
 *     _reportMetrics(metrics) {
 *        metrics.forEach(metric => {
 *          console.log(JSON.stringify({
 *            metricName: metric.name,
 *            dimensions: this._getDimensions(metric),
 *            data: metric.metricImpl.toJSON()
 *           }))
 *       });
 *     }
 *  }()
 * );
 *
 * @example
 * // Create a regular class that extends Reporter
 * class LoggingReporter extends Reporter {
 *   _reportMetrics(metrics) {
 *     metrics.forEach(metric => {
 *       this._log.info(JSON.stringify({
 *        metricName: metric.name,
 *        dimensions: this._getDimensions(metric),
 *        data: metric.metricImpl.toJSON()
 *       }))
 *     });
 *   }
 * }
 *
 * @abstract
 */
class Reporter {
  /**
   * @param {ReporterOptions} [options] The optional params to supply when creating a reporter.
   */
  constructor(options) {
    if (this.constructor === Reporter) {
      throw new TypeError("Can't instantiate abstract class!");
    }

    options = options || {};
    validateReporterParameters(options);

    /**
     * Map of intervals to metric keys, this will be used to look up what metrics should be reported at a given interval.
     *
     * @type {Object.<number, Set<string>>}
     * @private
     */
    this._intervalToMetric = {};
    this._intervals = [];

    /**
     * Map of default dimensions, that should be sent with every metric.
     *
     * @type {Dimensions}
     * @protected
     */
    this._defaultDimensions = options.defaultDimensions || {};

    /**
     * Loggers to use, defaults to a new console logger if nothing is supplied in options
     * @type {Logger}
     * @protected
     */
    this._log =
      options.logger || consoleLogLevel({ name: 'Reporter', level: options.logLevel || 'info', prefix: prefix });

    /**
     * The default reporting interval, a number in seconds.
     * If not overridden via the {@see ReporterOptions}, defaults to 10 seconds.
     *
     * @type {number}
     * @protected
     */
    this._defaultReportingIntervalInSeconds =
      options.defaultReportingIntervalInSeconds || DEFAULT_REPORTING_INTERVAL_IN_SECONDS;

    /**
     * Flag to indicate if reporting timers should be unref'd.
     * If not overridden via the {@see ReporterOptions}, defaults to false.
     *
     * @type {boolean}
     * @protected
     */
    this._unrefTimers = !!options.unrefTimers;

    /**
     * Flag to indicate if metrics should be reset on each reporting interval.
     * If not overridden via the {@see ReporterOptions}, defaults to false.
     *
     * @type {boolean}
     * @protected
     */
    this._resetMetricsOnInterval = !!options.resetMetricsOnInterval;
  }

  /**
   * Sets the registry, this must be called before reportMetricOnInterval.
   *
   * @param {DimensionAwareMetricsRegistry} registry
   */
  setRegistry(registry) {
    this._registry = registry;
  }

  /**
   * Informs the reporter to report a metric on a given interval in seconds.
   *
   * @param {string} metricKey The metric key for the metric in the metric registry.
   * @param {number} intervalInSeconds The interval in seconds to report the metric on.
   */
  reportMetricOnInterval(metricKey, intervalInSeconds) {
    intervalInSeconds = intervalInSeconds || this._defaultReportingIntervalInSeconds;

    if (!this._registry) {
      throw new Error(
        'You must call setRegistry(registry) before telling a Reporter to report a metric on an interval.'
      );
    }

    if (Object.prototype.hasOwnProperty.call(this._intervalToMetric, intervalInSeconds)) {
      this._intervalToMetric[intervalInSeconds].add(metricKey);
    } else {
      this._intervalToMetric[intervalInSeconds] = new Set([metricKey]);
      this._createIntervalCallback(intervalInSeconds);
      setImmediate(() => {
        this._reportMetricsWithInterval(intervalInSeconds);
      });
    }
  }

  /**
   * Creates the timed callback loop for the given interval.
   *
   * @param {number} intervalInSeconds the interval in seconds for the timeout callback
   * @private
   */
  _createIntervalCallback(intervalInSeconds) {
    this._log.debug(`_createIntervalCallback() called with intervalInSeconds: ${intervalInSeconds}`);

    const timer = setInterval(() => {
      this._reportMetricsWithInterval(intervalInSeconds);
    }, intervalInSeconds * 1000);

    if (this._unrefTimers) {
      timer.unref();
    }

    this._intervals.push(timer);
  }

  /**
   * Gathers all the metrics that have been registered to report on the given interval.
   *
   * @param {number} interval The interval to look up what metrics to report
   * @private
   */
  _reportMetricsWithInterval(interval) {
    this._log.debug(`_reportMetricsWithInterval() called with intervalInSeconds: ${interval}`);
    try {
      Optional.of(this._intervalToMetric[interval]).ifPresent(metrics => {
        const metricsToSend = [];
        metrics.forEach(metricKey => {
          metricsToSend.push(this._registry.getMetricWrapperByKey(metricKey));
        });
        this._reportMetrics(metricsToSend);

        if (this._resetMetricsOnInterval) {
          metricsToSend.forEach(({ name, metricImpl }) => {
            if (metricImpl && metricImpl.reset) {
              this._log.debug('Resetting metric', name);
              metricImpl.reset();
            }
          });
        }
      });
    } catch (error) {
      this._log.error('Failed to send metrics to signal fx', error);
    }
  }

  /**
   * This method gets called with an array of {@link MetricWrapper} on an interval, when metrics should be reported.
   *
   * This is the main method that needs to get implemented when created an aggregator specific reporter.
   *
   * @param {MetricWrapper[]} metrics The array of metrics to report.
   * @protected
   * @abstract
   */
  _reportMetrics(metrics) {
    throw new TypeError('Abstract method _reportMetrics(metrics) must be implemented in implementation class');
  }

  /**
   *
   * @param {MetricWrapper} metric The Wrapped Metric Object.
   * @return {Dimensions} The left merged default dimensions with the metric specific dimensions
   * @protected
   */
  _getDimensions(metric) {
    return Object.assign({}, this._defaultDimensions, metric.dimensions);
  }

  /**
   * Clears the intervals that are running to report metrics at an interval, and resets the state.
   */
  shutdown() {
    this._intervals.forEach(interval => clearInterval(interval));
    this._intervals = [];
    this._intervalToMetric = {};
  }
}

/**
 * Options for creating a {@link Reporter}
 * @interface ReporterOptions
 * @typedef ReporterOptions
 * @type {Object}
 * @property {Dimensions} defaultDimensions A dictionary of dimensions to include with every metric reported
 * @property {Logger} logger The logger to use, if not supplied a new Buynan logger will be created
 * @property {string} logLevel The log level to use with the created console logger if you didn't supply your own logger.
 * @property {number} defaultReportingIntervalInSeconds The default reporting interval to use if non is supplied when registering a metric, defaults to 10 seconds.
 * @property {boolean} unrefTimers Indicate if reporting timers should be unref'd, defaults to false.
 * @property {boolean} resetMetricsOnInterval Indicate if metrics should be reset on each reporting interval, defaults to false.
 */

module.exports = Reporter;