// Monitor.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 commonJS = (typeof exports !== 'undefined'),
Backbone = commonJS ? require('backbone') : root.Backbone,
_ = commonJS ? require('underscore')._ : root._,
log = null, stat = null,
Cron = commonJS ? require('cron') : null;
// Constants
var DEFAULT_DEEP_COPY_DEPTH = 4;
/**
* Monitor a remote probe
*
* Monitor objects are the local interface to a remote <a href="Probe.html">Probe</a>.
* The probe may be running in this process or on a remote server.
*
* In a disconnected state, the monitor object contains information about
* the type, attributes, and location of the probe it will monitor.
*
* In a connected state, the monitor object contains the data attributes of
* the probe it is monitoring, and emits change events as the probe changes
* state.
*
* Many monitors may be attached to a single probe. When the probe data model
* changes, changes are broadcast to the connected monitors.
*
* Probes can be remotely controlled using the control() method.
* The control() method acts an RPC in that it accepts input arguments and
* returns results to the monitor initiating the request.
*
* Example:
*
* // Connecting a monitor to a probe
* var processMonitor = new Monitor({
* probeClass: 'Process'
* });
* processMonitor.connect();
*
* // Monitoring the probe
* processMonitor.on('change', function(){
* console.log('Changes:', processMonitor.getChangedAttributes());
* });
*
* // Remote control
* processMonitor.control('ping', function(error, response) {
* console.log('Ping response: ', response);
* });
*
* Monitoring a probe on a remote server requires the ```hostName``` parameter
* to be set.
*
* // Connecting to a remote monitor
* var processMonitor = new Monitor({
* probeClass: 'Process',
* hostName: 'remote-server1'
* });
* processMonitor.connect();
*
* Additional parameters can be set to identify a specific server if many
* servers are running on the specified ```hostName```.
*
* @class Monitor
* @extends Backbone.Model
* @constructor
* @param model - Initial data model. Can be a JS object or another Model.
* @param [model.id] {String} The monitor object id. Set externally.
* @param model.probeClass {String} Class name of the probe this is (or will be) monitoring.
* @param [model.initParams] {Object} Initialization parameters passed to the probe during instantiation.
* @param [model.hostName] {String} Hostname the probe is (or will) run on.
* If not set, the Router will connect with the first host capable of running this probe.
* @param [model.appName] {String} Application name the probe is (or will) run within.
* If not set, the Router will disregard the appName of the process it is connecting with.
* @param [model.appInstance] {String} Application instance ID the probe is (or will) run within.
* If not set, the Router will disregard the appInstance of the process it is connecting with.
* Application instances can (should) set the $NODE_APP_INSTANCE environment
* variable prior to running to uniquely identify their unique instance within a
* server or network. If this variable is not set prior to running the
* app, node-monitor will assign a unique ID among other running apps on the host.
* @param model.probeId {String} ID of the probe this is monitoring (once connected). READONLY
* @param model.PROBE_PARAMS... {(defined by the probe)} ... all other <strong>```model```</strong> parameters are READONLY parameters of the connected probe
*/
/**
* Receive real time notifications from the probe
*
* When the probe data model changes, all changed attributes are forwarded
* to monitors, triggering this event.
*
* All probe attributes are available in the monitor, and the
* getChangedAttributes() method returns the list of attributes changed
* since the last change event.
*
* myMonitor.on('change', function(){
* console.log('Changes:', myMonitor.getChangedAttributes());
* });
*
* @event change
*/
var Monitor = Backbone.Model.extend({
defaults: {
id: '',
name: '',
probeClass: '',
initParams: {},
hostName: '',
appName: '',
appInstance: ''
},
initialize: function(params, options) {
log.info('init', params);
},
/**
* Connect the monitor to the remote probe
*
* Upon connection, the monitor data model is a proxy of the current state
* of the probe.
*
* @method connect
* @param callback {Function(error)} Called when the probe is connected (or error)
*/
/**
* The monitor has successfully connected with the probe
* @event connect
*/
connect: function(callback) {
var t = this, startTime = Date.now();
Monitor.getRouter().connectMonitor(t, function(error) {
// Give the caller first crack at knowing we're connected,
// followed by anyone registered for the connect event.
if (callback) {callback(error);}
// Initial data setting into the model was done silently
// in order for the connect event to fire before the first
// change event. Fire the connect / change in the proper order.
if (!error) {
// An unfortunate side effect is any change listeners registered during
// connect will get triggered with the same values as during connect.
// To get around this, add change listeners from connect on nextTick.
t.trigger('connect', t);
t.trigger('change', t);
log.info('connected', {initParams: t.get('initParams'), probeId: t.get('probeId')});
stat.time('connect', Date.now() - startTime);
}
});
},
/**
* Get the connection to the remote probe
*
* This method returns the Connection object that represents the remote
* server used for communicating with the connected probe.
*
* If the probe is running internally or the monitor isn't currently
* connected, this will return null.
*
* @method getConnection
* @return connection {Connection} The connection object
*/
getConnection: function() {
var t = this;
return (t.probe && t.probe.connection ? t.probe.connection : null);
},
/**
* Is the monitor currently connected?
*
* @method isConnected
* @return {boolean} True if the monitor is currently connected
*/
isConnected: function() {
var t = this;
return (t.probe != null);
},
/**
* Disconnect from the remote probe
*
* This should be called when the monitor is no longer needed.
* It releases resources associated with monitoring the probe.
*
* If this was the last object monitoring the probe, the probe will be
* stopped, releasing resources associated with running the probe.
*
* @method disconnect
* @param callback {Function(error)} Called when disconnected (or error)
*/
/**
* The monitor has disconnected from the probe
* @event disconnect
* @param reason {String} Reason specified for the disconnect
* <ul>Known Reasons:
* <li>manual_disconnect - A manual call to disconnect() was made.</li>
* <li>connect_failed - Underlying transport connection problem.</li>
* <li>remote_disconnect - Underlying transport disconnected.</li>
* </ul>
*/
disconnect: function(callback) {
var t = this,
reason = 'manual_disconnect',
startTime = Date.now(),
probeId = t.get('probeId');
Monitor.getRouter().disconnectMonitor(t, reason, function(error, reason) {
if (callback) {callback(error);}
if (error) {
log.error('disconnect', {error: error});
}
else {
t.trigger('disconnect', reason);
log.info('disconnected', {reason: reason, probeId: probeId});
stat.time('disconnect', Date.now() - startTime);
}
});
},
/**
* Send a control message to the probe.
*
* Monitors can use this method to send a message and receive a response
* from a connected probe.
*
* The probe must implement the specified control method. All probes are
* derived from the base <a href="Probe.html">Probe</a> class, which offers
* a ping control.
*
* To send a ping message to a probe and log the results:
*
* var myMonitor.control('ping', console.log);
*
* @method control
* @param name {String} Name of the control message.
* @param [params] {Object} Named input parameters specific to the control message.
* @param [callback] {Function(error, response)} Function to call upon return.
* <ul>
* <li>error (Any) - An object describing an error (null if no errors)</li>
* <li>response (Any) - Response parameters specific to the control message.</li>
* </ul>
*/
control: function(name, params, callback) {
var t = this,
probe = t.probe,
logId = 'control.' + t.get('probeClass') + '.' + name,
startTime = Date.now();
// Switch callback if sent in 2nd arg
if (typeof params === 'function') {
callback = params;
params = null;
}
log.info(logId, params);
var whenDone = function(error, args) {
if (error) {
log.error(logId, error);
}
else {
log.info('return.' + logId, args);
stat.time(logId, Date.now() - startTime);
}
if (callback) {
callback.apply(t, arguments);
}
};
if (!probe) {
return whenDone('Probe not connected');
}
// Send the message internally or to the probe connection
if (probe.connection) {
probe.connection.emit('probe:control', {probeId: t.get('probeId'), name: name, params:params}, whenDone);
} else {
probe.onControl(name, params, whenDone);
}
},
/**
* Produce an object without monitor attributes
*
* A Monitor object contains a union of the connection attributes required for
* a Monitor, and the additional attributes defined by the probe it's monitoring.
*
* This method produces an object containing only the probe portion of
* those attributes.
*
* The id attribute of the returned JSON is set to the probeId from
* the monitor.
*
* @method toProbeJSON
* @param [options] {Object} Options to pass onto the model toJSON
* @return {Object} The probe attributes
*/
toProbeJSON: function(options) {
var t = this,
json = {id: t.get('probeId')};
// Transfer all non-monitor attrs
_.each(t.toJSON(options), function(value, key) {
if (!(key in t.defaults)) {
json[key] = value;
}
});
return json;
},
/**
* Produce an object with the monitor only attributes.
*
* A Monitor object contains a union of the connection attributes required for
* a Monitor, and the additional attributes defined by the probe it's monitoring.
*
* This method produces an object containing only the monitor portion of
* those attributes.
*
* @method toMonitorJSON
* @param [options] {Object} Options to pass onto the model toJSON
* @return {Object} The monitor attributes
*/
toMonitorJSON: function(options) {
var t = this,
json = {};
// Transfer all monitor attrs
_.each(t.toJSON(options), function(value, key) {
if (key in t.defaults) {
json[key] = value;
}
});
return json;
},
/**
* Produce a server string representation of the hostName:appName:appInstance
*
* Depending on the presence of the appName and appInstance, this will produce
* one of the following:
*
* hostName
* hostName:appName
* hostName:appName:appInstance
*
* @method toServerString
* @return {String} A string representation of the monitor server
*/
toServerString: function() {
return Monitor.toServerString(this.toMonitorJSON());
}
});
/////////////////////////
// Static helper methods
/////////////////////////
/**
* Generate a unique UUID-v4 style string
*
* This is a cross-platform UUID implementation used to uniquely identify
* model instances. It is a random number based UUID, and as such can't be
* guaranteed unique.
*
* @static
* @protected
* @method generateUniqueId
* @return {String} A globally unique ID
*/
Monitor.generateUniqueId = function() {
// Generate a 4 digit random hex string
stat.increment('generateUniqueId');
function rhs4() {return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);}
return (rhs4()+rhs4()+"-"+rhs4()+"-"+rhs4()+"-"+rhs4()+"-"+rhs4()+rhs4()+rhs4());
};
/**
* Generate a unique ID for a collection
*
* This generates an ID to be used for new elements of the collection,
* assuring they don't clash with other elements in the collection.
*
* @method Monitor.generateUniqueCollectionId
* @param collection {Backbone.Collection} The collection to generate an ID for
* @param [prefix] {String} An optional prefix for the id
* @return id {String} A unique ID with the specified prefix
*/
Monitor.generateUniqueCollectionId = function(collection, prefix) {
var id = '';
prefix = prefix || '';
// First time - get the largest idSequence in the collection
if (!collection.idSequence) {
collection.idSequence = 0;
collection.forEach(function(item){
var id = item.get('id') || '',
sequence = +id.substr(prefix.length);
if (collection.idSequence <= sequence) {
collection.idSequence = sequence + 1;
}
});
}
return prefix + collection.idSequence++;
};
/**
* Get the default router (an application singleton)
*
* This instantiates a Router on first call.
*
* @static
* @protected
* @method getRouter
* @return {Router} The default router.
*/
Monitor.getRouter = function() {
// Instantiate a router if no default
if (!Monitor.defaultRouter) {
Monitor.defaultRouter = new Monitor.Router();
// If there's a global socket.io server available,
// then we're running on the browser. Set the default
// gateway to the global io socket.
if (root.io) {
Monitor.defaultRouter.setGateway({
socket:root.io.connect()
});
}
}
// Return the router
return Monitor.defaultRouter;
};
/**
* Start a monitor server in this process
*
* This is a shortand for the following:
*
* var Monitor = require('monitor');
* var server = new Monitor.Server();
* server.start();
*
* It can be chained like this:
*
* var Monitor = require('monitor').start(),
* log = Monitor.getLogger('my-app');
*
* For more fine-tuned starting, see the <a href="Server.html">Server</a> api.
*
* @static
* @method start
* @param options {Object} - Server.start() options. OPTIONAL
* @param options.port {Integer} - Port to attempt listening on if server isn't specified. Default: 42000
* @param callback {Function(error)} - Called when the server is accepting connections.
* @return monitor {Monitor} - Returns the static Monitor class (for chaining)
*/
Monitor.start = function(options, callback) {
log.info('start', options);
// Get a default monitor
if (!Monitor.defaultServer) {
Monitor.defaultServer = new Monitor.Server();
Monitor.defaultServer.start(options, callback);
} else {
callback();
}
return Monitor;
};
/**
* Stop a started monitor server in this process
*
* @static
* @method stop
* @param callback {Function(error)} - Called when the server is accepting connections.
*/
Monitor.stop = function(callback) {
log.info('stop');
if (Monitor.defaultServer) {
Monitor.defaultServer.stop(callback);
delete Monitor.defaultServer;
} else {
callback();
}
};
/**
* Produce a server string representation of the hostName:appName:appInstance
*
* Depending on the presence of the appName and appInstance, this will produce
* one of the following:
*
* hostName
* hostName:appName
* hostName:appName:appInstance
*
* @method toServerString
* @param monitorJSON [Object] JSON object containing the following
* @param hostName {String} The host to monitor
* @param [appName] {String} The app name running on the host
* @param [appInstance] {String} The application instance ID running on the host
* @return {String} A string representation of the monitor server
*/
Monitor.toServerString = function(monitorJSON) {
var str = monitorJSON.hostName;
if (monitorJSON.appName) {
str += ':' + monitorJSON.appName;
if (monitorJSON.appInstance) {
str += ':' + monitorJSON.appInstance;
}
}
return str;
};
/**
* Produce a depth-limited copy of the specified object
*
* Functions are copied for visual inspection purposes - the fact that
* they are a function, and any prototype members. This is so a JSON.stringify
* of the result will show the functions (normally JSON.stringify doesn't output
* functions).
*
* This method is mostly for debugging - for producing a human-readable stream
* representation of the object. It is an exact copy, except for elements of
* type function.
*
* @method deepCopy
* @param value {Mixed} Object or value to copy
* @param [depth=4] {Integer} Maximum depth to return. If the depth exceeds
* this value, the string "[Object]" is returned as the value.
* @return {Mixed} A depth-limited copy of the value
*/
Monitor.deepCopy = function(value, depth) {
// Defaults
depth = typeof(depth) === 'undefined' ? DEFAULT_DEEP_COPY_DEPTH : depth;
// Simple value - return the raw value
if (typeof value !== 'object' && typeof value !== 'function') {
return value;
}
// Build a string representation of the type
var strType = '[Object]';
if (typeof value === 'function') {
strType = '[Function]';
} else if (Array.isArray(value)) {
strType = '[Array]';
}
// Limit reached
if (depth <= 0) {
return strType;
}
// Create a new object to copy into.
// Proactively add constructor so it's at the top of a function
var copy = Array.isArray(value) ? [] : {};
// Copy all elements (by reference)
for (var prop in value) {
if (!value.hasOwnProperty || value.hasOwnProperty(prop)) {
var elem = value[prop];
if (typeof elem === 'object' || typeof elem === 'function') {
copy[prop] = Monitor.deepCopy(elem, depth - 1);
}
else {
copy[prop] = elem;
}
}
}
// Special string formatting for functions
if (typeof value === 'function') {
if (_.isEmpty(copy)) {
// No sub-elements. Identify it as a function.
copy = strType;
} else {
// Sub-elements exist. Identify it as a function by placing
// a constructor at the top of the object
copy = _.extend({constructor: strType},copy);
}
}
// Return the copy
return copy;
};
/**
* Produce a recursion-safe JSON string.
*
* This method recurses the specified object to a maximum specified depth
* (default 4).
*
* It also indents sub-objects for debugging output. The indent level can be
* specified, or set to 0 for no indentation.
*
* This is mostly useful in debugging when the standard JSON.stringify
* returns an error.
*
* @method stringify
* @param value {Mixed} Object or value to turn into a JSON string
* @param [depth=4] {Integer} Maximum depth to return. If the depth exceeds
* this value, the string "[Object]" is returned as the value.
* @param [indent=2] {Integer} Indent the specified number of spaces (0=no indent)
* @return {String} A JSON stringified value
*/
Monitor.stringify = function(value, depth, indent) {
// Defaults
indent = typeof(indent) === 'undefined' ? 2 : indent;
// Return a stringified depth-limited deep copy
return JSON.stringify(Monitor.deepCopy(value, depth), null, indent);
};
/**
* Expose the stat logger class
*
* @protected
* @method setStatLoggerClass
* @param statLoggerClass {Function} Stat logger class to expose
*/
Monitor.setStatLoggerClass = function(StatLoggerClass) {
// Build the getStatLogger function
Monitor.getStatLogger = function(module) {
return new StatLoggerClass(module);
};
// Get the logger for the Monitor module
stat = Monitor.getStatLogger('Monitor');
};
/**
* Expose the logger class
*
* @protected
* @method setLoggerClass
* @param loggerClass {Function} Logger class to expose
*/
Monitor.setLoggerClass = function(LoggerClass) {
// Build the getLogger function
Monitor.getLogger = function(module) {
return new LoggerClass(module);
};
// Get the logger for the Monitor module
log = Monitor.getLogger('Monitor');
};
/**
* Constructor for a list of Monitor objects
*
* var myList = new Monitor.List(initialElements);
*
* @static
* @method List
* @param [items] {Array} Initial list items. These can be raw JS objects or Monitor data model objects.
* @return {Backbone.Collection} Collection of Monitor data model objects
*/
Monitor.List = Backbone.Collection.extend({model: Monitor});
// Monitor configurations. If running in a commonJS environment, load the
// configs from the config package. Otherwise just use the defaults.
var defaultConfig = {
appName: 'unknown',
serviceBasePort: 42000,
portsToScan: 20,
allowExternalConnections: false,
consoleLogListener: {
pattern: "{trace,warn,error,fatal}.*"
}
};
if (commonJS) {
Monitor.Config = require('config');
Monitor.Config.setModuleDefaults('Monitor', defaultConfig);
} else {
Monitor.Config = {Monitor: defaultConfig};
}
// Expose external dependencies
Monitor._ = _;
Monitor.Backbone = Backbone;
Monitor.Cron = Cron;
Monitor.commonJS = commonJS;
// Export for both commonJS and the browser
if (commonJS) {
module.exports = Monitor;
} else {
root.Monitor = Monitor;
}
}(this));