import { reflect, string } from '@amp-metrics/mt-metricskit-utils-private'; /* * src/utils.js * mt-client-logger-core * * Copyright © 2016-2017 Apple Inc. All rights reserved. * */ function FlagSymbol(key) { this.key = key; } FlagSymbol.prototype.toString = function toString() { return this.key; }; var utils = { /** ************************************ PUBLIC METHODS/IVARS ************************************ */ /** Special flag classes that can be passed as arguments to logger methods in order to dictate logging behavior * Use class instances to guarantee that flag arguments are unique, and use constructor names for O(1) lookup */ flagArguments: { /** * When logging, if any of the arguments is an instance of this class, the log output will include a call stack trace. * @example usage: logger.warn('danger!', logger.INCLUDE_CALL_STACK); */ INCLUDE_CALL_STACK: new FlagSymbol('INCLUDE_CALL_STACK'), /** * When logging, if any of the arguments is an instance of this class, the remaining arguments will be mirrored to the logging server * @example usage: logger.info('some message', logger.MIRROR_TO_SERVER); */ MIRROR_TO_SERVER: new FlagSymbol('MIRROR_TO_SERVER'), /** * When logging, if any of the arguments is an instance of this class, the client (console) output will be suppressed * This would typically be used when callers want to log an event to the server without printing it * @example usage: logger.debug(someDiagnosticsInfoObject, logger.MIRROR_TO_SERVER, logger.SUPPRESS_CLIENT_OUTPUT); */ SUPPRESS_CLIENT_OUTPUT: new FlagSymbol('SUPPRESS_CLIENT_OUTPUT') }, /** * Allows replacement of one or more of this class' functions * Any method on the passed-in object which matches a method that this class has will be called instead of the built-in class method. * To replace *all* methods of his class, simply have your delegate implement all the methods of this class * Your delegate can be a true object instance, an anonymous object, or a class object. * Your delegate is free to have as many additional non-matching methods as it likes (these methods will not be copied to the target object). * It can even act as a delegate for multiple MetricsKit objects, though that is not recommended. * * "setDelegate()" may be called repeatedly, with the functions in the most-recently set delegates replacing any functions matching those in the earlier delegates, as well as any as-yet unreplaced functions. * This allows callers to replace some number of methods that need custom implementations. * If, for example, a client wants to use the standard logger implementation with the exception of, say, the "debug" method, they can * call "setDelegate()" with their own delegate containing only a single method of "debug" as the delegate, which would leave all the other methods intact. * * NOTE: The delegate function will have a property called origFunction representing the original function that it replaced. * This allows the delegate to, essentially, call "super" before or after it does some work. * If a replaced method is overridden again with a subsequent "setDelegate()" call, the "origFunction" property will be the previous delegate's function. * @example: * To override one or more methods, in place: * logger.setDelegate({ debug: console.debug }); * To override one or more methods with a separate object: * logger.setDelegate(customLoggerDelegate); * (where "customLoggerDelegate" might be defined elsewhere as, e.g.: * var customLoggerDelegate = { debug: function(msg) { document.getElementById('debugMsg').innerHTML = msg; }, * serverUrl: function() { return 'https://custom-log-server.apple.com'; } }; * To override one or more methods with an instantiated object from a class definition: * eventRecorder.setDelegate(new CustomLoggerDelegate()); * (where "CustomLoggerDelegate" might be defined elsewhere as, e.g.: * function CustomLoggerDelegate() { * } * CustomLoggerDelegate.prototype.debug = function debug(msg) { * document.getElementById('debugMsg').innerHTML = msg; * }; * CustomLoggerDelegate.prototype.serverUrl = function serverUrl() { * return 'https://custom-log-server.apple.com'; * }; * To override one or more methods with a class object (with "static" methods): * eventRecorder.setDelegate(CustomLoggerDelegate); * (where "CustomLoggerDelegate" might be defined elsewhere as, e.g.: * function CustomLoggerDelegate() { * } * CustomLoggerDelegate.debug = function debug(msg) { * document.getElementById('debugMsg').innerHTML = msg; * }; * CustomLoggerDelegate.serverUrl = function serverUrl() { * return 'https://custom-log-server.apple.com'; * }; * @param {Object} delegate Object or Class with delegate method(s) to be called instead of default (built-in) methods. * @returns {Boolean} true if one or more methods on the delegate object match one or more methods on the default object, * otherwise returns false. */ setDelegate: function setDelegate(delegate) { return reflect.attachDelegate(this, delegate); }, /** * If the log level allows, logs/throws an error to the console and mirrors the log event to the server * @param {Logger} logger * @param {String} methodName * @param {Array-like Object} origArguments */ execute: function execute(logger, methodName, origArguments) { var methodLevel = logger.levelStringToIntMap[methodName]; if (logger.level() !== logger.NONE && logger.level() <= methodLevel) { var argumentsArray = Array.prototype.slice.call(origArguments); var logArguments = utils.nonFlagLogArguments(argumentsArray); var logOptions = utils.logOptions(logger, methodLevel, argumentsArray); var callstack = logOptions.includeCallStack ? new Error().stack : null; var enrichedLogArguments = callstack ? logArguments.concat('\n' + callstack) : logArguments; // add newline for nicer output // so testing harness can verify logging done within tested functions: logger[methodName]._lastLog = enrichedLogArguments; if (logOptions.mirrorToServer) { utils.sendToServer(logger, methodName, logArguments, callstack); } if (logOptions.throwInsteadOfPrint) { throw new Error(logArguments.toString()); } else if (!logOptions.suppressClientOutput) { if (console[methodName]) { console[methodName].apply(console, enrichedLogArguments); } else { // fallback to console.log - node does not have console.debug console.log.apply(console, enrichedLogArguments); } } } }, /** * Indicates whether an item is a specific flag object that dictates logging behavior * @param {*} argument * @return {Boolean} */ isFlagObject: function isFlagObject(argument) { return argument && argument === utils.flagArguments[argument.toString()]; }, /** * Creates a new array without specific arguments that dictate logging behavior (and are not intended to be logged) * @param {Array} argumentsArray * @return {Array} */ nonFlagLogArguments: function nonFlagLogArguments(argumentsArray) { return argumentsArray.filter(function (argument) { return !utils.isFlagObject(argument); }); }, /** * Inspects an array of arguments for specific flag objects that dictate log behavior and returns an object representing the intended behavior * By checking for all of the various flags in one pass, we avoid looping over the arguments array more than necessary * @param {Logger} logger * @param {Int} methodLevel * @param {Array} argumentsArray * @return {Object} */ logOptions: function logOptions(logger, methodLevel, argumentsArray) { var logOptions = {}; var optionName; argumentsArray.forEach(function (argument) { if (utils.isFlagObject(argument)) { optionName = string.snakeCaseToCamelCase(argument.toString()); logOptions[optionName] = true; } }); if ( reflect.isFunction(logger.mirrorToServerLevel) && logger.mirrorToServerLevel() !== logger.NONE && logger.mirrorToServerLevel() <= methodLevel ) { logOptions.mirrorToServer = true; } if (logger.throwLevel() !== logger.NONE && logger.throwLevel() <= methodLevel) { logOptions.throwInsteadOfPrint = true; } return logOptions; }, /** * Sends a log event to the server immediately without checking resolution * TODO: refactor to use eventRecorder once it is a standalone package * NO DEFAULT IMPLEMENTATION... THIS METHOD MUST BE REPLACED * @param {Logger} logger * @param {String} level * @param {Array} logArguments * @param {String} (optional) callstack * @return {String} the JSON-stringified event that was sent to the server * @overridable */ sendToServer: function sendToServer(logger, level, logArguments, callstack) {} }; /* * src/logger.js * mt-client-logger-core * * Copyright © 2016-2017 Apple Inc. All rights reserved. * */ /** ************************************ PRIVATE METHODS/IVARS ************************************ */ // Define log levels separately to expose this constant. // TODO clean constants up when consolidate. var LOG_LEVELS = { NONE: 0, DEBUG: 1, INFO: 2, WARN: 3, ERROR: 4 }; var LOGGER_LEVELS = { MIN_LEVEL: LOG_LEVELS.NONE, MAX_LEVEL: LOG_LEVELS.ERROR, levelIntToStringMap: { 0: 'none', 1: 'debug', 2: 'info', 3: 'warn', 4: 'error' }, levelStringToIntMap: { none: 0, debug: 1, info: 2, warn: 3, error: 4 } }; reflect.extend(LOGGER_LEVELS, LOG_LEVELS); /** Global properties */ var LOGGER_PROPERTIES = { loggerName: 'defaultLogger', level: LOGGER_LEVELS.INFO, throwLevel: LOGGER_LEVELS.NONE }; var _initialized = false; /** A map of logger names to Logger instances */ var _loggers = {}; /** * Provides basic "log4j" type functionality. * The functionality in this class is typically replaced via a delegate. * NOTE: This class has a "secret" field extending each logger function called "_lastLog" which allows us to inspect logged errors from within our test cases of various functionality * to ensure that the correct errors are thrown. * DEFAULT implementation: console logging * DEFAULT logger level: INFO * @see setDelegate * @delegatable * @constructor * @param {String} loggerName */ function Logger(loggerName) { // @private this._loggerName = loggerName; /* These variables are enumerated here for clarity */ // @private this._level; // @private this._throwLevel; // lazily add prototype properties if (!_initialized) { _initialized = true; reflect.extend(Logger.prototype, LOGGER_LEVELS); reflect.extend(Logger.prototype, utils.flagArguments); } } /** * Returns the logger instance that has the name , creating a new one if it doesn't exist * @param {String} loggerName * @return {Logger} */ function loggerNamed(loggerName) { loggerName = loggerName || LOGGER_PROPERTIES.loggerName; var returnLogger = _loggers[loggerName]; if (!returnLogger) { returnLogger = new Logger(loggerName); _loggers[loggerName] = returnLogger; } return returnLogger; } /** * Remove a logger from the cache * @param loggerName */ function removeLogger(loggerName) { if (_loggers) { delete _loggers[loggerName]; } } function resetLoggerCache() { _loggers = {}; } /** Default class property setters and getters */ Logger.level = function level() { return LOGGER_PROPERTIES.level; }; Logger.throwLevel = function throwLevel() { return LOGGER_PROPERTIES.throwLevel; }; // TODO: new PR with this, flesh out and make app-wide with docs // Logger.setDelegate = function setDelegate() { }; // Logger.logCallback = function logCallback() { }; /** ************************************ PUBLIC METHODS/IVARS ************************************ */ /** * Allows replacement of one or more of this class instance's functions * Any method on the passed-in object which matches a method that this class has will be called instead of the built-in class method. * To replace *all* methods of his class, simply have your delegate implement all the methods of this class * Your delegate can be a true object instance, an anonymous object, or a class object. * Your delegate is free to have as many additional non-matching methods as it likes (these methods will not be copied to the target object). * It can even act as a delegate for multiple MetricsKit objects, though that is not recommended. * * "setDelegate()" may be called repeatedly, with the functions in the most-recently set delegates replacing any functions matching those in the earlier delegates, as well as any as-yet unreplaced functions. * This allows callers to replace some number of methods that need custom implementations. * If, for example, a client wants to use the standard logger implementation with the exception of, say, the "debug" method, they can * call "setDelegate()" with their own delegate containing only a single method of "debug" as the delegate, which would leave all the other methods intact. * * NOTE: The delegate function will have a property called origFunction representing the original function that it replaced. * This allows the delegate to, essentially, call "super" before or after it does some work. * If a replaced method is overridden again with a subsequent "setDelegate()" call, the "origFunction" property will be the previous delegate's function. * @example: * To override one or more methods, in place: * logger.setDelegate({ debug: console.debug }); * To override one or more methods with a separate object: * logger.setDelegate(customLoggerDelegate); * (where "customLoggerDelegate" might be defined elsewhere as, e.g.: * var customLoggerDelegate = { debug: function(msg) { document.getElementById('debugMsg').innerHTML = msg; }, * serverUrl: function() { return 'https://custom-log-server.apple.com'; } }; * To override one or more methods with an instantiated object from a class definition: * eventRecorder.setDelegate(new CustomLoggerDelegate()); * (where "CustomLoggerDelegate" might be defined elsewhere as, e.g.: * function CustomLoggerDelegate() { * } * CustomLoggerDelegate.prototype.debug = function debug(msg) { * document.getElementById('debugMsg').innerHTML = msg; * }; * CustomLoggerDelegate.prototype.serverUrl = function serverUrl() { * return 'https://custom-log-server.apple.com'; * }; * To override one or more methods with a class object (with "static" methods): * eventRecorder.setDelegate(CustomLoggerDelegate); * (where "CustomLoggerDelegate" might be defined elsewhere as, e.g.: * function CustomLoggerDelegate() { * } * CustomLoggerDelegate.debug = function debug(msg) { * document.getElementById('debugMsg').innerHTML = msg; * }; * CustomLoggerDelegate.serverUrl = function serverUrl() { * return 'https://custom-log-server.apple.com'; * }; * @param {Object} delegate Object or Class with delegate method(s) to be called instead of default (built-in) methods. * @returns {Boolean} true if one or more methods on the delegate object match one or more methods on the default object, * otherwise returns false. */ Logger.prototype.setDelegate = function setDelegate(delegate) { return reflect.attachDelegate(this, delegate); }; /** * The name of this logger * @returns {String} * @overridable */ Logger.prototype.loggerName = function loggerName() { return this._loggerName; }; /** * Deduces the integer level from either a string or integer * @param {*} level loglevel which may be either a string (e.g. 'debug', 'DEBUG', 'Debug', etc.) or an integer (e.g. 1, 2, 3 or logger.DEBUG, logger.INFO, logger.WARN, etc. * @return {Int} the level as an integer or null if an invalid level argument was passed * @overrideable */ Logger.prototype.levelParameterAsInt = function levelParameterAsInt(level) { var returnLevel = null; var integerLevel; if (reflect.isString(level)) { integerLevel = this.levelStringToIntMap[level.toLowerCase()]; } else if (reflect.isNumber(level)) { integerLevel = level; } if (integerLevel >= this.MIN_LEVEL && integerLevel <= this.MAX_LEVEL) { returnLevel = integerLevel; } return returnLevel; }; /** * Sets the level at which we will log at or above * @param {*} level loglevel which may be either a string (e.g. 'debug', 'DEBUG', 'Debug', etc.) or an integer (e.g. 1, 2, 3 or logger.DEBUG, logger.INFO, logger.WARN, etc. * @overridable */ Logger.prototype.setLevel = function setLevel(level) { var integerLevel = this.levelParameterAsInt(level); if (integerLevel !== null) { this._level = integerLevel; } }; /** * NOTE: This setting should be honored by all delegates. * This setting will cause any emitted log message at or above the specified level to throw an exception with the log message instead of logging to the console. * This is useful during testcase execution when we would expect to have no log output, or perhaps only "info" log output, etc. * @param {*} throwLevel loglevel which may be either a string (e.g. 'debug', 'DEBUG', 'Debug', etc.) or an integer (e.g. 1, 2, 3 or logger.DEBUG, logger.INFO, logger.WARN, etc. */ Logger.prototype.setThrowLevel = function setThrowLevel(throwLevel) { var integerLevel = this.levelParameterAsInt(throwLevel); if (integerLevel !== null) { this._throwLevel = integerLevel; } }; /** * Returns the current logger level as an integer * @overridable */ Logger.prototype.level = function level() { var level = this._level; return reflect.isNumber(level) ? level : Logger.level(); }; /** * Returns the current logger level as a string * @overridable */ Logger.prototype.levelString = function levelString() { return this.levelIntToStringMap[this.level()]; }; /** * Returns the current logger throw level as an integer * @overridable */ Logger.prototype.throwLevel = function throwLevel() { var throwLevel = this._throwLevel; return reflect.isNumber(throwLevel) ? throwLevel : Logger.throwLevel(); }; /** * Emits the log message if log level is set to "debug". * DEFAULT implementation: console.debug() * @param {Object} a list of objects (perhaps a single string) to be stringified and emitted as the log message * @api public * @overridable */ Logger.prototype.debug = function debug() { utils.execute(this, 'debug', arguments); }; /** * Emits the log message if log level is set to "info". * DEFAULT implementation: console.info() * @param {Object} a list of objects (perhaps a single string) to be stringified and emitted as the log message * @api public * @overridable */ Logger.prototype.info = function info() { utils.execute(this, 'info', arguments); }; /** * Emits the log message if log level is set to "warn". * DEFAULT implementation: console.warn() * @param {Object} a list of objects (perhaps a single string) to be stringified and emitted as the log message * @api public * @overridable */ Logger.prototype.warn = function warn() { utils.execute(this, 'warn', arguments); }; /** * Emits the log message if log level is set to "error". * DEFAULT implementation: console.error() * @param {Object} a list of objects (perhaps a single string) to be stringified and emitted as the log message * @api public * @overridable */ Logger.prototype.error = function error() { utils.execute(this, 'error', arguments); }; /** * @param {String} levelString * @return {String} the most recent log event for this level */ Logger.prototype.lastLog = function lastLog(levelString) { return this[levelString] ? this[levelString]._lastLog : null; }; var level = Logger.level; var throwLevel = Logger.throwLevel; /* * mt-client-logger-core/index.js * mt-client-logger-core * * Copyright © 2016-2017 Apple Inc. All rights reserved. * */ export default Logger; export { LOG_LEVELS, level, loggerNamed, removeLogger, resetLoggerCache, throwLevel, utils };