Home Reference Source

lib/statsd.js

const Transport = require('./transport');
const helpers = require('./helpers');

/**
 * StatsD Client
 * @description The main entry-point for hot-shots.
 */
class StatsD extends Transport {

  /**
   * @constructor
   * @param {object} options - The options to use for the client.
   * @param {string} options.host - The host to send metrics to. Required for TCP and UDP protocols.
   * @param {number} options.port - The port that StatsD is listening on. Required for TCP and UDP protocols.
   * @param {string} options.path - The UDS path to connect to. Required for UNIX_DGRAM protocol.
   * @param {string} options.prefix - Global prefix string to append to all stats. Optional
   * @param {string} options.suffix - Global suffix string to append to all stats. Optional
   * @param {boolean} options.globalize - Assigns the client to a global instance. Optional
   * @param {boolean} options.cacheDns - Whether or not to cache the DNS for subsequent requests. Optional
   * @param {boolean} options.mock - Whether or not to mock the client. Optional
   * @param {string[]|object} options.globalTags - Tags to include for every metroc. Optional
   * @param {number} options.maxBufferSize - The maximum size of the in-memory buffer. Optional
   * @param {number} options.bufferFlushInterval - How often the buffer is sent to StatsD. Optional
   * @param {boolean} options.telegraf - Flag to indicate telegraf support. Optional
   * @param {number} option.sampleRate - The sample rate to use fo stats. Optional
   * @param {string} options.protocol - The protocol to use for sending stats. Optional
   */
  constructor(...args) {
    super(...args);
  }

  /**
   * Represents the timing stat
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} time - The time in milliseconds to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  timing(stat, time, sampleRate, tags, callback) {
    this.sendAll(stat, time, 'ms', sampleRate, tags, callback);
  }

  /**
   * Represents the timing stat by recording the duration a function takes to run (in milliseconds)
   * @param {function} func - The function to run
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  timer(func, stat, sampleRate, tags, callback) {
    return (...args) => {
      const start = process.hrtime();
      try {
        return func(...args);
      } finally {
        // get duration in milliseconds
        const durationComponents = process.hrtime(start);
        const seconds = durationComponents[0];
        const nanoseconds = durationComponents[1];
        const duration = (seconds * 1000) + (nanoseconds / 1E6);

        this.timing(
          stat,
          duration,
          sampleRate,
          tags,
          callback
        );
      }
    };
  }

  /**
   * Decorates an async function with timing recording behaviour.
   *
   * This version of `timer` will record the time take for the asyncronus action returned by `func`
   * not just the execution time of `func` itself.
   *
   * @param {function} func The function to run
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  asyncTimer(func, stat, sampleRate, tags, callback) {
    return (...args) => {
      const end = helpers.createHrTimer();
      const p = func(...args);
      const recordStat = () => { this.timing(stat, end(), sampleRate, tags, callback); };
      p.then(recordStat, recordStat);
      return p;
    };
  }

  /**
   * Increments a stat by a specified amount
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} value - The value to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  increment(stat, value, sampleRate, tags, callback) {
    // allow use of tags without explicit value or sampleRate
    if (arguments.length < 3) {
      if (typeof value !== 'number') {
        tags = value;
        value = undefined;
      }
    }

    // we explicitly check for undefined and null (and don't do a "! value" check)
    // so that 0 values are allowed and sent through as-is
    if (value === undefined || value === null) {
      value = 1;
    }
    this.sendAll(stat, value, 'c', sampleRate, tags, callback);
  }

  /**
   * Decrements a stat by a specified amount
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} value - The value to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  decrement(stat, value, sampleRate, tags, callback) {
    this.sendAll(stat, -value || -1, 'c', sampleRate, tags, callback);
  }

  /**
   * Represents the histogram stat
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} value - The value to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  histogram(stat, value, sampleRate, tags, callback) {
    this.sendAll(stat, value, 'h', sampleRate, tags, callback);
  }

  /**
   * Represents the distribution stat
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} value - The value to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  distribution(stat, value, sampleRate, tags, callback) {
    this.sendAll(stat, value, 'd', sampleRate, tags, callback);
  }

  /**
   * Gauges a stat by a specified amount
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} value - The value to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  gauge(stat, value, sampleRate, tags, callback) {
    this.sendAll(stat, value, 'g', sampleRate, tags, callback);
  }

  /**
   * Counts unique values by a specified amount
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} value - The value to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  unique(stat, value, sampleRate, tags, callback) {
    this.sendAll(stat, value, 's', sampleRate, tags, callback);
  }

  /**
   * Counts unique values by a specified amount
   * @param {string|string[]} stat - The stat(s) to send
   * @param {number} value - The value to send
   * @param {number} sampleRate - The Number of times to sample (0 to 1). Optional.
   * @param {string[]|object} tags - The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  set(stat, value, sampleRate, tags, callback) {
    this.sendAll(stat, value, 's', sampleRate, tags, callback);
  }

  /**
   * Send a service check
   * @param {string} name - The name of the service check
   * @param {number} status - The status of the service check (0 to 3).
   * @param {object} options - Additional options
   * @param {Date} options.date_happened - Assign a timestamp to the event. Default is now.
   * @param {string} options.hostname - Assign a hostname to the check.
   * @param {string} options.message - Assign a message to the check.
   * @param {string[]|object} tags - The Array of tags to add to the check. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  check(name, status, options, tags, callback) {
    if (this.telegraf) {
      const err = new Error('Not supported by Telegraf / InfluxDB');
      if (callback) {
        return callback(err);
      } else if (this.errorHandler) {
        return this.errorHandler(err);
      }

      throw err;
    }

    const check = ['_sc', this.prefix + name + this.suffix, status], metadata = options || {};

    if (metadata.date_happened) {
      const timestamp = helpers.formatDate(metadata.date_happened);
      if (timestamp) {
        check.push(`d:${timestamp}`);
      }
    }
    if (metadata.hostname) {
      check.push(`h:${metadata.hostname}`);
    }

    let mergedTags = this.globalTags;
    if (tags && typeof(tags) === 'object') {
      mergedTags = helpers.overrideTags(mergedTags, tags, this.telegraf);
    }
    if (mergedTags.length > 0) {
      check.push(`#${mergedTags.join(',')}`);
    }

    // message has to be the last part of a service check
    if (metadata.message) {
      check.push(`m:${metadata.message}`);
    }

    // allow for tags to be omitted and callback to be used in its place
    if (typeof tags === 'function' && callback === undefined) {
      callback = tags;
    }

    const message = check.join('|');
    // Service checks are unique in that message has to be the last element in
    // the stat if provided, so we can't append tags like other checks. This
    // directly calls the `_send` method to avoid appending tags, since we've
    // already added them.
    this._send(message, callback);
  }

  /**
   * Send on an event
   * @param {string} title - The title of the event
   * @param {string} text - The description of the event.  Optional- title is used if not given.
   * @param {object} options - Additional options
   * @param {Date} options.date_happened - Assign a timestamp to the event. Default is now.
   * @param {string} options.hostname - Assign a hostname to the event.
   * @param {string} options.aggregation_key - Assign an aggregation key to the event, to group it with some others.
   * @param {string} options.priority - Can be ‘normal’ or ‘low’. Default is 'normal'.
   * @param {string} options.source_type_name - Assign a source type to the event.
   * @param {string} options.alert_type - Can be ‘error’, ‘warning’, ‘info’ or ‘success’. Default is 'info'.
   * @param {string[]|object} tags - options.tags The Array of tags to add to metrics. Optional.
   * @param {function} callback - Callback when message is done being delivered. Optional.
   */
  event(title, text, options, tags, callback) {
    if (this.telegraf) {
      const err = new Error('Not supported by Telegraf / InfluxDB');
      if (callback) {
        return callback(err);
      }
      else if (this.errorHandler) {
        return this.errorHandler(err);
      }

      throw err;
    }

    // Convert to strings
    let message;

    const msgTitle = String(title ? title : '');
    let msgText = String(text ? text : msgTitle);
    // Escape new lines (unescaping is supported by DataDog)
    msgText = msgText.replace(/\n/g, '\\n');

    // start out the message with the event-specific title and text info
    message = `_e{${msgTitle.length},${msgText.length}}:${msgTitle}|${msgText}`;

    // add in the event-specific options
    if (options) {
      if (options.date_happened) {
        const timestamp = helpers.formatDate(options.date_happened);
        if (timestamp) {
          message += `|d:${timestamp}`;
        }
      }
      if (options.hostname) {
        message += `|h:${options.hostname}`;
      }
      if (options.aggregation_key) {
        message += `|k:${options.aggregation_key}`;
      }
      if (options.priority) {
        message += `|p:${options.priority}`;
      }
      if (options.source_type_name) {
        message += `|s:${options.source_type_name}`;
      }
      if (options.alert_type) {
        message += `|t:${options.alert_type}`;
      }
    }

    // allow for tags to be omitted and callback to be used in its place
    if (typeof tags === 'function' && callback === undefined) {
      callback = tags;
    }

    this.send(message, tags, callback);
  }

