Source: measured-signalfx-reporter/lib/reporters/SignalFxMetricsReporter.js

const { Reporter } = require('measured-reporting');
const { MetricTypes } = require('measured-core');
const { validateSignalFxClient } = require('../validators/inputValidators');
const { USER_DEFINED } = require('../SignalFxEventCategories');

/**
 * A Reporter that reports metrics to Signal Fx
 * @extends {Reporter}
 */
class SignalFxMetricsReporter extends Reporter {
  /**
   * @param {SignalFxClient} signalFxClient The configured signal fx client.
   * @param {ReporterOptions} [options] See {@link ReporterOptions}.
   */
  constructor(signalFxClient, options) {
    options = options || {};
    super(options);
    validateSignalFxClient(signalFxClient);
    this._signalFxClient = signalFxClient;

    this._log.debug(`SignalFx Metrics Reporter Created with the following default reporting interval: ${options.defaultReportingIntervalInSeconds}, default dimensions: ${JSON.stringify(options.defaultDimensions, null, 2)}`);
  }

  /**
   * Sends metrics to signal fx, converting name and dimensions and {@link Metric} to data signal fx can ingest
   * @param {MetricWrapper[]} metrics The array of metrics to send to signal fx.
   * @protected
   */
  _reportMetrics(metrics) {
    this._log.debug('_reportMetrics() called');

    let signalFxDataPointRequest = {};

    metrics.forEach(metric => {
      if (!metric) {
        this._log.warn('Metric was null when it should not have been');
        return;
      }
      signalFxDataPointRequest = this._processMetric(metric, signalFxDataPointRequest);
    });

    this._log.debug(`Sending data to Signal Fx. Request: ${JSON.stringify(signalFxDataPointRequest)}`);

    this._signalFxClient.send(signalFxDataPointRequest).catch(error => {
      this._log.error('Failed to send metrics to signal fx error:', error);
    });
  }

  /**
   * Method for getting raw signal fx api request values from the Timer Object.
   *
   * @param {MetricWrapper} metric metric The Wrapped Metric Object.
   * @param {any} currentBuiltRequest The signal fx request that is being built.
   * @return {any} the currentBuiltRequest The signal fx request that is being built with the given metric in it.
   * @protected
   */
  _processMetric(metric, currentBuiltRequest) {
    const newRequest = Object.assign({}, currentBuiltRequest);

    const { name, metricImpl } = metric;
    const mergedDimensions = this._getDimensions(metric);

    const valuesToProcess = this._getValuesToProcessForType(name, metricImpl);

    valuesToProcess.forEach(metricValueTypeWrapper => {
      const signalFxDataPointMetric = {
        metric: metricValueTypeWrapper.metric,
        value: metricValueTypeWrapper.value,
        dimensions: mergedDimensions
      };

      if (Object.prototype.hasOwnProperty.call(newRequest, metricValueTypeWrapper.type)) {
        newRequest[metricValueTypeWrapper.type].push(signalFxDataPointMetric);
      } else {
        newRequest[metricValueTypeWrapper.type] = [signalFxDataPointMetric];
      }
    });

    return newRequest;
  }

  /**
   * Maps Measured Metrics Object JSON outputs to their respective signal fx metrics using logic from
   * com.signalfx.codahale.reporter.AggregateMetricSenderSessionWrapper in the java lib to derive naming
   *
   * @param {string} name The registered metric base name
   * @param {Metric} metric The metric.
   * @return {MetricValueTypeWrapper[]} an array of MetricValueTypeWrapper that can be used to
   * build the SignalFx data point request
   * @protected
   */
  _getValuesToProcessForType(name, metric) {
    const type = metric.getType();
    switch (type) {
      case MetricTypes.TIMER:
        return this._getValuesToProcessForTimer(name, metric);
      case MetricTypes.GAUGE:
        return this._getValuesToProcessForGauge(name, metric);
      case MetricTypes.COUNTER:
        return this._getValuesToProcessForCounter(name, metric);
      case MetricTypes.HISTOGRAM:
        return this._getValuesToProcessForHistogram(name, metric);
      case MetricTypes.METER:
        this._log.warn(
          'Meters are not reported to SignalFx. Meters do not make sense to use with SignalFx because the same values ' +
            'can be calculated using simple counters and aggregations within SignalFx itself.'
        );
        return [];
      default:
        this._log.error(`Metric Type: ${type} has not been implemented to report to signal fx`);
        return [];
    }
  }

