/* Copyright (c) 2013 [Web App Solution, Inc.](mailto:admin@webappsolution.com) FlowMVC is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. FlowMVC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with FlowMVC. If not, see <http://www.gnu.org/licenses/>. */ /** * This is a simple, one-class logger that attempts to do the bare minimum required for logging without a ton * of bells and whistles; simply put, this logger offers console logging as the only target, filtering by context * and leg-level, and a fixed output that's non-configurable. There are many other logging libraries that support this * type of advanced logging support: [log4javascript](http://log4javascript.org/), [log4js-ext](https://code.google.com/p/log4js-ext/) * and so on. * * The logger provides a simple wrapper to the console but with some added benefits like checking for console * availability and parametrized variable substitution in logging messages using array or JSON notation as * the second parameter to the log statement. The output for the logger looks like: * * HH:MM:SS:SSS LEVEL [context or className] - message * * A fully backed example log message might look like: * * 22:07:44:968 DEBUG [FlowMVC.mvc.event.AbstractEvent] - AbstractEvent.Constructor: type = flowMVCEvent * * Variable substitution with tokens is achieved with arrays: * * logger.debug("execute: first = {0}, last = {1}", [first, last]); * * OR with JSON: * * logger.debug("execute: name = {name}, last = {foo.bar}", { first:"john doe", foo: { bar:"foo-bar" } }); */ Ext.define("FlowMVC.logger.Logger", { statics: { /** * @property {Boolean} isEnabled Global flag indicating if logging is enabled. * @static */ isEnabled: true, /** * @property {Object} filters An associative array or hash of all the logging filters. * @static */ filters: null, /** * @property {Boolean} isEnabled Global flag indicating if logging is enabled. * @static */ level: 0, /** * @property {String} LEVEL_LOG A constant. Indicates the "log" logging level. * @static */ LEVEL_ALL: 0, /** * @property {String} LEVEL_LOG A constant. Indicates the "log" logging level. * @static */ LEVEL_LOG: 2, /** * @property {String} LEVEL_DEBUG A constant. Indicates the "debug" logging level. * @static */ LEVEL_DEBUG: 4, /** * @property {String} LEVEL_INFO A constant. Indicates the "info" logging level. * @static */ LEVEL_INFO: 6, /** * @property {String} LEVEL_WARN A constant. Indicates the "warn" logging level. * @static */ LEVEL_WARN: 8, /** * @property {String} LEVEL_ERROR A constant. Indicates the "error" logging level. * @static */ LEVEL_ERROR: 10, /** * @property {String} LEVEL_FATAL A constant. Indicates the "fatal" logging level. * @static */ LEVEL_FATAL: 1000, /** * Creates a logger that outputs the following format: * * 16:11:45:956 DEBUG [CafeTownsend.controller.AuthenticationController] - login: username = a, password = a * * @param {String} context The string name used for the logger. This is often the class name of the object the * logger is used in. * @returns {FlowMVC.logger.Logger} A FlowMVC logger. * @static */ getLogger: function(context) { if(!Ext.isString(context)) { context = Ext.getClassName(context); } if ((context == null) || (context == "undefined") || (context == "")) { context = "Unknown Context"; } return Ext.create("FlowMVC.logger.Logger", context); }, /** * Adds an acceptable filter to the list of log statements that are acceptable. * * @param {String} filter The filter to add. * @static */ addFilter: function(filter) { if(FlowMVC.logger.Logger.filters == null) { FlowMVC.logger.Logger.filters = {}; } FlowMVC.logger.Logger.filters[filter] = filter; }, /** * Removes a filter from the list of log statements that are acceptable. * * @param {String} filter The filter to remove. * @static */ removeFilter: function(filter) { if(FlowMVC.logger.Logger.filters != null) { delete FlowMVC.logger.Logger.filters[filter]; } }, /** * Sets the log level. Only allows logging to the console >= the current level set. * * @param {Number} level The log level. * @static */ setLevel: function(level) { FlowMVC.logger.Logger.level = level; }, /** * Returns an object of the logger as a factory so it can be injected into client objects. The factory is used * so we can use the reference to the instance of the object it's injected into, thus allowing log messages * to take the following format: * * 16:11:45:956 DEBUG [CafeTownsend.controller.AuthenticationController] - login: username = a, password = a * * The use of the singleton property of the returned object ensures that the logger is unique and created * for each injection, again allowing the logger to gain a reference to the instance it's injected into. * * @returns {{fn: Function, singleton: Boolean}} * @static */ getInjectableLogger: function() { return { // The factory function will be passed a single argument: // The object instance that the new object will be injected into // NOTE: the factory function for DeftJS must be named "fn" fn: function(instance) { return FlowMVC.logger.Logger.getLogger(instance); }, singleton: false } } }, /** * @property {String} context String name to be used when logging; typically this is the client object's * fully-qualified name. */ context: null, /** * Constructor. * * @param {String} context The context is a string indicator used when logging with this logger; * often times this is the class name of the client object using this logger. */ constructor: function(context) { this.context = context; }, /** * Provides logging with a level of "log". */ log: function() { this.internalLog(FlowMVC.logger.Logger.LEVEL_LOG, arguments); }, /** * Provides logging with a level of "debug". */ debug: function() { this.internalLog(FlowMVC.logger.Logger.LEVEL_DEBUG, arguments); }, /** * Provides logging with a level of "info". */ info: function() { this.internalLog(FlowMVC.logger.Logger.LEVEL_INFO, arguments); }, /** * Provides logging with a level of "warn". */ warn: function() { this.internalLog(FlowMVC.logger.Logger.LEVEL_WARN, arguments); }, /** * Provides logging with a level of "error". */ error: function() { this.internalLog(FlowMVC.logger.Logger.LEVEL_ERROR, arguments); }, /** * Provides logging with a level of "fatal". */ fatal: function() { this.internalLog(FlowMVC.logger.Logger.LEVEL_FATAL, arguments); }, /** * Provides log grouping. */ group: function() { try { if(window.console && console.group && Ext.isFunction(console.group)) { console.group(msg); } } catch (e) { } }, /** * Ends log grouping. */ groupEnd: function() { try { if(window.console && console.groupEnd && Ext.isFunction(console.groupEnd)) { console.groupEnd(msg); } } catch (e) { } }, /** * Creates a print-friendly timestamp in the form of 16:11:45:956 for logging purposes. * * @return {String} A timestamp in the form of 16:11:45:956. */ getTimestamp: function() { var date = new Date(); var hours = date.getHours(); var minutes = date.getMinutes(); var seconds = date.getSeconds(); var milliseconds = date.getMilliseconds(); if (hours < 10) { hours = "0" + hours; } if (minutes < 10) { minutes = "0" + minutes; } if (seconds < 10) { seconds = "0" + seconds; } if (milliseconds < 10) { milliseconds = "00" + milliseconds; } else if (milliseconds < 100) { milliseconds = "0" + milliseconds; } return hours + ":" + minutes + ":" + seconds + ":" + milliseconds; }, /** * A simple method that returns the log level as a printable string. * * @param {Number} level The log level. * @returns {String} The printable log level message. */ getPrintableLogMessage: function(level) { switch(level) { case FlowMVC.logger.Logger.LEVEL_DEBUG: return "[DEBUG]"; case FlowMVC.logger.Logger.LEVEL_INFO: return "[INFO] "; case FlowMVC.logger.Logger.LEVEL_WARN: return "[WARN] "; case FlowMVC.logger.Logger.LEVEL_ERROR: return "[ERROR]"; case FlowMVC.logger.Logger.LEVEL_FATAL: return "[FATAL]"; case FlowMVC.logger.Logger.LEVEL_LOG: default: return "[LOG] "; } }, /** * Creates a print-friendly context in the form of * 16:11:45:956 DEBUG [CafeTownsend.controller.AuthenticationController] - login: username = {a}, password = {b} * for logging purposes, where {a} and {b} are tokenized parameters passed into the logging method. * * @return {String} A context in the form of 16:11:45:956 DEBUG [CafeTownsend.controller.AuthenticationController] * - login: username = {a}, password = {b}. */ getPrintFriendlyLogStatement: function(level, msg) { msg = (msg == "undefined") || (msg == null) ? "" : msg; return this.getTimestamp() + " " + this.getPrintableLogMessage(level) + " " + this.context + " - " + msg; }, /** * Determines if the token value object (second parameter in the original log function) is an array or object * and attempts to perform token substitution based on the valuers in the array or JSON object. Tokens in the * message either looks like {0}, {1}, ... {n} for array substitution or {user.username}, {firstName} for * JSON substitution. * * @return {String} The final message with tokens replaced with values. */ replaceTokens: function(args, msg) { var tokenValues = args[1]; // do substitution of tokens with the passed in array of values if (Ext.isArray(tokenValues)) { var len = tokenValues.length; for (var i = 0; i < len; i++) { msg = msg.replace(new RegExp("\\{" + i + "\\}", "g"), tokenValues[i]); } // do substitution of tokens using the passed in JSON object } else if (Ext.isObject(tokenValues)) { var tokens = msg.match(/\{(.*?)\}/g); if(Ext.isArray(tokens)) { var value = ""; var len = tokens.length; // loop through all the tokens and repalace them with values from the JSON object for (var j = 0; j < len; j++) { // replace the brackets for "{user.username}" becomes "user.username" var token = tokens[j].replace(/\{(.*?)\}/g,"$1"); // create an array of all the tokens var properties = token.split("."); // nested function to dig down into a JSON object and grab the actual value of the nested property // allows for the retrieval of a json object like foo.bar.count. getNestedValue = function(tokenValues, properties) { var property = ""; var len = properties.length; for (var j = 0; j < len; j++) { property = properties[j]; tokenValues = tokenValues[property]; } return tokenValues; } try { value = getNestedValue(tokenValues, properties); } catch(e) { value = ""; }; msg = msg.replace(new RegExp(tokens[j]), value); } } } return msg; }, /** * Determines if the log message contains a context that matches one of the acceptable log filters. * * @param {String} msg The entire log message to search for a matching filter. * @returns {Boolean} A flag indicating if the message contains an acceptable log filter. */ isFilterEnabled: function(msg) { var filterRef = FlowMVC.logger.Logger.filters; if(filterRef == null) { filterRef = {}; filterRef["*"] = "*"; } if(filterRef["*"] != null) { return true; } for(var filter in filterRef) { var lastChar = filter.charAt(filter.length-1); if(lastChar == "*") { filter = filter.slice(0, -1); } if(msg.indexOf(filter) != -1) { return true; } } return false; }, /** * @private Determines the log level and logs to the console accordingly. Can take tokenized log messages and substitute * values passed into the logging method. * * @param {String} level The logging level. * @param {Array} args An array of logging arguments. The first argument is typically the message and the following * can be used in log message token substitution. */ internalLog: function(level, args) { // do not log anything if logging is not enabled if (!FlowMVC.logger.Logger.isEnabled) { return; } // determine if the level requested is greater or equal to the current log level if(level < FlowMVC.logger.Logger.level) { return; } // get the console print friendly message var msg = this.getPrintFriendlyLogStatement(level, args[0]); // filter out the acceptable logging statements so it only shows contexts that exist in the filter list if(!this.isFilterEnabled(msg)) { return; } // determine if the message has parametrized tokens if(args && (args.length >= 2)) { msg = this.replaceTokens(args, msg); } // determine the log level and log to the console accordingly switch (level) { case FlowMVC.logger.Logger.LEVEL_INFO: this.logToConsole("info", msg); break; case FlowMVC.logger.Logger.LEVEL_WARN: this.logToConsole("warn", msg); break; case FlowMVC.logger.Logger.LEVEL_ERROR: case FlowMVC.logger.Logger.LEVEL_FATAL: this.logToConsole("error", msg); break; case FlowMVC.logger.Logger.LEVEL_LOG: case FlowMVC.logger.Logger.LEVEL_DEBUG: default: this.logToConsole("debug", msg); break; } }, /** * @private Internal method that determines if the console logging method is available -- if so, print to the console. * * @param {Function} method The request console logging method. * @param {String} msg The message to log to the console. */ logToConsole: function(method, msg) { try { if(this.isConsoleMethodAvailable(method)) { console[method](msg); } } catch (e) { } }, /** * @private Determines if the requested console logging method is available, since it is not with IE. * * @param {Function} method The request console logging method. * @returns {Boolean} Indicates if the console logging method is available. */ isConsoleMethodAvailable: function(method) { return window.console && console[method] && Ext.isFunction(console[method]); } });