Drop metrics-logger

This commit is contained in:
Dannon Baker
2023-02-14 07:59:24 -05:00
parent 59092bb753
commit fd66b30a9d
15 changed files with 12 additions and 1077 deletions
-34
View File
@@ -1,41 +1,8 @@
import $ from "jquery";
import _ from "underscore";
import Backbone from "backbone";
import addLogging from "utils/add-logging";
import _l from "utils/localization";
//==============================================================================
/** @class Mixin to add logging capabilities to an object.
* Designed to allow switching an objects log output off/on at one central
* statement. Can be used with plain browser console (or something more
* complex like an AJAX logger).
* <br />NOTE: currently only uses the console.debug log function
* (as opposed to debug, error, warn, etc.)
* @name LoggableMixin
*
* @example
* // Add to your models/views at the definition using chaining:
* var MyModel = Backbone.Model.extend( LoggableMixin ).extend({ // ... });
*
* // or - more explicitly AFTER the definition:
* var MyModel = Backbone.Model.extend({
* logger : console
* // ...
* this.log( '$#%& it! - broken already...' );
* })
* _.extend( MyModel.prototype, LoggableMixin )
*
*/
var LoggableMixin = /** @lends LoggableMixin# */ {
// replace null with console (if available) to see all logs for a particular view/model
/** The logging object whose log function will be used to output
* messages. Null will supress all logging. Commonly set to console.
*/
logger: null,
/** @type {String} a namespace for filtering/focusing log output */
_logNamespace: ".",
};
addLogging(LoggableMixin);
//==============================================================================
/** Backbone model that syncs to the browser's sessionStorage API.
@@ -575,7 +542,6 @@ function buildComparator(attribute_name, options) {
//==============================================================================
export default {
LoggableMixin: LoggableMixin,
SessionStorageModel: SessionStorageModel,
mixin: mixin,
SearchableModelMixin: SearchableModelMixin,
-108
View File
@@ -1,10 +1,7 @@
import $ from "jquery";
import _ from "underscore";
import Backbone from "backbone";
import BASE_MVC from "./base-mvc";
import userModel from "./user-model";
import metricsLogger from "utils/metrics-logger";
import addLogging from "utils/add-logging";
import { localize, _setUserLocale, _getUserLocale } from "utils/localization";
import { getGalaxyInstance } from "app";
import { create, dialog } from "utils/data";
@@ -12,7 +9,6 @@ import { create, dialog } from "utils/data";
// ============================================================================
/** Base galaxy client-side application.
* Iniitializes:
* logger : the logger/metrics-logger
* localize : the string localizer
* config : the current configuration (any k/v in
* galaxy.ini available from the configuration API)
@@ -22,29 +18,9 @@ export function GalaxyApp(options = {}, bootstrapped = {}) {
this._init(options, bootstrapped);
}
// add logging shortcuts for this object
addLogging(GalaxyApp, "GalaxyApp");
// a debug flag can be set via local storage and made available during script/page loading
const DEBUGGING_KEY = "galaxy:debug";
const NAMESPACE_KEY = `${DEBUGGING_KEY}:namespaces`;
const FLATTEN_LOG_MESSAGES_KEY = `${DEBUGGING_KEY}:flatten`;
let localDebugging = false;
try {
localDebugging = localStorage.getItem(DEBUGGING_KEY) == "true";
} catch (storageErr) {
console.log(localize("localStorage not available for debug flag retrieval"));
}
/** initalize options and sub-components */
GalaxyApp.prototype._init = function (options, bootstrapped) {
_.extend(this, Backbone.Events);
if (localDebugging) {
this.logger = console;
console.debug("debugging galaxy:", "options:", options, "bootstrapped:", bootstrapped);
}
this._processOptions(options);
this._initConfig(options.config || {});
@@ -119,9 +95,6 @@ GalaxyApp.prototype._processOptions = function _processOptions(options) {
GalaxyApp.prototype._initConfig = function _initConfig(config) {
this.config = config;
// give precendence to localdebugging for this setting
this.config.debug = localDebugging || this.config.debug;
return this;
};
@@ -141,41 +114,6 @@ GalaxyApp.prototype._patchGalaxy = function _patchGalaxy(patchWith) {
}
};
/** set up the metrics logger (utils/metrics-logger) and pass loggerOptions */
GalaxyApp.prototype._initLogger = function _initLogger(loggerOptions) {
// default to console logging at the debug level if the debug flag is set
if (this.config.debug) {
loggerOptions.consoleLogger = loggerOptions.consoleLogger || console;
loggerOptions.consoleLevel = loggerOptions.consoleLevel || metricsLogger.MetricsLogger.ALL;
// load any logging namespaces from localStorage if we can
try {
loggerOptions.consoleNamespaceWhitelist = localStorage.getItem(NAMESPACE_KEY).split(",");
} catch (storageErr) {
console.debug(storageErr);
}
try {
loggerOptions.consoleFlattenMessages = localStorage.getItem(FLATTEN_LOG_MESSAGES_KEY) == "true";
} catch (storageErr) {
console.debug(storageErr);
}
console.log(loggerOptions.consoleFlattenMessages);
}
this.logger = new metricsLogger.MetricsLogger(loggerOptions);
this.emit = {};
["log", "debug", "info", "warn", "error", "metric"].map((i) => {
this.emit[i] = (data) => {
this.logger.emit(i, arguments[0], Array.prototype.slice.call(arguments, 1));
};
});
if (this.config.debug) {
// add this logger to mvc's loggable mixin so that all models can use the logger
BASE_MVC.LoggableMixin.logger = this.logger;
}
return this;
};
/** add the localize fn to this object and the window namespace (as '_l') */
GalaxyApp.prototype._initLocale = function _initLocale(options) {
this.debug("_initLocale:", options);
@@ -222,7 +160,6 @@ GalaxyApp.prototype._initUserLocale = function _initUserLocale(options) {
GalaxyApp.prototype._initUser = function _initUser(userJSON) {
this.debug("_initUser:", userJSON);
this.user = new userModel.User(userJSON);
this.user.logger = this.logger;
return this;
};
@@ -248,51 +185,6 @@ GalaxyApp.prototype._setUpListeners = function _setUpListeners() {
return this;
};
/** Turn debugging/console-output on/off by passing boolean. Pass nothing to get current setting. */
GalaxyApp.prototype.debugging = function _debugging(setting) {
try {
if (setting === undefined) {
return localStorage.getItem(DEBUGGING_KEY) === "true";
}
if (setting) {
localStorage.setItem(DEBUGGING_KEY, true);
return true;
}
localStorage.removeItem(DEBUGGING_KEY);
// also remove all namespaces
this.debuggingNamespaces(null);
} catch (storageErr) {
console.log(localize("localStorage not available for debug flag retrieval"));
}
return false;
};
/** Add, remove, or clear namespaces from the debugging filters
* Pass no arguments to retrieve the existing namespaces as an array.
* Pass in null to clear all namespaces (all logging messages will show now).
* Pass in an array of strings or single string of the namespaces to filter to.
* Returns the new/current namespaces as an array;
*/
GalaxyApp.prototype.debuggingNamespaces = function _debuggingNamespaces(namespaces) {
try {
if (namespaces === undefined) {
const csv = localStorage.getItem(NAMESPACE_KEY);
return typeof csv === "string" ? csv.split(",") : [];
} else if (namespaces === null) {
localStorage.removeItem(NAMESPACE_KEY);
} else {
localStorage.setItem(NAMESPACE_KEY, namespaces);
}
const newSettings = this.debuggingNamespaces();
if (this.logger) {
this.logger.options.consoleNamespaceWhitelist = newSettings;
}
return newSettings;
} catch (storageErr) {
console.log(localize("localStorage not available for debug namespace retrieval"));
}
};
/** string rep */
GalaxyApp.prototype.toString = function toString() {
-4
View File
@@ -2,7 +2,6 @@
* TODO: Make this part of galaxy.js later, once the global refs are gone
*/
import addLogging from "utils/add-logging";
import config from "config";
import { GalaxyApp } from "./galaxy";
import { serverPath } from "utils/serverPath";
@@ -17,9 +16,6 @@ export function setGalaxyInstance(factory) {
if (!(newInstance instanceof GalaxyApp)) {
newInstance = new GalaxyApp(newInstance);
}
if (newInstance.debug === undefined) {
addLogging(newInstance, "GalaxyApp");
}
storage._galaxyInstance = newInstance;
+1 -12
View File
@@ -3,14 +3,12 @@ import { getAppRoot } from "onload/loadConfig";
import baseMVC from "./base-mvc";
import _l from "utils/localization";
var logNamespace = "user";
//==============================================================================
/** @class Model for a Galaxy user (including anonymous users).
* @name User
*/
var User = Backbone.Model.extend(baseMVC.LoggableMixin).extend(
var User = Backbone.Model.extend(
/** @lends User.prototype */ {
_logNamespace: logNamespace,
/** API location for this resource */
urlRoot: function () {
@@ -142,15 +140,6 @@ User.getCurrentUserFromApi = (options) => {
return currentUser;
};
// (stub) collection for users (shouldn't be common unless admin UI)
//var UserCollection = Backbone.Collection.extend(baseMVC.LoggableMixin).extend({
// model: User,
// urlRoot: function() {
// return `${getAppRoot()}api/users`;
// }
// //logger : console,
//});
//==============================================================================
export default {
User: User,
@@ -42,7 +42,7 @@ export default Backbone.View.extend({
this.field = typeof this[fieldClass] === "function" ? this[fieldClass].call(this, input_def) : null;
if (!this.field) {
this.field = input_def.options ? this._fieldSelect(input_def) : this._fieldText(input_def);
Galaxy.emit.debug("form-parameters::_addRow()", `Auto matched field type (${input_def.type}).`);
console.debug("form-parameters::_addRow()", `Auto matched field type (${input_def.type}).`);
}
if (input_def.value === undefined) {
input_def.value = null;
@@ -153,7 +153,7 @@ var FolderContainer = Backbone.Model.extend({
var file_item = new Ldda(obj.folder_contents[i]);
this.get("folder").add(file_item);
} else {
Galaxy.emit.error("Unknown folder item type encountered while parsing response.");
console.error("Unknown folder item type encountered while parsing response.");
}
}
return obj;
+4 -4
View File
@@ -222,7 +222,7 @@ const View = Backbone.View.extend({
/** Return the currently selected dataset values */
value: function (new_value) {
const galaxy = getGalaxyInstance();
const Galaxy = getGalaxyInstance();
if (new_value) {
this._patchValue(new_value);
this.model.set("value", new_value);
@@ -240,7 +240,7 @@ const View = Backbone.View.extend({
const unpatchedValue = this._unpatchValue(details);
result.values.push(unpatchedValue);
} else {
galaxy.emit.debug(
console.debug(
"ui-select-content::value()",
`Requested details not found for '${id_list[i]}'.`
);
@@ -252,7 +252,7 @@ const View = Backbone.View.extend({
}
}
} else {
galaxy.emit.debug("ui-select-content::value()", `Invalid value/source '${new_value}'.`);
console.debug("ui-select-content::value()", `Invalid value/source '${new_value}'.`);
}
return null;
},
@@ -303,7 +303,7 @@ const View = Backbone.View.extend({
this.config = Configurations[config_id];
} else {
this.config = Configurations["data"];
galaxy.emit.debug("ui-select-content::_changeType()", `Invalid configuration/type id '${config_id}'.`);
console.debug("ui-select-content::_changeType()", `Invalid configuration/type id '${config_id}'.`);
}
// prepare extension component of error message
@@ -44,7 +44,7 @@ export var View = Backbone.View.extend({
add: function (options) {
const Galaxy = getGalaxyInstance();
if (!options.id || this.list[options.id]) {
Galaxy.emit.debug("form-repeat::add()", "Duplicate or invalid repeat block id.");
console.debug("form-repeat::add()", "Duplicate or invalid repeat block id.");
return;
}
var button_delete = new Ui.Button({
@@ -77,7 +77,7 @@ export var View = Backbone.View.extend({
del: function (id) {
const Galaxy = getGalaxyInstance();
if (!this.list[id]) {
Galaxy.emit.debug("form-repeat::del()", "Invalid repeat block id.");
console.debug("form-repeat::del()", "Invalid repeat block id.");
return;
}
this.$list.find(`#${id}`).remove();
-35
View File
@@ -1,35 +0,0 @@
//==============================================================================
var LOGGING_FNS = ["log", "debug", "info", "warn", "error", "metric"];
/** adds logging functions to an obj.prototype (or obj directly) adding a namespace for filtering
* @param {Object} obj
* @param {String} namespace
*/
function addLogging(obj, namespace) {
var addTo = obj.prototype !== undefined ? obj.prototype : obj;
if (namespace !== undefined) {
addTo._logNamespace = namespace;
}
// give the object each
LOGGING_FNS.forEach((logFn) => {
addTo[logFn] = function () {
if (!this.logger) {
return undefined;
}
if (this.logger.emit) {
return this.logger.emit(logFn, this._logNamespace, arguments);
}
if (this.logger[logFn]) {
//TODO:! there has to be a way to get the lineno/file into this
// http://stackoverflow.com/questions/13815640/a-proper-wrapper-for-console-log-with-correct-line-number
// http://www.paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/
return this.logger[logFn].apply(this.logger, arguments);
}
return undefined;
};
});
return obj;
}
//==============================================================================
export default addLogging;
+3 -3
View File
@@ -29,7 +29,7 @@ export default Backbone.Model.extend({
process.promise().always(function () {
delete self.active[id];
has_deferred &&
Galaxy.emit.debug(
console.debug(
"deferred::execute()",
`${this.state().charAt(0).toUpperCase() + this.state().slice(1)} ${id}`
);
@@ -38,7 +38,7 @@ export default Backbone.Model.extend({
// deferred queue
$.when(this.last).always(() => {
if (self.active[id]) {
has_deferred && Galaxy.emit.debug("deferred::execute()", `Running ${id}`);
has_deferred && console.debug("deferred::execute()", `Running ${id}`);
callback(process);
!has_deferred && process.resolve();
} else {
@@ -52,7 +52,7 @@ export default Backbone.Model.extend({
*/
reset: function () {
const Galaxy = getGalaxyInstance();
Galaxy.emit.debug("deferred::execute()", "Reset");
console.debug("deferred::execute()", "Reset");
for (var i in this.active) {
this.active[i] = false;
}
-504
View File
@@ -1,504 +0,0 @@
/*=============================================================================
TODO:
while anon: logs saved to 'logs-null' - this will never post
unless we manually do so at/after login
OR prepend when userId and localStorage has 'logs-null'
wire up _delayPost and test
=============================================================================*/
/** @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: ...'
*/
import jQuery from "jquery";
function MetricsLogger(options) {
options = options || {};
var self = this;
///** get the current user's id from bootstrapped data or options */
self.userId = window.bootstrapped && window.bootstrapped.user ? window.bootstrapped.user.id : null;
self.userId = 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.LOG = 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.NONE,
/** 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 namespaces output to the console (all namespaces will be output if this is falsy)
* note: applies only to the console (not the event/metrics log/cache)
*/
consoleNamespaceWhitelist: null,
/** Force all messages into simple strings. */
consoleFlattenMessages: false,
/** 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,
/** string to prefix to userid for cache web storage */
cacheKeyPrefix: "logs-",
/** the relative url to post messages to */
postUrl: "/api/metrics",
/** delay before trying post again after two failures */
delayPostInMs: 1000 * 60 * 10,
/** 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 (Object.prototype.hasOwnProperty.call(MetricsLogger.defaultOptions, k)) {
self.options[k] = Object.prototype.hasOwnProperty.call(options, 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 );
/** is the logger currently sending? */
self._sending = false;
/** the setTimeout id if the logger POST has failed more than once */
self._waiting = null;
/** the current number of entries to send in a POST */
self._postSize = self.options.postSize;
self._initCache();
return self;
};
/** initialize the cache */
MetricsLogger.prototype._initCache = function _initCache() {
try {
this.cache = new LoggingCache({
maxSize: this.options.maxCacheSize,
key: this.options.cacheKeyPrefix + this.userId,
});
} catch (err) {
this._emitToConsole("warn", "MetricsLogger", ["Could not intitialize logging cache:", err]);
this.options.logLevel = MetricsLogger.NONE;
}
};
/** 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;
}
if (type === "string") {
var upper = level.toUpperCase();
if (Object.prototype.hasOwnProperty.call(MetricsLogger, 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) {
self._emitToConsole("warn", "MetricsLogger", [
"Metrics logger could not stringify logArguments:",
namespace,
logArguments,
]);
self._emitToConsole("error", "MetricsLogger", [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]);
// short circuit if we're already sending
if (!this.options.postUrl || this._sending) {
return jQuery.when({});
}
var self = this;
var postSize = options.count || self._postSize;
var // do not splice - remove after *successful* post
entries = self.cache.get(postSize);
var entriesLength = entries.length;
var // 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 = JSON.stringify(entries);
//console.debug( postData.metrics );
self._sending = true;
return jQuery
.post(self.options.postUrl, postData)
.always(() => {
self._sending = false;
})
.fail((xhr, status, message) => {
// if we failed the previous time, set the next post target to the max num of entries
self._postSize = self.options.maxCacheSize;
//TODO:??
// log this failure to explain any gap in metrics
self.emit("error", "MetricsLogger", [
"_postCache error:",
xhr.readyState,
xhr.status,
xhr.responseJSON || xhr.responseText,
]);
//TODO: still doesn't solve the problem that when cache == max, post will be tried on every emit
//TODO: see _delayPost
})
.done((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
};
/** set _waiting to true and, after delayPostInMs, set it back to false */
MetricsLogger.prototype._delayPost = function _delayPost() {
//TODO: this won't work between pages
var self = this;
self._waiting = window.setTimeout(() => {
self._waiting = null;
}, self.options.delayPostInMs);
};
function usefulToString(arg) {
var asStr = String(arg);
if (asStr == "[object Object]") {
try {
asStr = JSON.stringify(arg);
} catch (e) {
// If arg has cyclic reference, return String
// otherwise rendering stops and we have incomplete page
console.error(e);
return String(arg);
}
}
return asStr;
}
//----------------------------------------------------------------------------- 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;
var whitelist = self.options.consoleNamespaceWhitelist;
if (!self.consoleLogger) {
return self;
}
// if a whitelist for namespaces is set, bail if this namespace is not in the list
if (whitelist && whitelist.indexOf(namespace) === -1) {
return self;
}
var args = Array.prototype.slice.call(logArguments, 0);
args.unshift(namespace);
if (self.options.consoleFlattenMessages) {
args = [args.map(usefulToString).join(" ")];
}
//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)
/** log to default namespace */
MetricsLogger.prototype.log = function log() {
this.emit(1, this.options.defaultNamespace, Array.prototype.slice.call(arguments, 0));
};
/** debug to default namespace */
MetricsLogger.prototype.debug = function debug() {
this.emit(MetricsLogger.DEBUG, this.options.defaultNamespace, Array.prototype.slice.call(arguments, 0));
};
/** info to default namespace */
MetricsLogger.prototype.info = function info() {
this.emit(MetricsLogger.INFO, this.options.defaultNamespace, Array.prototype.slice.call(arguments, 0));
};
/** warn to default namespace */
MetricsLogger.prototype.warn = function warn() {
this.emit(MetricsLogger.WARN, this.options.defaultNamespace, Array.prototype.slice.call(arguments, 0));
};
/** error to default namespace */
MetricsLogger.prototype.error = function error() {
this.emit(MetricsLogger.ERROR, this.options.defaultNamespace, Array.prototype.slice.call(arguments, 0));
};
/** metric to default namespace */
MetricsLogger.prototype.metric = function metric() {
this.emit(MetricsLogger.METRIC, this.options.defaultNamespace, Array.prototype.slice.call(arguments, 0));
};
/* ============================================================================
TODO:
need a performance pass - the JSON un/parsing is a bit much
============================================================================ */
/** @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;
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) {
if (!this._hasStorage()) {
//TODO: fall back to local storage
throw new Error("LoggingCache needs localStorage");
}
if (!options.key) {
throw new Error("LoggingCache needs key for localStorage");
}
this.key = options.key;
this._initStorage();
this.maxSize = options.maxSize || LoggingCache.defaultOptions.maxSize;
return this;
};
/** tests for localStorage fns */
LoggingCache.prototype._hasStorage = function _hasStorage() {
//TODO: modernizr
var test = "test";
try {
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
};
/** if no localStorage set for key, initialize to empty array */
LoggingCache.prototype._initStorage = function _initStorage() {
if (localStorage.getItem(this.key) === null) {
return this.empty();
}
return this;
};
/** add an entry to the cache, removing the oldest beforehand if size >= maxSize */
LoggingCache.prototype.add = function add(entry) {
var self = this;
var _cache = self._fetchAndParse();
var overage = _cache.length + 1 - self.maxSize;
if (overage > 0) {
_cache.splice(0, overage);
}
_cache.push(entry);
self._unparseAndStore(_cache);
return _cache.length;
};
/** get the entries from localStorage and parse them */
LoggingCache.prototype._fetchAndParse = function _fetchAndParse() {
var self = this;
return JSON.parse(localStorage.getItem(self.key));
};
/** stringify the entries and put them in localStorage */
LoggingCache.prototype._unparseAndStore = function _unparseAndStore(entries) {
var self = this;
return localStorage.setItem(self.key, JSON.stringify(entries));
};
///** 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._fetchAndParse().length;
};
/** get count number of entries starting with the oldest */
LoggingCache.prototype.get = function get(count) {
return this._fetchAndParse().slice(0, count);
};
/** remove count number of entries starting with the oldest */
LoggingCache.prototype.remove = function remove(count) {
var _cache = this._fetchAndParse();
var removed = _cache.splice(0, count);
this._unparseAndStore(_cache);
return removed;
};
/** empty/clear the entire cache */
LoggingCache.prototype.empty = function empty() {
localStorage.setItem(this.key, "[]");
return this;
};
/** stringify count number of entries (but do not remove) */
LoggingCache.prototype.stringify = function stringify(count) {
return JSON.stringify(this.get(count));
};
/** outputs entire cache to console */
LoggingCache.prototype.print = function print() {
// popup? (really, carl? a popup?) - easier to copy/paste
console.log(JSON.stringify(this._fetchAndParse(), null, " "));
};
//=============================================================================
export default {
MetricsLogger: MetricsLogger,
LoggingCache: LoggingCache,
};
-354
View File
@@ -1,354 +0,0 @@
// This file isn't really testing anything useful yet, it is just testing
// (or demonstrating) qunit+backbone interactions.
import jQuery from "jquery";
import sinon from "sinon";
import metrics from "utils/metrics-logger";
const MockConsole = function () {
this.lastMessage = null;
["log", "debug", "info", "warn", "error"].forEach((fnName) => {
this[fnName] = function () {
var args = Array.prototype.slice.call(arguments, 0);
this.lastMessage = { level: fnName, args: args };
};
});
return this;
};
describe("metrics-logger tests", function () {
test("logger construction/initializiation defaults", function () {
const logger = new metrics.MetricsLogger({});
expect(logger.consoleLogger).toEqual(null);
expect(logger.options.logLevel).toEqual(metrics.MetricsLogger.NONE);
expect(logger.options.consoleLevel).toEqual(metrics.MetricsLogger.NONE);
expect(logger.options.defaultNamespace).toEqual("Galaxy");
expect(logger.options.clientPrefix).toEqual("client.");
expect(logger.options.postSize).toEqual(1000);
expect(logger.options.maxCacheSize).toEqual(3000);
expect(logger.options.addTime).toEqual(true);
expect(logger.options.postUrl).toEqual("/api/metrics");
expect(logger.options.getPingData).toEqual(undefined);
expect(logger.options.onServerResponse).toEqual(undefined);
expect(logger._postSize).toEqual(1000);
expect(logger.cache.constructor).toEqual(metrics.LoggingCache);
});
test("_parseLevel", function () {
const logger = new metrics.MetricsLogger({});
expect(logger._parseLevel("all")).toEqual(metrics.MetricsLogger.ALL);
expect(logger._parseLevel("debug")).toEqual(metrics.MetricsLogger.DEBUG);
expect(logger._parseLevel("info")).toEqual(metrics.MetricsLogger.INFO);
expect(logger._parseLevel("warn")).toEqual(metrics.MetricsLogger.WARN);
expect(logger._parseLevel("error")).toEqual(metrics.MetricsLogger.ERROR);
expect(logger._parseLevel("metric")).toEqual(metrics.MetricsLogger.METRIC);
expect(logger._parseLevel("none")).toEqual(metrics.MetricsLogger.NONE);
expect(logger._parseLevel(15)).toEqual(15);
//
expect(() => {
logger._parseLevel(undefined);
}).toThrowError(/Unknown log level/);
expect(() => {
logger._parseLevel("nope");
}).toThrowError(/Unknown log level/);
});
test("emit to cache at level", function () {
const logger = new metrics.MetricsLogger({
logLevel: "metric",
});
logger.cache.empty();
expect(logger.options.logLevel).toEqual(metrics.MetricsLogger.METRIC);
logger.emit("metric", "test", [1, 2, { three: 3 }]);
expect(logger.cache.length()).toEqual(1);
const cached = logger.cache.get(1)[0];
expect(cached.level).toEqual(metrics.MetricsLogger.METRIC);
expect(cached.namespace).toEqual("client.test");
expect(cached.args.length).toEqual(3);
expect(cached.args[2].three).toEqual(3);
expect(typeof cached.time === "string").toBeTruthy();
expect(cached.time === new Date(cached.time).toISOString());
});
test("emit to cache below does not cache", function () {
const logger = new metrics.MetricsLogger({
logLevel: "metric",
});
logger.cache.empty();
logger.emit("error", "test", [1, 2, { three: 3 }]);
expect(logger.cache.length()).toBe(0);
});
test("emit to cache (silently) drops non-parsable", function () {
const logger = new metrics.MetricsLogger({
logLevel: "metric",
});
logger.cache.empty();
logger.emit("metric", "test", [{ window: window }]);
expect(logger.cache.length()).toBe(0);
});
// function metricsFromRequestBody(request) {
// // assumes 'metrics' is only entry in requestBody
// return JSON.parse(decodeURIComponent(request.requestBody.replace("metrics=", "")));
// }
// QUnit.test("_postCache success", function (assert) {
// var callback = sinon.spy(),
// logger = new metrics.MetricsLogger({
// logLevel: "metric",
// onServerResponse: function (response) {
// callback();
// },
// });
// logger.cache.empty();
// var server = sinon.fakeServer.create(),
// metricsOnServer;
// server.respondWith("POST", "/api/metrics", function (request) {
// metricsOnServer = metricsFromRequestBody(request);
// //console.debug( 'requestBody:', request.requestBody );
// //console.debug( 'metricsOnServer:', JSON.stringify( metricsOnServer, null, ' ' ) );
// request.respond(
// 200,
// { "Content-Type": "application/json" },
// JSON.stringify({
// fakeResponse: "yes",
// })
// );
// });
// logger.emit("metric", "test", [1, 2, { three: 3 }]);
// logger._postCache();
// server.respond();
// assert.ok(callback.calledOnce, "onServerResponse was called");
// assert.equal(logger.cache.length(), 0, "should have emptied cache (on success)");
// assert.equal(logger._postSize, 1000, "_postSize still at default");
// // metrics were in proper form on server
// assert.equal(metricsOnServer.length, 1);
// var metric = metricsOnServer[0];
// assert.equal(metric.level, metrics.MetricsLogger.METRIC);
// assert.equal(metric.namespace, "client.test");
// assert.equal(metric.args.length, 3);
// assert.equal(metric.args[2].three, 3);
// assert.ok(typeof metric.time === "string");
// assert.ok(metric.time === new Date(metric.time).toISOString());
// server.restore();
// });
// QUnit.test("_postCache failure", function (assert) {
// var callback = sinon.spy(),
// logger = new metrics.MetricsLogger({
// logLevel: "metric",
// onServerResponse: function (response) {
// callback();
// },
// });
// logger.cache.empty();
// var server = sinon.fakeServer.create();
// server.respondWith("POST", "/api/metrics", function (request) {
// request.respond(
// 500,
// { "Content-Type": "application/json" },
// JSON.stringify({
// err_msg: "NoooOPE!",
// })
// );
// });
// logger.emit("metric", "test", [1, 2, { three: 3 }]);
// logger._postCache();
// server.respond();
// //TODO: is the following what we want?
// assert.ok(!callback.calledOnce, "onServerResponse was NOT called");
// assert.equal(logger.cache.length(), 1, "should NOT have emptied cache");
// assert.equal(logger._postSize, logger.options.maxCacheSize, "_postSize changed to max");
// server.restore();
// });
// // ------------------------------------------------------------------------ Emit to console
// QUnit.test("emit to console at level", function (assert) {
// var mockConsole = new MockConsole(),
// logger = new metrics.MetricsLogger({
// consoleLevel: "debug",
// consoleLogger: mockConsole,
// });
// assert.equal(logger.options.consoleLevel, metrics.MetricsLogger.DEBUG);
// assert.equal(logger.consoleLogger.constructor, MockConsole);
// logger.emit("debug", "test", [1, 2, { three: 3 }]);
// assert.equal(logger.cache.length(), 1);
// //console.debug( JSON.stringify( mockConsole.lastMessage ) );
// assert.equal(mockConsole.lastMessage.level, "debug");
// assert.equal(mockConsole.lastMessage.args.length, 4);
// assert.equal(mockConsole.lastMessage.args[0], "test");
// assert.equal(mockConsole.lastMessage.args[3].three, 3);
// });
// QUnit.test("emit to console below does not output", function (assert) {
// var mockConsole = new MockConsole(),
// logger = new metrics.MetricsLogger({
// consoleLevel: "error",
// consoleLogger: mockConsole,
// });
// logger.emit("debug", "test", [1, 2, { three: 3 }]);
// assert.equal(mockConsole.lastMessage, null);
// });
// // ------------------------------------------------------------------------ Shortcuts
// QUnit.test("logger shortcuts emit to default namespace properly", function (assert) {
// var logger = new metrics.MetricsLogger({
// logLevel: "all",
// });
// logger.cache.empty();
// assert.equal(logger.options.logLevel, metrics.MetricsLogger.ALL);
// logger.log(0);
// logger.debug(1);
// logger.info(2);
// logger.warn(3);
// logger.error(4);
// logger.metric(5);
// assert.equal(logger.cache.length(), 6);
// var cached = logger.cache.remove(6),
// entry;
// cached.forEach(function (entry) {
// assert.ok(entry.namespace === logger.options.clientPrefix + logger.options.defaultNamespace);
// assert.ok(jQuery.type(entry.args) === "array");
// assert.ok(typeof entry.time === "string");
// });
// // log is different
// entry = cached[0];
// assert.ok(entry.level === 1);
// assert.ok(entry.args[0] === 0);
// ["debug", "info", "warn", "error", "metric"].forEach(function (level, i) {
// entry = cached[i + 1];
// assert.ok(entry.level === logger._parseLevel(level));
// assert.ok(entry.args[0] === i + 1);
// });
// });
// // ======================================================================== LoggingCache
// QUnit.test("cache construction/initializiation defaults", function (assert) {
// // use empty to prevent tests stepping on one another due to persistence
// var cache = new metrics.LoggingCache({ key: "logs-test" }).empty();
// assert.equal(cache.maxSize, 5000);
// assert.equal(window.localStorage.getItem("logs-test"), "[]");
// });
// QUnit.test("cache construction/initializiation failure", function (assert) {
// ////TODO: doesn't work - readonly
// //window.localStorage = null;
// //console.debug( 'localStorage:', window.localStorage );
// var oldFn = metrics.LoggingCache.prototype._hasStorage;
// metrics.LoggingCache.prototype._hasStorage = function () {
// return false;
// };
// assert.throws(
// function () {
// return new metrics.LoggingCache({ key: "logs-test" });
// },
// /LoggingCache needs localStorage/,
// "lack of localStorage throws error"
// );
// metrics.LoggingCache.prototype._hasStorage = oldFn;
// assert.throws(
// function () {
// return new metrics.LoggingCache();
// },
// /LoggingCache needs key for localStorage/,
// "lack of key throws error"
// );
// });
// QUnit.test("cache construction/initializiation setting max cache size", function (assert) {
// var cache = new metrics.LoggingCache({
// key: "logs-test",
// maxSize: 5,
// }).empty();
// assert.equal(cache.maxSize, 5);
// });
// QUnit.test("cache plays well with no data", function (assert) {
// var cache = new metrics.LoggingCache({ key: "logs-test" }).empty();
// assert.equal(cache.length(), 0);
// var get = cache.get(10);
// assert.ok(jQuery.type(get) === "array" && get.length === 0);
// var remove = cache.remove(10);
// assert.ok(jQuery.type(remove) === "array" && remove.length === 0);
// assert.equal(cache.length(), 0);
// });
test("cache add properly adds and removes data", function () {
const cache = new metrics.LoggingCache({
key: "logs-test",
maxSize: 5,
}).empty();
const entry1 = [{ one: 1 }, "two"];
cache.add(entry1);
expect(cache.length()).toBe(1);
expect(JSON.stringify(cache.get(1)[0])).toBe(JSON.stringify(entry1));
const entry2 = { blah: { one: 1 }, bler: ["three", { two: 2 }] };
cache.add(entry2);
expect(cache.length()).toBe(2);
expect(cache.stringify(2)).toBe("[" + JSON.stringify(entry1) + "," + JSON.stringify(entry2) + "]");
// FIFO
var returned = cache.remove(1);
assert.equal(cache.length(), 1);
assert.ok(jQuery.type(returned) === "array" && returned.length === 1);
var returned0 = returned[0];
assert.ok(jQuery.type(returned0) === "array" && JSON.stringify(returned0) === JSON.stringify(entry1));
});
test("cache past max loses oldest", function () {
const cache = new metrics.LoggingCache({
key: "logs-test",
maxSize: 5,
}).empty();
for (let i = 0; i < 10; i += 1) {
cache.add({ index: i });
}
expect(cache.length()).toBe(5);
const get = cache.get(5);
expect(get[0].index === 5).toBeTruthy();
expect(get[1].index === 6).toBeTruthy();
expect(get[2].index === 7).toBeTruthy();
expect(get[3].index === 8).toBeTruthy();
expect(get[4].index === 9).toBeTruthy();
});
test("cache is properly persistent", function () {
const cache1 = new metrics.LoggingCache({ key: "logs-test" }).empty();
const entry = [{ one: 1 }, "two"];
cache1.add(entry);
expect(cache1.length()).toBe(1);
const cache2 = new metrics.LoggingCache({ key: "logs-test" });
expect(cache2.length()).toBe(1); // , "old key gets previously stored");
expect(JSON.stringify(cache2.get(1)[0])).toBe(JSON.stringify(entry));
const cache3 = new metrics.LoggingCache({ key: "logs-bler" });
expect(cache3.length()).toBe(0); //, "new key causes new storage");
});
});
-6
View File
@@ -18,12 +18,6 @@ export function setupTestGalaxy(galaxyOptions_ = null) {
galaxy.currHistoryPanel = {
model: new Backbone.Model(),
};
galaxy.emit = {
debug: function () {},
error: function (v) {
window.console.error(v);
},
};
return galaxy;
});
}
-1
View File
@@ -4,6 +4,5 @@
import "./tests/galaxy_app_base_tests";
import "./tests/jquery_plugin_tests";
import "./tests/metrics_logger_tests";
import "./tests/modal_tests";
import "./tests/ui_tests";
@@ -35,14 +35,6 @@ QUnit.test("App base extends from Backbone.Events", function (assert) {
});
});
QUnit.test("App base has logging methods from utils/add-logging.js", function (assert) {
var app = getGalaxyInstance();
["debug", "info", "warn", "error", "metric"].forEach(function (fn) {
assert.ok(typeof app[fn] === "function");
});
assert.ok(app._logNamespace === "GalaxyApp");
});
// We no longer want this behavior
QUnit.test("App base will patch in attributes from existing Galaxy objects", function (assert) {
var existingApp = getGalaxyInstance();