  /**
   * Creates a child client that adds prefix, suffix and/or tags to this client. Child client can itself have children.
   * @param {object} options
   * @param {string} options.prefix - An optional prefix to assign to each stat name sent
   * @param {string} options.suffix - An optional suffix to assign to each stat name sent
   * @param {string[]|object} options.globalTags - Optional tags that will be added to every metric
   */
  childClient(options) {
    return new StatsD({
      isChild     : true,
      socket      : this.socket, // Child inherits socket from parent. Parent itself can be a child.
      // All children and parent share the same buffer via sharing an object (cannot mutate strings)
      bufferHolder: this.bufferHolder,
      dnsError    : this.dnsError, // Child inherits an error from parent (if it is there)
      errorHandler: options.errorHandler || this.errorHandler, // Handler for callback errors
      host        : this.host,
      port        : this.port,
      prefix      : (options.prefix || '') + this.prefix, // Child has its prefix prepended to parent's prefix
      suffix      : this.suffix + (options.suffix || ''), // Child has its suffix appended to parent's suffix
      globalize   : false, // Only 'root' client can be global
      mock        : this.mock,
      // Append child's tags to parent's tags
      globalTags  : typeof options.globalTags === 'object' ?
          helpers.overrideTags(this.globalTags, options.globalTags, this.telegraf) : this.globalTags,
      maxBufferSize : this.maxBufferSize,
      bufferFlushInterval: this.bufferFlushInterval,
      telegraf    : this.telegraf,
      protocol    : this.protocol
    });
  }
}

module.exports = StatsD;