mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
387 lines
15 KiB
JavaScript
387 lines
15 KiB
JavaScript
define([
|
|
], function(){
|
|
/*global window, jQuery, console */
|
|
/*=============================================================================
|
|
TODO:
|
|
broken pipe due to onunload post in webkit, safari
|
|
need to use persistence
|
|
|
|
=============================================================================*/
|
|
/** @class MetricsLogger
|
|
*
|
|
* Object to cache, output, and post log/metric messages to the server.
|
|
* Meant to be attached to the Galaxy object.
|
|
*
|
|
* Log from objects by either attaching logger directly:
|
|
* panel.logger.metric( 'user dataset deletion', panel.user.id, hda.toJSON() )
|
|
* or using the LoggableMixin or addLogging function:
|
|
* MyBackboneModel.extend( LoggableMixin ).extend({ ... })
|
|
* addLogging( MyBackboneModel, 'my-backbone-model' )
|
|
*
|
|
* Log from templates by calling directly from Galaxy object:
|
|
* Galaxy.logger.metric( 'template loaded', { ownedByUser : true });
|
|
*
|
|
* If you attempt to log an un-serializable object (circular reference, window, etc.),
|
|
* that entry will not be cached (or sent). If you set consoleLevel and consoleLogger
|
|
* appropriately, a warning will be shown when this happens:
|
|
* > panel.metric( 'something weird with window', { window : window })
|
|
* !'Metrics logger could not stringify logArguments: ...'
|
|
*/
|
|
function MetricsLogger( options ){
|
|
options = options || {};
|
|
var self = this;
|
|
|
|
//TODO: this might be used if we store the logs in browser storage
|
|
///** */
|
|
//self.userId = options.userId || null;
|
|
|
|
/** the (optional) console to emit logs to */
|
|
self.consoleLogger = options.consoleLogger || null;
|
|
|
|
self._init( options );
|
|
return self;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------- defaults and constants
|
|
// see: python std lib, logging
|
|
MetricsLogger.ALL = 0;
|
|
MetricsLogger.DEBUG = 10;
|
|
MetricsLogger.INFO = 20;
|
|
MetricsLogger.WARN = 30;
|
|
MetricsLogger.ERROR = 40;
|
|
// metrics levels here?
|
|
//MetricsLogger.MinorEvent = 45;
|
|
//MetricsLogger.MajorEvent = 50;
|
|
MetricsLogger.METRIC = 50;
|
|
MetricsLogger.NONE = 100;
|
|
|
|
/** default options - override these through the constructor */
|
|
MetricsLogger.defaultOptions = {
|
|
/** if an incoming message has a level >= this, it will be cached - can also be a string (e.g. 'debug') */
|
|
logLevel : MetricsLogger.INFO,
|
|
/** if an incoming message has a level >= this, it will be output to the console */
|
|
consoleLevel : MetricsLogger.NONE,
|
|
/** the default 'namespace' or label associated with an incoming message (if none is passed) */
|
|
defaultNamespace : 'Galaxy',
|
|
/** the prefix attached to client-side logs to distinguish them in the metrics db */
|
|
clientPrefix : 'client.',
|
|
|
|
/** the maximum number of messages the cache should hold; if exceeded older messages are removed first */
|
|
maxCacheSize : 3000,
|
|
/** the number of messages accumulate before posting to the server; should be <= maxCacheSize */
|
|
postSize : 1000,
|
|
/** T/F whether to add a timestamp to incoming cached messages */
|
|
addTime : true,
|
|
|
|
/** the relative url to post messages to */
|
|
postUrl : '/api/metrics',
|
|
|
|
/** post when the page is unloaded? */// needs to be true if using the LoggingCache (for now)
|
|
postOnUnload : true,
|
|
|
|
/** an (optional) function that should return an object; used to send additional data with the metrics */
|
|
getPingData : undefined,
|
|
/** an (optional) function that will handle the servers response after successfully posting messages */
|
|
onServerResponse : undefined
|
|
};
|
|
|
|
//----------------------------------------------------------------------------- set up
|
|
/** initialize the logger with options, set up instance vars and cache, and add onpageunload to window */
|
|
MetricsLogger.prototype._init = function _init( options ){
|
|
var self = this;
|
|
self.options = {};
|
|
for( var k in MetricsLogger.defaultOptions ){
|
|
if( MetricsLogger.defaultOptions.hasOwnProperty( k ) ){
|
|
self.options[ k ] = ( options.hasOwnProperty( k ) )?( options[ k ] ):( MetricsLogger.defaultOptions[ k ] );
|
|
}
|
|
}
|
|
self.options.logLevel = self._parseLevel( self.options.logLevel );
|
|
self.options.consoleLevel = self._parseLevel( self.options.consoleLevel );
|
|
//self._emitToConsole( 'debug', 'MetricsLogger', 'MetricsLogger.options:', self.options );
|
|
|
|
self._sending = false;
|
|
self._postSize = self.options.postSize;
|
|
self._initCache();
|
|
|
|
// post the entire cache on unload (since LoggingCache isn't persistent ATM)
|
|
if( self.options.postOnUnload ){
|
|
self.onpageunload = function onpageunload( ev ){
|
|
ev.preventDefault();
|
|
self._postCache({ count: self.cache.length() });
|
|
};
|
|
window.addEventListener( 'unload', self.onpageunload );
|
|
}
|
|
|
|
return self;
|
|
};
|
|
|
|
/** initialize the cache */
|
|
MetricsLogger.prototype._initCache = function _initCache(){
|
|
this.cache = new LoggingCache({ maxSize : this.options.maxCacheSize });
|
|
|
|
};
|
|
|
|
/** return the numeric log level if level in 'none, debug, log, info, warn, error' */
|
|
MetricsLogger.prototype._parseLevel = function _parseLevel( level ){
|
|
var type = typeof level;
|
|
if( type === 'number' ){ return level; }
|
|
var upper = level.toUpperCase();
|
|
if( type === 'string' && MetricsLogger.hasOwnProperty( upper ) ){
|
|
return MetricsLogger[ upper ];
|
|
}
|
|
throw new Error( 'Unknown log level: ' + level );
|
|
};
|
|
|
|
|
|
//----------------------------------------------------------------------------- main entry point
|
|
/** record a log/message's arguments to the cache and/or the console based on level and namespace */
|
|
MetricsLogger.prototype.emit = function emit( level, namespace, logArguments ){
|
|
//this._emitToConsole( 'debug', 'MetricsLogger', [ 'emit:', level, namespace, logArguments ]);
|
|
var self = this;
|
|
namespace = namespace || self.options.defaultNamespace;
|
|
if( !level || !logArguments ){
|
|
return self;
|
|
}
|
|
// add to cache if proper level
|
|
//TODO: respect do not track?
|
|
//if( !navigator.doNotTrack && level >= self.options.logLevel ){
|
|
level = self._parseLevel( level );
|
|
if( level >= self.options.logLevel ){
|
|
self._addToCache( level, namespace, logArguments );
|
|
}
|
|
// also emit to consoleLogger if proper level for that
|
|
if( self.consoleLogger && level >= self.options.consoleLevel ){
|
|
self._emitToConsole( level, namespace, logArguments );
|
|
}
|
|
return self;
|
|
};
|
|
|
|
//----------------------------------------------------------------------------- cache
|
|
/** add a message to the cache and if messages.length is high enough post them to the server */
|
|
MetricsLogger.prototype._addToCache = function _addToCache( level, namespace, logArguments ){
|
|
this._emitToConsole( 'debug', 'MetricsLogger',
|
|
[ '_addToCache:', arguments, this.options.addTime, this.cache.length() ]);
|
|
//this._emitToConsole( 'debug', 'MetricsLogger', [ '\t logArguments:', logArguments ]);
|
|
var self = this;
|
|
// try add to the cache and if we've got _postSize number of entries, attempt to post them to the server
|
|
try {
|
|
var newLength = self.cache.add( self._buildEntry( level, namespace, logArguments ) );
|
|
if( newLength >= self._postSize ){
|
|
self._postCache();
|
|
}
|
|
// discard entry if an error occurs, but warn if level set to do so
|
|
} catch( err ){
|
|
if( self.options.consoleLevel <= MetricsLogger.WARN ){
|
|
console.warn( 'Metrics logger could not stringify logArguments:', namespace, logArguments );
|
|
console.error( err );
|
|
}
|
|
}
|
|
return self;
|
|
};
|
|
|
|
/** build a log cache entry object from the given level, namespace, and arguments (optionally adding timestamp */
|
|
MetricsLogger.prototype._buildEntry = function _buildEntry( level, namespace, logArguments ){
|
|
this._emitToConsole( 'debug', 'MetricsLogger', [ '_buildEntry:', arguments ]);
|
|
var entry = {
|
|
level : level,
|
|
namespace : this.options.clientPrefix + namespace,
|
|
args : logArguments
|
|
};
|
|
if( this.options.addTime ){
|
|
entry.time = new Date().toISOString();
|
|
}
|
|
return entry;
|
|
};
|
|
|
|
/** post _postSize messages from the cache to the server, removing them if successful
|
|
* if the post fails, wait until maxCacheSize is accumulated instead and try again then
|
|
* in addition to the messages from the cache ('metrics'), any info from getPingData (if set) will be sent
|
|
* onServerResponse will be called (if set) with any response from the server
|
|
*/
|
|
MetricsLogger.prototype._postCache = function _postCache( options ){
|
|
options = options || {};
|
|
this._emitToConsole( 'info', 'MetricsLogger', [ '_postCache', options, this._postSize ]);
|
|
//TODO: remove jq dependence
|
|
|
|
// short circuit if we're already sending
|
|
if( !this.options.postUrl || this._sending ){
|
|
return jQuery.when({});
|
|
}
|
|
|
|
var self = this,
|
|
postSize = options.count || self._postSize,
|
|
// do not splice - remove after *successful* post
|
|
entries = self.cache.get( postSize ),
|
|
entriesLength = entries.length,
|
|
// use the optional getPingData to add any extra info we may want to send
|
|
postData = ( typeof self.options.getPingData === 'function' )?( self.options.getPingData() ):( {} );
|
|
//console.debug( postSize, entriesLength );
|
|
|
|
// add the metrics and send
|
|
postData.metrics = self._preprocessCache( entries );
|
|
self._sending = true;
|
|
return jQuery.post( self.options.postUrl, postData )
|
|
.always( function(){
|
|
self._sending = false;
|
|
})
|
|
.fail( function(){
|
|
// if we failed the previous time, set the next post target to the max num of entries
|
|
self._postSize = self.options.maxCacheSize;
|
|
})
|
|
.done( function( response ){
|
|
if( typeof self.options.onServerResponse === 'function' ){
|
|
self.options.onServerResponse( response );
|
|
}
|
|
// only remove if post successful
|
|
self.cache.remove( entriesLength );
|
|
console.debug( 'removed entries:', entriesLength, 'size now:', self.cache.length() );
|
|
// if we succeeded, reset the post target to the normal num of entries
|
|
self._postSize = self.options.postSize;
|
|
});
|
|
// return the xhr promise
|
|
};
|
|
|
|
/** Preprocess a number of cache entries for sending to the server (stringification) */
|
|
MetricsLogger.prototype._preprocessCache = function _preprocessCache( entries ){
|
|
return [ '[', ( entries.join( ',\n' ) ), ']' ].join( '\n' );
|
|
//return [ '[', ( entries.join( ',' ) ), ']' ].join( '' );
|
|
};
|
|
|
|
|
|
//----------------------------------------------------------------------------- console
|
|
/** output message to console based on level and consoleLogger type */
|
|
MetricsLogger.prototype._emitToConsole = function _emitToConsole( level, namespace, logArguments ){
|
|
//console.debug( '_emitToConsole:', level, namespace, logArguments );
|
|
var self = this;
|
|
if( !self.consoleLogger ){ return self; }
|
|
|
|
var args = Array.prototype.slice.call( logArguments, 0 );
|
|
args.unshift( namespace );
|
|
//TODO: script location and/or source maps?
|
|
//TODO: branch on navigator.userAgent == AIIEEE - it only has log
|
|
if( level >= MetricsLogger.METRIC && typeof( self.consoleLogger.info ) === 'function' ){
|
|
return self.consoleLogger.info.apply( self.consoleLogger, args );
|
|
|
|
} else if( level >= MetricsLogger.ERROR && typeof( self.consoleLogger.error ) === 'function' ){
|
|
return self.consoleLogger.error.apply( self.consoleLogger, args );
|
|
} else if( level >= MetricsLogger.WARN && typeof( self.consoleLogger.warn ) === 'function' ){
|
|
self.consoleLogger.warn.apply( self.consoleLogger, args );
|
|
} else if( level >= MetricsLogger.INFO && typeof( self.consoleLogger.info ) === 'function' ){
|
|
self.consoleLogger.info.apply( self.consoleLogger, args );
|
|
} else if( level >= MetricsLogger.DEBUG && typeof( self.consoleLogger.debug ) === 'function' ){
|
|
self.consoleLogger.debug.apply( self.consoleLogger, args );
|
|
} else if( typeof( self.consoleLogger.log ) === 'function' ){
|
|
self.consoleLogger.log.apply( self.consoleLogger, args );
|
|
}
|
|
return self;
|
|
};
|
|
|
|
//----------------------------------------------------------------------------- shortcuts
|
|
// generic functions when logging from non-namespaced object (e.g. templates)
|
|
/** debug to default namespace */
|
|
MetricsLogger.prototype.debug = function debug(){
|
|
this.emit( MetricsLogger.DEBUG, this.options.defaultNamespace, arguments );
|
|
};
|
|
|
|
/** log to default namespace */
|
|
MetricsLogger.prototype.log = function log(){
|
|
this.emit( 1, this.options.defaultNamespace, arguments );
|
|
};
|
|
|
|
/** info to default namespace */
|
|
MetricsLogger.prototype.info = function info(){
|
|
this.emit( MetricsLogger.INFO, this.options.defaultNamespace, arguments );
|
|
};
|
|
|
|
/** warn to default namespace */
|
|
MetricsLogger.prototype.warn = function warn(){
|
|
this.emit( MetricsLogger.WARN, this.options.defaultNamespace, arguments );
|
|
};
|
|
|
|
/** error to default namespace */
|
|
MetricsLogger.prototype.error = function error(){
|
|
this.emit( MetricsLogger.ERROR, this.options.defaultNamespace, arguments );
|
|
};
|
|
|
|
/** metric to default namespace */
|
|
MetricsLogger.prototype.metric = function metric(){
|
|
this.emit( MetricsLogger.METRIC, this.options.defaultNamespace, arguments );
|
|
};
|
|
|
|
|
|
//=============================================================================
|
|
/** @class LoggingCache
|
|
* Simple implementation of cache wrapping an array.
|
|
*
|
|
* Formats an entry before it's cached and only keeps options.maxSize number
|
|
* of entries. Older entries are deleted first.
|
|
*/
|
|
function LoggingCache( options ){
|
|
var self = this;
|
|
self._cache = [];
|
|
return self._init( options || {} );
|
|
}
|
|
|
|
/** default options */
|
|
LoggingCache.defaultOptions = {
|
|
/** maximum number of entries to keep before discarding oldest */
|
|
maxSize : 5000
|
|
};
|
|
|
|
/** initialize with options */
|
|
LoggingCache.prototype._init = function _init( options ){
|
|
this.maxSize = options.maxSize || LoggingCache.defaultOptions.maxSize;
|
|
return this;
|
|
};
|
|
|
|
/** add an entry to the cache, removing the oldest beforehand if size >= maxSize */
|
|
LoggingCache.prototype.add = function add( entry ){
|
|
var self = this,
|
|
overage = ( self.length() + 1 ) - self.maxSize;
|
|
if( overage > 0 ){
|
|
self.remove( overage );
|
|
}
|
|
self._cache.push( self._preprocessEntry( entry ) );
|
|
return self.length();
|
|
};
|
|
|
|
/** process the entry before caching */
|
|
LoggingCache.prototype._preprocessEntry = function _preprocessEntry( entry ){
|
|
return JSON.stringify( entry );
|
|
};
|
|
|
|
/** return the length --- oh, getters where are you? */
|
|
LoggingCache.prototype.length = function length(){
|
|
return this._cache.length;
|
|
};
|
|
|
|
/** get count number of entries starting with the oldest */
|
|
LoggingCache.prototype.get = function get( count ){
|
|
return this._cache.slice( 0, count );
|
|
};
|
|
|
|
/** remove count number of entries starting with the oldest */
|
|
LoggingCache.prototype.remove = function remove( count ){
|
|
return this._cache.splice( 0, count );
|
|
};
|
|
|
|
/** stringify count number of entries (but do not remove) */
|
|
LoggingCache.prototype.stringify = function stringify( count ){
|
|
return [ '[', ( this.get( count ).join( ',\n' ) ), ']' ].join( '\n' );
|
|
};
|
|
|
|
/** outputs entire cache to console */
|
|
LoggingCache.prototype.print = function print(){
|
|
// popup? (really, carl? a popup?) - easier to copy/paste
|
|
this._cache.forEach( function( entry ){
|
|
console.log( entry );
|
|
});
|
|
};
|
|
|
|
|
|
//=============================================================================
|
|
return {
|
|
MetricsLogger : MetricsLogger
|
|
};
|
|
});
|