Node Monitor v0.6.5

Show:

File: lib/Stat.js

/*jslint browser: true */
// Stat.js (c) 2010-2014 Loren West and other contributors
// May be freely distributed under the MIT license.
// For further details and documentation:
// http://lorenwest.github.com/node-monitor
(function(root){

  // Module loading
  var Monitor = root.Monitor || require('./Monitor'),
      // Raw events on the server (for speed), backbone events on the browser (for functionality)
      EventEmitter = Monitor.commonJS ? require('events').EventEmitter.prototype : Monitor.Backbone.Events,
      _ = Monitor._,
      emittingNow = false;


  /**
  * A lightweight component for gathering and emitting application statistics
  *
  * This is both a collector and emitter for application stats.
  *
  * It's designed with low development and runtime cost in mind, encouraging
  * usage with minimum concern for overhead.
  *
  * Stat Collector
  * --------------
  *
  * As a collector, it's a place to send application stats as they're discovered.
  *
  * Example for incrementing a stat in your application:
  *
  *     var stat = require('monitor').getStatLogger('myModule');
  *     ...
  *     stat.increment('requests.inbound');
  *
  * The above is a request to increment the ```myModule.requests.inbound``` stat.
  * It peforms work only if someone is listening for that event.
  *
  * Stat Emitter
  * -------------
  * As an emitter, Stat is a place to gather stats as they're collected.
  *
  * When listening for stats, wildcards can be used to register for many stats
  * within a group. For example, the following call:
  *
  *     var Stat = require('monitor').Stat;
  *     Stat.on('myModule.*.timer', myFunction);
  *
  * Will call ```myFunction``` when all ```myModule.*.timer``` stats are emitted.
  *
  * Listeners are invoked with 4 arguments:
  *
  * - module - The statLogger module name
  * - name - The name of the stat that just fired
  * - value - The numeric value passed
  * - type - An enumeration of the types of stats:<br/>
  *   'c'  - Counter.  Add (or subtract) the value to (or from) the prior value<br/>
  *   'g'  - Gague.  Value is to be recorded as provided<br/>
  *   'ms' - Timer.  Millisecond amount of time something took.
  *
  * <h2 id="wildcards">Wildcards</h2>
  *
  * The following wildcards are allowed for registering events.  They're
  * modeled after the graphite wildcard syntax (from the
  * <a href="https://graphite.readthedocs.org/en/latest/render_api.html#paths-and-wildcards">graphite docs</a>):
  *
  * #### Delimiter
  * The period (.) character is literal, and matches name segment separators.
  *
  * #### Asterisk
  * The asterisk (*) matches zero or more characters. It is non-greedy, so you
  * can have more than one within a single path element.
  *
  * Example: servers.ix\*ehssvc\*v.cpu.total.\* will return all total CPU metrics
  * for all servers matching the given name pattern.
  *
  * An asterisk at the far right of the pattern matches everything to the right,
  * including all path segments.  For example, ```servers.*``` matches all
  * names beginning with ```servers.```.
  *
  * #### Character list or range
  * Characters in square brackets ([...]) specify a single character position in
  * the path string, and match if the character in that position matches one of
  * the characters in the list or range.
  *
  * A character range is indicated by 2 characters separated by a dash (-), and
  * means that any character between those 2 characters (inclusive) will match.
  * More than one range can be included within the square brackets, e.g. foo[a-z0-9]bar
  * will match foopbar, foo7bar etc..
  *
  * If the characters cannot be read as a range, they are treated as a
  * list - any character in the list will match, e.g. foo[bc]ar will match
  * foobar and foocar. If you want to include a dash (-) in your list, put
  * it at the beginning or end, so it's not interpreted as a range.
  *
  * #### Value list
  * Comma-separated values within curly braces ({foo,bar,...}) are treated as
  * value lists, and match if any of the values matches the current point in
  * the path. For example, servers.ix01ehssvc04v.cpu.total.{user,system,iowait}
  * will match the user, system and I/O wait total CPU metrics for the specified
  * server.
  *
  * #### Javascript Regex
  * For finer grained expression matching, a javascript style regex can be
  * specified using the ```/.../``` syntax.  This style spans the entire identifier.
  * You can ignore case using the ```/.../i``` syntax.  If the first character of the
  * string is a slash, it considers the string a javascript regular expression.
  *
  * Choosing Good Names
  * -------------------
  * It's a good idea to pick a good naming scheme with each dot-delimited segment
  * having a consistent, well-defined purpose.  Volatile segments should be as deep
  * into the hierarchy (furthest right) as possible.  Keeping the names less
  * volatile makes it easier to turn recording on for all statistics.
  *
  * @class Stat
  * @constructor
  */
  var Stat = Monitor.Stat = function(module) {
    var t = this;
    t.module = module;
  };
  var proto = Stat.prototype;

  // This is a map of registered event names to compiled regexs, for
  // quickly testing if a statistic needs to be emitted.
  Stat.eventRegex = {};

  /**
  * Increment a counter by a specified value
  *
  * Assuming someone is listening to this stat, this is an instruction for that
  * listener to add the specified value (usually 1) to their prior value for this stat.
  *
  * This is known as server-side setting, as the server (listener) is responsible
  * for maintaining the prior and new value for the stat.
  *
  * @method increment
  * @param name {String} Dot.separated name of the counter to increment
  * @param [value=1] {Number} Amount to increment the counter by.
  */
  proto.increment = function(name, value){
    value = _.isNumber(value) ? value : 1;
    Stat._emit(this.module, name, value, 'c');
  };

  /**
  * Decrement a counter by a specified value
  *
  * Assuming someone is listening to this stat, this is an instruction for that
  * listener to subtract the specified value (usually 1) to their prior value for this stat.
  *
  * This is known as server-side setting, as the server (listener) is responsible
  * for maintaining the prior and new value for the stat.
  *
  * @method decrement
  * @param name {String} Dot.separated name of the counter to decrement
  * @param [value=1] {Number} Amount to decrement the counter by.
  */
  proto.decrement = function(name, value){
    value = _.isNumber(value) ? value : 1;
    Stat._emit(this.module, name, value * -1, 'c');
  };

  /**
  * Set the stat to the specified value
  *
  * This is an instruction to any (all) listener(s) to set the stat to a
  * specific value.
  *
  * This is known as client-side setting, because the client determines the value
  * of the stat.
  *
  * @method gauge
  * @param name {String} Dot.separated name of the stat
  * @param value {Number} Number to set the gauge to
  */
  proto.gauge = function(name, value){
    Stat._emit(this.module, name, value, 'g');
  };

  /**
  * Record the specified duration (in milliseconds) for the stat
  *
  * This is like Stat.gauge() in that it is a client-side setting of a
  * specified value.  The difference is the scale of the value is specified
  * as milliseconds.
  *
  * This may be one of the most widely used stat methods.  It can (should?) be
  * used upon callback from asynchronous methods.
  *
  * Pattern:
  *
  *     var stat = require('monitor').getStatLogger('myModule');
  *     ...
  *     var stamp = Date.now();
  *     SomeAsyncFunction(arg1, function(error) {
  *       stat.time('SomeAsyncFunction.time', Date.Now() - stamp);
  *       ...continue with error handling & callback handling
  *     });
  *
  * @method time
  * @param name {String} Dot.separated name of the stat
  * @param duration {Integer} Number of milliseconds this stat took to complete
  */
  proto.time = function(name, duration){
    Stat._emit(this.module, name, duration, 'ms');
  };

  /**
  * Send the stat to all registered listeners
  *
  * @private
  * @static
  * @method emit
  * @param module {String} Module name
  * @param name {String} Stat name
  * @param value {Numeric} Stat value
  * @param type {String} Enumeration.  One of the following:
  *   'c'  - Counter.  + values increment, - values decrement
  *   'g'  - Gague.  Statistic is recorded as provided
  *   'ms' - Timer.  Millisecond amount of time something took
  */
  Stat._emit = function(module, name, value, type) {
    var eventName,
        fullName;

    // Prevent stat recursion. This has the effect of disabling all stats
    // for stat handlers (and their downstream effect), but is necessary to
    // prevent infinite recursion.  If it's desired to stat the output of
    // stat handlers, then delay that processing until nextTick.
    if (emittingNow) {
      return;
    }
    emittingNow = true;

    // Test the name against all registered events
    for (eventName in Stat._events) {

      // Build the full name only if someone is listening
      if (!fullName) {
        fullName = module + '.' + name;
      }

      // Get the regex associated with the name
      var regex = Stat.eventRegex[eventName];
      if (!regex) {
        regex = Stat.eventRegex[eventName] = Stat._buildRegex(eventName);
      }

      // Test the name with the regex, and emit if it matches
      if (regex.test(fullName)) {
        Stat.emit(eventName, module, name, value, type);
      }
    }

    // Turn off recursion prevention
    emittingNow = false;
  };

  /**
  * Build a regex from a user entered string following the pattern described
  * in the class definition.  Loosely:
  *
  *    If it looks like a JS regexp, process it as a regexp
  *    Change all '.' to '\.'
  *    Change all '*' to '[^\.]*' (unless it's at the end, then convert to '.*')
  *    Change all {one,two} to (one|two)
  *    Leave all [...] alone - they work as-is
  *
  *  If an error occurs, throw an exception
  *
  * @private
  * @static
  * @method _buildRegex
  * @param str {String} String to build the regular expression from
  * @return {RegExp}The regular expression object
  *
  */
  Stat._buildRegex = function(str) {
    var regexStr = '',
        modifier = '',
        lastIdx = str.length - 1,
        inSquiggly = false;

    // Javascript regular expressions
    if (/^\/[^\/]*\/i*$/.test(str)) {
      if (/i$/.test(str)) {
        modifier = 'i';
        str = str.replace(/i$/,'');
      }
      regexStr = '^' + str.replace(/^\//,'').replace(/\/$/,'') + '$';
    }

    // Process character by character
    else {
      for (var i = 0, l = str.length; i < l; i++) {
        var c = str.substr(i,1);
        switch (c) {
          case '.':
            c = '\\.';
            break;
          case '*':
            c = (i === lastIdx ? '.*' : '[^\\.]*');
            break;
          case '{':
            c = '(';
            inSquiggly = true;
            break;
          case '}':
            c = ')';
            inSquiggly = false;
            break;
          case ',':
            if (inSquiggly) {
              c = '|';
            }
            break;
        }
        regexStr += c;
      }

      // Force it to match the full string
      regexStr = '^' + regexStr + '$';
    }

    // Now build the regex.  This throws an exception if poorly formed.
    return new RegExp(regexStr, modifier);
  };

  // Mixin event processing for the Stat class
  _.extend(Stat, EventEmitter);

  // Expose this class from the Monitor module
  Monitor.setStatLoggerClass(Stat);

}(this));