  /**
   * Maps and Filters values from a Timer to a set of metrics to report to SigFx.
   *
   * @param {string} name The registry name
   * @param {Timer} timer The Timer
   * @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
   * @protected
   */
  _getValuesToProcessForTimer(name, timer) {
    let valuesToProcess = [];
    // only grab histogram data as Meters can be accomplished with signal fx using the count from the histogram
    valuesToProcess = valuesToProcess.concat(this._getValuesToProcessForHistogram(name, timer._histogram));
    return valuesToProcess;
  }

  /**
   * Maps values from a Gauge to a set of metrics to report to SigFx.
   *
   * @param {string} name The registry name
   * @param {Gauge} gauge The Gauge
   * @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
   * @protected
   */
  _getValuesToProcessForGauge(name, gauge) {
    const valuesToProcess = [];
    valuesToProcess.push({
      metric: `${name}`,
      value: gauge.toJSON(),
      type: SIGNAL_FX_GAUGE
    });
    return valuesToProcess;
  }

  /**
   * Maps values from a Counter to a set of metrics to report to SigFx.
   *
   * @param {string} name The registry name
   * @param {Counter} counter The data from the measure metric object
   * @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
   * @protected
   */
  _getValuesToProcessForCounter(name, counter) {
    const valuesToProcess = [];
    valuesToProcess.push({
      metric: `${name}.count`,
      value: counter.toJSON(),
      type: SIGNAL_FX_CUMULATIVE_COUNTER
    });
    return valuesToProcess;
  }

  /**
   * Maps and Filters values from a Histogram to a set of metrics to report to SigFx.
   *
   * @param {string} name The registry name
   * @param {Histogram} histogram The Histogram
   * @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
   * @protected
   */
  _getValuesToProcessForHistogram(name, histogram) {
    // TODO add full list of histogram metrics but enable filter
    const data = histogram.toJSON();
    const valuesToProcess = [];
    valuesToProcess.push({
      metric: `${name}.count`,
      value: data.count || 0,
      type: SIGNAL_FX_CUMULATIVE_COUNTER
    });
    valuesToProcess.push({
      metric: `${name}.max`,
      value: data.max || 0,
      type: SIGNAL_FX_GAUGE
    });
    valuesToProcess.push({
      metric: `${name}.min`,
      value: data.min || 0,
      type: SIGNAL_FX_GAUGE
    });
    valuesToProcess.push({
      metric: `${name}.mean`,
      value: data.mean || 0,
      type: SIGNAL_FX_GAUGE
    });
    valuesToProcess.push({
      metric: `${name}.p95`,
      value: data.p95 || 0,
      type: SIGNAL_FX_GAUGE
    });
    valuesToProcess.push({
      metric: `${name}.p99`,
      value: data.p99 || 0,
      type: SIGNAL_FX_GAUGE
    });
    return valuesToProcess;
  }

  /**
   * Function exposes the event API of Signal Fx.
   * See {@link https://github.com/signalfx/signalfx-nodejs#sending-events} for more details.
   *
   * @param {string} eventType The event type (name of the event time series).
   * @param {SignalFxEventCategoryId} [category] the category of event. See {@link module:SignalFxEventCategories}. Value by default is USER_DEFINED.
   * @param {Dimensions} [dimensions] a map of event dimensions, empty dictionary by default
   * @param {Object.<string, string>} [properties] a map of extra properties on that event, empty dictionary by default
   * @param {number} [timestamp] a timestamp, by default is current time.
   *
   * @example
   * const {
   *   SignalFxSelfReportingMetricsRegistry,
   *   SignalFxMetricsReporter,
   *   SignalFxEventCategories
   * } = require('measured-signalfx-reporter');
   * const registry = new SignalFxSelfReportingMetricsRegistry(new SignalFxMetricsReporter(signalFxClient));
   * registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT);
   */
  sendEvent(eventType, category, dimensions, properties, timestamp) {
    const body = {
      eventType,
      category: category || USER_DEFINED,
      dimensions: this._getDimensions({ dimensions }),
      properties,
      timestamp
    };
    Object.keys(body).forEach(key => body[key] == null && delete body[key]);
    return this._signalFxClient.sendEvent(body);
  }
}

// const SIGNAL_FX_COUNTER = 'counters';
const SIGNAL_FX_GAUGE = 'gauges';
const SIGNAL_FX_CUMULATIVE_COUNTER = 'cumulative_counters';

module.exports = SignalFxMetricsReporter;

/**
 * Wrapper object to wrap metric value and SFX metadata needed to send metric value to SFX data ingestion.
 *
 * @interface MetricValueTypeWrapper
 * @typedef MetricValueTypeWrapper
 * @type {Object}
 * @property {string} metric The metric name to report to SignalFx
 * @property {number} value the value to report to SignalFx
 * @property {string} type The mapped SignalFx metric type
 */