Source: aws-metrics/aws-metrics-buffer.js

/**
 * @file aws-metrics-buffer.js
 * @copyright Matthew Bill
 */
const AwsMetricDatum = require('./aws-metric-datum.js');

class AwsMetricsBuffer {
  /**
     * Constructor for AwsMetricsBuffer.
     * @param {Logger} logger The winston logger.
     */
  constructor(logger) {
    const self = this;
    self.logger = logger;
    self.buffer = {};
  }

  /**
     * Clears the buffer of all metrics.
     */
  clearBuffer() {
    const self = this;
    self.buffer = {};
  }

  /**
     * Adds a collection of metrics for a specific namespace.
     * If the collection already exists, then it is ignored.
     * @param {string} namespace The namespace which the collection of metrics belongs to.
     */
  addNamespaceMetricsCollection(namespace) {
    const self = this;
    if (self.buffer[namespace] === undefined) {
      self.buffer[namespace] = [];
    }
  }

  /**
     * Adds a collection of metric datums to the buffer under the specified namespace.
     * @param {string} namespace The namespace that the metrics should belong to.
     */
  addNamespaceMetrics(namespace) {
    const self = this;
    const namespaceMetrics = {
      MetricData: [],
      Namespace: namespace,
    };
    if (self.buffer[namespace] === undefined) {
      self.addNamespaceMetricsCollection(namespace);
    }
    self.buffer[namespace].push(namespaceMetrics);
    return namespaceMetrics;
  }

  /**
     * Adds a metric datum to the buffer, aggregating it if possible.
     * @param {string} namespace The namespace for the metrics.
     * @param {string} metricName The name of the metric.
     * @param {json} dimensions The dimensions for the metric.
     * @param {string} unit The unit for the metric.
     * @param {string} value The value for the metric.
     * @param {bool} aggregate If set to true, then the metric datum values will be aggregated.
     * If there is already a matching metric datum with the same dimensions and namespace.
     */
  addMetricDatum(namespace, metricName, dimensions, unit, value, aggregate) {
    const self = this;
    if (aggregate) {
      const metricDatum = self.findMetricDatum(namespace,
        metricName, dimensions);
      if (metricDatum === null) {
        self.addNewMetricDatum(namespace, metricName, dimensions, unit, value);
      } else {
        AwsMetricsBuffer.aggregateMetricDatum(metricDatum, value);
      }
    } else {
      self.addNewMetricDatum(namespace, metricName, dimensions, unit, value);
    }
  }

  /**
     * Adds a new metric datum to the buffer.
     * This is done even if a matching one already exists within the buffer.
     * @param {string} namespace The namespace for the metrics.
     * @param {string} metricName The name of the metric.
     * @param {json} dimensions The dimensions for the metric.
     * @param {string} unit The unit for the metric.
     * @param {string} value The value for the metric.
     */
  addNewMetricDatum(namespace, metricName, dimensions, unit, value) {
    const self = this;

    if (self.buffer[namespace] === undefined) {
      self.addNamespaceMetricsCollection(namespace);
    }
    let namespaceMetrics = null;
    const namespaceMetricsCollection = self.buffer[namespace];
    if (namespaceMetricsCollection.length === 0) {
      namespaceMetrics = self.addNamespaceMetrics(namespace);
    } else {
      const index = namespaceMetricsCollection.length - 1;
      namespaceMetrics = namespaceMetricsCollection[index];
      if (namespaceMetrics.MetricData.length === 20) {
        // Add new metrics namespace as aws limit reached.
        namespaceMetrics = self.addNamespaceMetrics(namespace);
      }
    }
    const metricDatum = new AwsMetricDatum(metricName, dimensions, unit, value);
    namespaceMetrics.MetricData.push(metricDatum);
  }

  /**
     * Finds a metric datum with the specified dimensions and namespace within the buffer.
     * Null is returend if no metric datum exists with the specified crieria.
     * @param {string} namespace The namespace that the metric datum belongs to.
     * @param {string} metricName The name of the metric.
     * @param {*} dimensions The dimensions of the mertic.
     */
  findMetricDatum(namespace, metricName, dimensions) {
    const self = this;
    let metricDatum = null;
    if (self.buffer[namespace] === undefined) {
      return metricDatum;
    }
    self.buffer[namespace].forEach((namespaceMetrics) => {
      namespaceMetrics.MetricData.forEach((metricsDatum) => {
        if (metricsDatum.MetricName === metricName
            && AwsMetricsBuffer.dimensionsAreEqual(metricsDatum.Dimensions,
              dimensions)) {
          metricDatum = metricsDatum;
        }
      });
    });
    return metricDatum;
  }

  /**
     * Aggregates the value to the statistic set of the metric datum passed in. If there is currently not a statistic set, then one is created.
     * @param {AwsMetricDatum} metricDatum The metric datum.
     * @param {string} value The value to aggregate the metric datum by.
     */
  static aggregateMetricDatum(metricDatum, value) {
    const aggregatedMetricDatum = metricDatum;
    if (aggregatedMetricDatum.StatisticValues !== undefined) {
      if (aggregatedMetricDatum.StatisticValues.Maximum < value) {
        aggregatedMetricDatum.StatisticValues.Maximum = value;
      }
      if (aggregatedMetricDatum.StatisticValues.Minimum > value) {
        aggregatedMetricDatum.StatisticValues.Minimum = value;
      }
      aggregatedMetricDatum.StatisticValues.SampleCount += 1;
      aggregatedMetricDatum.StatisticValues.Sum += value;
    } else {
      aggregatedMetricDatum.StatisticValues = {
        Maximum: value,
        Minimum: value,
        SampleCount: 2,
        Sum: value + aggregatedMetricDatum.Value,
      };
      // Remove value as statisticValues now used instead.
      delete aggregatedMetricDatum.Value;
    }
  }

  /**
     * Compares two dimensions objects to see if they are equivilant to each other.
     * @param {*} a
     * @param {*} b
     */
  static dimensionsAreEqual(a, b) {
    if ((a === null || a === undefined) && (b === null || b === undefined)) {
      return true;
    }
    const aString = JSON.stringify(a);
    const bString = JSON.stringify(b);
    return aString === bString;
  }
}

module.exports = AwsMetricsBuffer;