// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== sc_require('ext/function'); // .......................................................... // CONSTANTS // // Implementation note: We use two spaces after four-letter prefixes and one // after five-letter prefixes so things align in monospaced consoles. /** If {@link SC.Logger.format} is true, this delimiter will be put between arguments. @type String */ SC.LOGGER_LOG_DELIMITER = ", "; /** If {@link SC.Logger.error} falls back onto {@link SC.Logger.log}, this will be prepended to the output. @type String */ SC.LOGGER_LOG_ERROR = "ERROR: "; /** If {@link SC.Logger.info} falls back onto {@link SC.Logger.log}, this will be prepended to the output. @type String */ SC.LOGGER_LOG_INFO = "INFO: "; /** If {@link SC.Logger.warn} falls back onto {@link SC.Logger.log}, this will be prepended to the output. @type String */ SC.LOGGER_LOG_WARN = "WARN: "; /** If {@link SC.Logger.debug} falls back onto {@link SC.Logger.log}, this will be prepended to the output. @type String */ SC.LOGGER_LOG_DEBUG = "DEBUG: "; /** If {@link SC.Logger.group} falls back onto {@link SC.Logger.log}, this will be prepended to the output. @type String */ SC.LOGGER_LOG_GROUP_HEADER = "** %@"; // The variable is the group title /** If the reporter does not support group(), then we’ll add our own indentation to our output. This constant represents one level of indentation. @type String */ SC.LOGGER_LOG_GROUP_INDENTATION = " "; /** When reporting recorded log messages, the timestamp is included with this prefix. @type String */ SC.LOGGER_RECORDED_LOG_TIMESTAMP_PREFIX = "%@: "; SC.LOGGER_LEVEL_DEBUG = 'debug'; SC.LOGGER_LEVEL_INFO = 'info'; SC.LOGGER_LEVEL_WARN = 'warn'; SC.LOGGER_LEVEL_ERROR = 'error'; SC.LOGGER_LEVEL_NONE = 'none'; /** @class Object to allow for safe logging actions, such as using the browser console. In addition to being output to the console, logs can be optionally recorded in memory, to be accessed by your application as appropriate. This class also adds in the concept of a “current log level”, which allows your application to potentially determine a subset of logging messages to output and/or record. The order of levels is: - debug SC.LOGGER_LEVEL_DEBUG - info SC.LOGGER_LEVEL_INFO - warn SC.LOGGER_LEVEL_WARN - error SC.LOGGER_LEVEL_ERROR All messages at the level or “above” will be output/recorded. So, for example, if you set the level to 'info', all 'info', 'warn', and 'error' messages will be output/recorded, but no 'debug' messages will be. Also, there are two separate log levels: one for output, and one for recording. You may wish to only output, say, 'warn' and above, but record everything from 'debug' on up. (You can also limit the number log messages to record.) This mechanism allows your application to avoid needless output (which has a non-zero cost in many browsers) in the general case, but turning up the log level when necessary for debugging. Note that there can still be a performance cost for preparing log messages (calling {@link String.fmt}, etc.), so it’s still a good idea to be selective about what log messages are output even to 'debug', especially in hot code. Similarly, you should be aware that if you wish to log objects without stringification — using the {@link SC.Logger.debugWithoutFmt} variants — and you enable recording, the “recorded messages” array will hold onto a reference to the arguments, potentially increasing the amount of memory used. As a convenience, this class also adds some shorthand methods to SC: - SC.debug() ==> SC.Logger.debug() - SC.info() ==> SC.Logger.info() - SC.warn() ==> SC.Logger.warn() - SC.error() ==> SC.Logger.error() …although note that no shorthand versions exist for the less-common functions, such as defining groups. The FireFox plugin Firebug was used as a function reference. Please see [Firebug Logging Reference](http://getfirebug.com/logging.html) for further information. @author Colin Campbell @author Benedikt Böhm @author William Kakes @extends SC.Object @since SproutCore 1.0 @see Firebug Logging Reference */ SC.Logger = SC.Object.create( /** @scope SC.Logger.prototype */{ // .......................................................... // PROPERTIES // /** An optional prefix that will be prepended to all log messages, but not any group titles. @type String */ messagePrefix: null, /** An optional prefix that will be prepended to all log messages that are output to the browser console, but not those that are recorded. If you specify both this and a 'messagePrefix', both will be output, and only the 'messagePrefix' will be recorded. @type String */ outputMessagePrefix: null, /** An optional prefix that will be prepended to all log messages that are recorded, but not those that are output to the browser console. If you specify both this and a 'messagePrefix', both will be recorded, and only the 'messagePrefix' will be output to the browser console. @type String */ recordedMessagePrefix: null, /** The current log level determining what is output to the reporter object (usually your browser’s console). Valid values are: - SC.LOGGER_LEVEL_DEBUG - SC.LOGGER_LEVEL_INFO - SC.LOGGER_LEVEL_WARN - SC.LOGGER_LEVEL_ERROR - SC.LOGGER_LEVEL_NONE If you do not specify this value, it will default to SC.LOGGER_LEVEL_DEBUG when running in development mode and SC.LOGGER_LEVEL_INFO when running in production mode. @property: {Constant} */ logOutputLevel: null, // If null, set appropriately during init() /** The current log level determining what is recorded to the 'recordedLogMessages' buffer. Valid values are the same as with 'logOutputLevel': - SC.LOGGER_LEVEL_DEBUG - SC.LOGGER_LEVEL_INFO - SC.LOGGER_LEVEL_WARN - SC.LOGGER_LEVEL_ERROR - SC.LOGGER_LEVEL_NONE If you do not specify this value, it will default to SC.LOGGER_LEVEL_NONE. @type Constant */ logRecordingLevel: SC.LOGGER_LEVEL_NONE, /** All recorded log messages. You generally should not need to interact with this array, as most commonly-used functionality can be achieved via the {@link SC.Logger.outputRecordedLogMessages} and {@link SC.Logger.stringifyRecordedLogMessages} methods. This array will be lazily created when the first message is recorded. Format: For efficiency, each entry in the array is a simple hash rather than a full SC.Object instance. Furthermore, to minimize memory usage, niceties like “type of entry: message” are avoided; if you need to parse this structure, you can determine which type of entry you’re looking at by checking for the 'message' and 'indentation' fields.
    Log entry:
    {
      type:               {Constant}     (SC.LOGGER_LEVEL_DEBUG, etc.)
      message:            {String | Boolean}
      originalArguments:  {Arguments}    // optional
      timestamp:          {Date}
    }

    Group entry (either beginning or end of):
    {
      type:         {Constant}     SC.LOGGER_LEVEL_DEBUG, etc.
      indentation:  {Number}       The value is the new group indentation level
      beginGroup:   {Boolean}      Whether this entry is the beginning of a new group (as opposed to the end)
      title:        {String}       Optional for new groups, and never present for end-of-group
      timestamp:    {Date}
    }
@type Array */ recordedLogMessages: null, /** If the recording level is set such that messages will be recorded, this is the maximum number of messages that will be saved in the 'recordedLogMessages' array. Any further recorded messages will push older messages out of the array, so the most recent messages will be saved. @type Number */ recordedLogMessagesMaximumLength: 500, /** If the recording level is set such that messages will be recorded, this is the minimum number of messages that will be saved whenever the recordings are pruned. (They are pruned whenever you hit the maximum length, as specified via the 'recordedLogMessagesMaximumLength' property. This mechanism avoids thrashing the array for each log message once the maximum is reached.) When pruning, the most recent messages will be saved. @type Number */ recordedLogMessagesPruningMinimumLength: 100, /** Whether or not to enable debug logging. This property exists for backwards compatibility with previous versions of SC.Logger. In newer code, you should instead set the appropriate output/recording log levels. If this property is set to YES, it will set 'logOutputLevel' to SC.LOGGER_LEVEL_DEBUG. Otherwise, it will have no effect. @deprecated Set the log level instead. @property: {Boolean} */ debugEnabled: NO, /** Computed property that checks for the existence of the reporter object. @type Boolean */ exists: function() { return !SC.none(this.get('reporter')); }.property('reporter').cacheable(), /** If console.log does not exist, SC.Logger will use window.alert instead when {@link SC.Logger.log} is invoked. Note that this property has no effect for messages initiated via the debug/info/warn/error methods, on the assumption that it is better to simply utilize the message recording mechanism than put up a bunch of alerts when there is no browser console. @type Boolean */ fallBackOnAlert: NO, /** The reporter is the object which implements the actual logging functions. @default The browser’s console @type Object */ reporter: console, // .......................................................... // METHODS // /** Logs a debug message to the console and potentially to the recorded array, provided the respective log levels are set appropriately. The first argument must be a string, and if there are any additional arguments, it is assumed to be a format string. Thus, you can (and should) use it like: SC.Logger.debug("%@: My debug message", this); // good …and not: SC.Logger.debug("%@: My debug message".fmt(this)); // bad The former method can be more efficient because if the log levels are set in such a way that the debug() invocation will be ignored, then the String.fmt() call will never actually be performed. @param {String} A message or a format string @param {…} (optional) Other arguments to pass to String.fmt() when using a format string */ debug: function(message, optionalFormatArgs) { // Implementation note: To avoid having to put the SC.debug() shorthand // variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleMessage(SC.LOGGER_LEVEL_DEBUG, YES, message, arguments); }, /** Logs a debug message to the console and potentially to the recorded array, provided the respective log levels are set appropriately. Unlike simply debug(), this method does not try to apply String.fmt() to the arguments, and instead passes them directly to the reporter (and stringifies them if recording). This can be useful if the browser formats a type in a manner more useful to you than you can achieve with String.fmt(). @param {String|Array|Function|Object} */ debugWithoutFmt: function() { this._handleMessage(SC.LOGGER_LEVEL_DEBUG, NO, null, arguments); }, /** Begins a new group in the console and/or in the recorded array provided the respective log levels are set to output/record 'debug' messages. Every message after this call (at any log level) will be indented for readability until a matching {@link SC.Logger.debugGroupEnd} is invoked, and you can create as many levels as you want. Assuming you are using 'debug' messages elsewhere, it is preferable to group them using this method over simply {@link SC.Logger.group} — the log levels could be set such that the 'debug' messages are never seen, and you wouldn’t want an empty/needless group! You can optionally provide a title for the group. If there are any additional arguments, the first argument is assumed to be a format string. Thus, you can (and should) use it like: SC.Logger.debugGroup("%@: My debug group", this); // good …and not: SC.Logger.debugGroup("%@: My debug group".fmt(this)); // bad The former method can be more efficient because if the log levels are set in such a way that the debug() invocation will be ignored, then the String.fmt() call will never actually be performed. @param {String} (optional) A title or format string to display above the group @param {…} (optional) Other arguments to pass to String.fmt() when using a format string as the title */ debugGroup: function(message, optionalFormatArgs) { // Implementation note: To avoid having to put the SC.debugGroup() // shorthand variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleGroup(SC.LOGGER_LEVEL_DEBUG, message, arguments); }, /** Ends a group initiated with {@link SC.Logger.debugGroup}, provided the respective output/recording log levels are set appropriately. @see SC.Logger.debugGroup */ debugGroupEnd: function() { // Implementation note: To avoid having to put the SC.debugGroupEnd() // shorthand variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleGroupEnd(SC.LOGGER_LEVEL_DEBUG); }, /** Logs an informational message to the console and potentially to the recorded array, provided the respective log levels are set appropriately. The first argument must be a string, and if there are any additional arguments, it is assumed to be a format string. Thus, you can (and should) use it like: SC.Logger.info("%@: My info message", this); // good …and not: SC.Logger.info("%@: My info message".fmt(this)); // bad The former method can be more efficient because if the log levels are set in such a way that the info() invocation will be ignored, then the String.fmt() call will never actually be performed. @param {String} A message or a format string @param {…} (optional) Other arguments to pass to String.fmt() when using a format string */ info: function(message, optionalFormatArgs) { // Implementation note: To avoid having to put the SC.info() shorthand // variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleMessage(SC.LOGGER_LEVEL_INFO, YES, message, arguments); }, /** Logs an information message to the console and potentially to the recorded array, provided the respective log levels are set appropriately. Unlike simply info(), this method does not try to apply String.fmt() to the arguments, and instead passes them directly to the reporter (and stringifies them if recording). This can be useful if the browser formats a type in a manner more useful to you than you can achieve with String.fmt(). @param {String|Array|Function|Object} */ infoWithoutFmt: function() { this._handleMessage(SC.LOGGER_LEVEL_INFO, NO, null, arguments); }, /** Begins a new group in the console and/or in the recorded array provided the respective log levels are set to output/record 'info' messages. Every message after this call (at any log level) will be indented for readability until a matching {@link SC.Logger.infoGroupEnd} is invoked, and you can create as many levels as you want. Assuming you are using 'info' messages elsewhere, it is preferable to group them using this method over simply {@link SC.Logger.group} — the log levels could be set such that the 'info' messages are never seen, and you wouldn’t want an empty/needless group! You can optionally provide a title for the group. If there are any additional arguments, the first argument is assumed to be a format string. Thus, you can (and should) use it like: SC.Logger.infoGroup("%@: My info group", this); // good …and not: SC.Logger.infoGroup("%@: My info group".fmt(this)); // bad The former method can be more efficient because if the log levels are set in such a way that the info() invocation will be ignored, then the String.fmt() call will never actually be performed. @param {String} (optional) A title or format string to display above the group @param {…} (optional) Other arguments to pass to String.fmt() when using a format string as the title */ infoGroup: function(message, optionalFormatArgs) { // Implementation note: To avoid having to put the SC.infoGroup() // shorthand variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleGroup(SC.LOGGER_LEVEL_INFO, message, arguments); }, /** Ends a group initiated with {@link SC.Logger.infoGroup}, provided the respective output/recording log levels are set appropriately. @see SC.Logger.infoGroup */ infoGroupEnd: function() { // Implementation note: To avoid having to put the SC.infoGroupEnd() // shorthand variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleGroupEnd(SC.LOGGER_LEVEL_INFO); }, /** Logs a warning message to the console and potentially to the recorded array, provided the respective log levels are set appropriately. The first argument must be a string, and if there are any additional arguments, it is assumed to be a format string. Thus, you can (and should) use it like: SC.Logger.warn("%@: My warning message", this); // good …and not: SC.Logger.warn("%@: My warning message".fmt(this)); // bad The former method can be more efficient because if the log levels are set in such a way that the warn() invocation will be ignored, then the String.fmt() call will never actually be performed. @param {String} A message or a format string @param {…} (optional) Other arguments to pass to String.fmt() when using a format string */ warn: function(message, optionalFormatArgs) { // Implementation note: To avoid having to put the SC.warn() shorthand // variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleMessage(SC.LOGGER_LEVEL_WARN, YES, message, arguments); }, /** Logs a warning message to the console and potentially to the recorded array, provided the respective log levels are set appropriately. Unlike simply warn(), this method does not try to apply String.fmt() to the arguments, and instead passes them directly to the reporter (and stringifies them if recording). This can be useful if the browser formats a type in a manner more useful to you than you can achieve with String.fmt(). @param {String|Array|Function|Object} */ warnWithoutFmt: function() { this._handleMessage(SC.LOGGER_LEVEL_WARN, NO, null, arguments); }, /** Begins a new group in the console and/or in the recorded array provided the respective log levels are set to output/record 'warn' messages. Every message after this call (at any log level) will be indented for readability until a matching {@link SC.Logger.warnGroupEnd} is invoked, and you can create as many levels as you want. Assuming you are using 'warn' messages elsewhere, it is preferable to group them using this method over simply {@link SC.Logger.group} — the log levels could be set such that the 'warn' messages are never seen, and you wouldn’t want an empty/needless group! You can optionally provide a title for the group. If there are any additional arguments, the first argument is assumed to be a format string. Thus, you can (and should) use it like: SC.Logger.warnGroup("%@: My warn group", this); // good …and not: SC.Logger.warnGroup("%@: My warn group".fmt(this)); // bad The former method can be more efficient because if the log levels are set in such a way that the warn() invocation will be ignored, then the String.fmt() call will never actually be performed. @param {String} (optional) A title or format string to display above the group @param {…} (optional) Other arguments to pass to String.fmt() when using a format string as the title */ warnGroup: function(message, optionalFormatArgs) { // Implementation note: To avoid having to put the SC.warnGroup() // shorthand variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleGroup(SC.LOGGER_LEVEL_WARN, message, arguments); }, /** Ends a group initiated with {@link SC.Logger.warnGroup}, provided the respective output/recording log levels are set appropriately. @see SC.Logger.warnGroup */ warnGroupEnd: function() { // Implementation note: To avoid having to put the SC.warnGroupEnd() // shorthand variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleGroupEnd(SC.LOGGER_LEVEL_WARN); }, /** Logs an error message to the console and potentially to the recorded array, provided the respective log levels are set appropriately. The first argument must be a string, and if there are any additional arguments, it is assumed to be a format string. Thus, you can (and should) use it like: SC.Logger.error("%@: My error message", this); // good …and not: SC.Logger.warn("%@: My error message".fmt(this)); // bad The former method can be more efficient because if the log levels are set in such a way that the warn() invocation will be ignored, then the String.fmt() call will never actually be performed. @param {String} A message or a format string @param {…} (optional) Other arguments to pass to String.fmt() when using a format string */ error: function(message, optionalFormatArgs) { // Implementation note: To avoid having to put the SC.error() shorthand // variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleMessage(SC.LOGGER_LEVEL_ERROR, YES, message, arguments); }, /** Logs an error message to the console and potentially to the recorded array, provided the respective log levels are set appropriately. Unlike simply error(), this method does not try to apply String.fmt() to the arguments, and instead passes them directly to the reporter (and stringifies them if recording). This can be useful if the browser formats a type in a manner more useful to you than you can achieve with String.fmt(). @param {String|Array|Function|Object} */ errorWithoutFmt: function() { this._handleMessage(SC.LOGGER_LEVEL_ERROR, NO, null, arguments); }, /** Begins a new group in the console and/or in the recorded array provided the respective log levels are set to output/record 'error' messages. Every message after this call (at any log level) will be indented for readability until a matching {@link SC.Logger.errorGroupEnd} is invoked, and you can create as many levels as you want. Assuming you are using 'error' messages elsewhere, it is preferable to group them using this method over simply {@link SC.Logger.group} — the log levels could be set such that the 'error' messages are never seen, and you wouldn’t want an empty/needless group! You can optionally provide a title for the group. If there are any additional arguments, the first argument is assumed to be a format string. Thus, you can (and should) use it like: SC.Logger.errorGroup("%@: My error group", this); // good …and not: SC.Logger.errorGroup("%@: My error group".fmt(this)); // bad The former method can be more efficient because if the log levels are set in such a way that the error() invocation will be ignored, then the String.fmt() call will never actually be performed. @param {String} (optional) A title or format string to display above the group @param {…} (optional) Other arguments to pass to String.fmt() when using a format string as the title */ errorGroup: function(message, optionalFormatArgs) { // Implementation note: To avoid having to put the SC.errorGroup() // shorthand variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleGroup(SC.LOGGER_LEVEL_ERROR, message, arguments); }, /** Ends a group initiated with {@link SC.Logger.errorGroup}, provided the respective output/recording log levels are set appropriately. @see SC.Logger.errorGroup */ errorGroupEnd: function() { // Implementation note: To avoid having to put the SC.errorGroupEnd() // shorthand variant inside a function wrapper, we'll avoid 'this'. SC.Logger._handleGroupEnd(SC.LOGGER_LEVEL_ERROR); }, /** This method will output all recorded log messages to the reporter. This provides a convenient way to see the messages “on-demand” without having to have them always output. The timestamp of each message will be included as a prefix if you specify 'includeTimestamps' as YES, although in some browsers the native group indenting can make the timestamp formatting less than ideal. @param {Boolean} (optional) Whether to include timestamps in the output */ outputRecordedLogMessages: function(includeTimestamps) { // If we have no reporter, there's nothing we can do. if (!this.get('exists')) return; var reporter = this.get('reporter'), entries = this.get('recordedLogMessages'), indentation = 0, timestampFormat = SC.LOGGER_RECORDED_LOG_TIMESTAMP_PREFIX, i, iLen, entry, type, timestampStr, message, originalArguments, output, title, newIndentation, disparity, j, jLen; if (entries) { for (i = 0, iLen = entries.length; i < iLen; ++i) { entry = entries[i]; type = entry.type; if (includeTimestamps) { timestampStr = timestampFormat.fmt(entry.timestamp.toUTCString()); } // Is this a message or a group directive? message = entry.message; if (message) { // It's a message entry. Were the original arguments stored? If // so, we need to use those instead of the message. originalArguments = entry.originalArguments; this._outputMessage(type, timestampStr, indentation, message, originalArguments); } else { // It's a group directive. Update our indentation appropriately. newIndentation = entry.indentation; title = entry.title; disparity = newIndentation - indentation; // If the reporter implements group() and the indentation level // changes by more than 1, that implies that some earlier “begin // group” / “end group” directives were pruned from the beginning of // the buffer and we need to insert empty groups to compensate. if (reporter.group) { if (Math.abs(disparity) > 1) { for (j = 0, jLen = (disparity - 1); j < jLen; ++j) { if (disparity > 0) { reporter.group(); } else { reporter.groupEnd(); } } } if (disparity > 0) { output = timestampStr ? timestampStr : ""; output += title; reporter.group(output); } else { reporter.groupEnd(); } } else { // The reporter doesn't implement group()? Then simulate it using // log(), assuming it implements that. if (disparity > 0) { // We're beginning a group. Output the header at an indentation // that is one smaller. this._outputGroup(type, timestampStr, newIndentation - 1, title); } // else {} (There is no need to simulate a group ending.) } // Update our indentation. indentation = newIndentation; } } } }, /** This method will return a string representation of all recorded log messages to the reporter, which can be convenient for saving logs and so forth. The timestamp of each message will be included in the string. If there are no recorded log messages, an empty string will be returned (as opposed to null). @returns {String} */ stringifyRecordedLogMessages: function() { var ret = "", entries = this.get('recordedLogMessages'), indentation = 0, timestampFormat = SC.LOGGER_RECORDED_LOG_TIMESTAMP_PREFIX, prefixMapping = this._LOG_FALLBACK_PREFIX_MAPPING, groupHeader = SC.LOGGER_LOG_GROUP_HEADER, i, iLen, entry, type, message, originalArguments, prefix, line, title, newIndentation, disparity; if (entries) { for (i = 0, iLen = entries.length; i < iLen; ++i) { entry = entries[i]; type = entry.type; // First determine the prefix. prefix = timestampFormat.fmt(entry.timestamp.toUTCString()); prefix += prefixMapping[type] || ""; // Is this a message or a group directive? message = entry.message; if (message) { // It's a message entry. Were arguments used, or did we format a // message? If arguments were used, we need to stringify those // instead of using the message. originalArguments = entry.originalArguments; line = prefix + this._indentation(indentation); line += originalArguments ? this._argumentsToString(originalArguments) : message; } else { // It's a group directive, so we need to update our indentation // appropriately. Also, if it's the beginning of the group and it // has a title, then we need to include an appropriate header. newIndentation = entry.indentation; title = entry.title; disparity = newIndentation - indentation; if (disparity > 0) { // We're beginning a group. Output the header at an indentation // that is one smaller. line = prefix + this._indentation(indentation) + groupHeader.fmt(title); } // Update our indentation. indentation = newIndentation; } // Add the line to our string. ret += line + "\n"; } } return ret; }, /** Log output to the console, but only if it exists. IMPORTANT: Unlike debug(), info(), warn(), and error(), messages sent to this method do not consult the log level and will always be output. Similarly, they will never be recorded. In general, you should avoid this method and instead choose the appropriate categorization for your message, choosing the appropriate method. @param {String|Array|Function|Object} @returns {Boolean} Whether or not anything was logged */ log: function() { var reporter = this.get('reporter'), message = arguments[0], prefix = this.get('messagePrefix'), outputPrefix = this.get('outputMessagePrefix'), ret = NO; // If the first argument is a string and a prefix was specified, use it. if (message && SC.typeOf(message) === SC.T_STRING) { if (prefix || outputPrefix) { if (prefix) message = prefix + message; if (outputPrefix) message = outputPrefix + message; arguments[0] = message; } } // Log through the reporter. if (this.get('exists')) { if (typeof reporter.log === "function") { reporter.log.apply(reporter, arguments); ret = YES; } else if (reporter.log) { // IE8 implements console.log but reports the type of console.log as // "object", so we cannot use apply(). Because of this, the best we // can do is call it directly with an array of our arguments. reporter.log(this._argumentsToArray(arguments)); ret = YES; } } // log through alert if (!ret && this.get('fallBackOnAlert')) { // include support for overriding the alert through the reporter // if it has come this far, it's likely this will fail if (this.get('exists') && (typeof reporter.alert === "function")) { reporter.alert(arguments); ret = YES; } else { alert(arguments); ret = YES; } } return ret; }, /** Every log after this call until {@link SC.Logger.groupEnd} is called will be indented for readability. You can create as many levels as you want. IMPORTANT: Unlike debugGroup(), infoGroup(), warnGroup(), and errorGroup(), this method do not consult the log level and will always result in output when the reporter supports it. Similarly, group messages logged via this method will never be recorded. @param {String} (optional) An optional title to display above the group */ group: function(title) { var reporter = this.get('reporter'); if (this.get('exists') && (typeof reporter.group === "function")) { reporter.group(title); } }, /** Ends a group declared with {@link SC.Logger.group}. @see SC.Logger.group */ groupEnd: function() { var reporter = this.get('reporter'); if (this.get('exists') && (typeof reporter.groupEnd === "function")) { reporter.groupEnd(); } }, /** Outputs the properties of an object. Logs the object using {@link SC.Logger.log} if the reporter.dir function does not exist. @param {Object} */ dir: function() { var reporter = this.get('reporter'); if (this.get('exists') && (typeof reporter.dir === "function")) { // Firebug's console.dir doesn't support multiple objects here // but maybe custom reporters will reporter.dir.apply(reporter, arguments); } else { this.log.apply(this, arguments); } }, /** Prints an XML outline for any HTML or XML object. Logs the object using {@link SC.Logger.log} if reporter.dirxml function does not exist. @param {Object} */ dirxml: function() { var reporter = this.get('reporter'); if (this.get('exists') && (typeof reporter.dirxml === "function")) { // Firebug's console.dirxml doesn't support multiple objects here // but maybe custom reporters will reporter.dirxml.apply(reporter, arguments); } else { this.log.apply(this, arguments); } }, /** Begins the JavaScript profiler, if it exists. Call {@link SC.Logger.profileEnd} to end the profiling process and receive a report. @param {String} (optional) A title to associate with the profile @returns {Boolean} YES if reporter.profile exists, NO otherwise */ profile: function(title) { var reporter = this.get('reporter'); if (this.get('exists') && (typeof reporter.profile === "function")) { reporter.profile(title); return YES; } return NO; }, /** Ends the JavaScript profiler, if it exists. If you specify a title, the profile with that title will be ended. @param {String} (optional) A title to associate with the profile @returns {Boolean} YES if reporter.profileEnd exists, NO otherwise @see SC.Logger.profile */ profileEnd: function(title) { var reporter = this.get('reporter'); if (this.get('exists') && (typeof reporter.profileEnd === "function")) { reporter.profileEnd(title); return YES; } return NO; }, /** Measure the time between when this function is called and {@link SC.Logger.timeEnd} is called. @param {String} The name of the profile to begin @returns {Boolean} YES if reporter.time exists, NO otherwise @see SC.Logger.timeEnd */ time: function(name) { var reporter = this.get('reporter'); if (this.get('exists') && (typeof reporter.time === "function")) { reporter.time(name); return YES; } return NO; }, /** Ends the profile specified. @param {String} The name of the profile to end @returns {Boolean} YES if reporter.timeEnd exists, NO otherwise @see SC.Logger.time */ timeEnd: function(name) { var reporter = this.get('reporter'); if (this.get('exists') && (typeof reporter.timeEnd === "function")) { reporter.timeEnd(name); return YES; } return NO; }, /** Prints a stack-trace. @returns {Boolean} YES if reporter.trace exists, NO otherwise */ trace: function() { var reporter = this.get('reporter'); if (this.get('exists') && (typeof reporter.trace === "function")) { reporter.trace(); return YES; } return NO; }, // .......................................................... // INTERNAL SUPPORT // init: function() { sc_super(); // Set a reasonable default value if none has been set. if (!this.get('logOutputLevel')) { if (SC.buildMode === "debug") { this.set('logOutputLevel', SC.LOGGER_LEVEL_DEBUG); } else { this.set('logOutputLevel', SC.LOGGER_LEVEL_INFO); } } this.debugEnabledDidChange(); }, /** @private For backwards compatibility with the older 'debugEnabled' property, set our log output level to SC.LOGGER_LEVEL_DEBUG if 'debugEnabled' is set to YES. */ debugEnabledDidChange: function() { if (this.get('debugEnabled')) { this.set('logOutputLevel', SC.LOGGER_LEVEL_DEBUG); } }.observes('debugEnabled'), /** @private Outputs and/or records the specified message of the specified type if the respective current log levels allow for it. Assuming 'automaticallyFormat' is specified, then String.fmt() will be called automatically on the message, but only if at least one of the log levels is such that the result will be used. @param {String} type Expected to be SC.LOGGER_LEVEL_DEBUG, etc. @param {Boolean} automaticallyFormat Whether or not to treat 'message' as a format string if there are additional arguments @param {String} message Expected to a string format (for String.fmt()) if there are other arguments @param {String} (optional) originalArguments All arguments passed into debug(), etc. (which includes 'message'; for efficiency, we don’t copy it) */ _handleMessage: function(type, automaticallyFormat, message, originalArguments) { // Are we configured to show this type? var shouldOutput = this._shouldOutputType(type), shouldRecord = this._shouldRecordType(type), hasOtherArguments, i, len, args, output, entry, prefix, outputPrefix, recordedPrefix; // If we're neither going to output nor record the message, then stop now. if (!(shouldOutput || shouldRecord)) return; // Do we have arguments other than 'message'? (Remember that // 'originalArguments' contains the message here, too, hence the > 1.) hasOtherArguments = (originalArguments && originalArguments.length > 1); // If we're automatically formatting and there is no message (or it is // not a string), then don't automatically format after all. if (automaticallyFormat && (SC.none(message) || (typeof message !== "string"))) { automaticallyFormat = NO; } // If we should automatically format, and the client specified other // arguments in addition to the message, then we'll call .fmt() assuming // that the message is a format string. if (automaticallyFormat) { if (hasOtherArguments) { args = []; for (i = 1, len = originalArguments.length; i < len; ++i) { args.push(originalArguments[i]); } message = message.fmt.apply(message, args); } } // If a message prefix was specified, use it. prefix = this.get('messagePrefix'); if (prefix) message = prefix + message; if (shouldOutput) { outputPrefix = this.get('outputMessagePrefix'); // We only want to pass the original arguments to _outputMessage() if we // didn't format the message ourselves. args = automaticallyFormat ? null : originalArguments; this._outputMessage(type, null, this._outputIndentationLevel, (outputPrefix ? outputPrefix + message : message), args); } // If we're recording the log, append the message now. if (shouldRecord) { recordedPrefix = this.get('recordedMessagePrefix'); entry = { type: type, message: message ? (recordedPrefix ? recordedPrefix + message : message) : YES, timestamp: new Date() }; // If we didn't automatically format, and we have other arguments, then // be sure to record them, too. if (!automaticallyFormat && hasOtherArguments) { entry.originalArguments = originalArguments; } this._addRecordedMessageEntry(entry); } }, /** @private Outputs and/or records a group with the (optional) specified title assuming the respective current log levels allow for it. This will output the title (if there is one) and indent all further messages (of any type) until _handleGroupEnd() is invoked. If additional arguments beyond a title are passed in, then String.fmt() will be called automatically on the title, but only if at least one of the log levels is such that the result will be used. @param {String} type Expected to be SC.LOGGER_LEVEL_DEBUG, etc. @param {String} (optional) title Expected to a string format (for String.fmt()) if there are other arguments @param {String} (optional) originalArguments All arguments passed into debug(), etc. (which includes 'title'; for efficiency, we don’t copy it) */ _handleGroup: function(type, title, originalArguments) { // Are we configured to show this type? var shouldOutput = this._shouldOutputType(type), shouldRecord = this._shouldRecordType(type), hasOtherArguments, i, len, args, arg, reporter, func, header, output, indentation, entry; // If we're neither going to output nor record the group, then stop now. if (!(shouldOutput || shouldRecord)) return; // Do we have arguments other than 'title'? (Remember that // 'originalArguments' contains the title here, too, hence the > 1.) hasOtherArguments = (originalArguments && originalArguments.length > 1); // If the client specified a title as well other arguments, then we'll // call .fmt() assuming that the title is a format string. if (title && hasOtherArguments) { args = []; for (i = 1, len = originalArguments.length; i < len; ++i) { args.push(originalArguments[i]); } title = title.fmt.apply(title, args); } if (shouldOutput) { this._outputGroup(type, null, this._outputIndentationLevel, title); // Increase our indentation level to accommodate the group. this._outputIndentationLevel++; } // If we're recording the group, append the entry now. if (shouldRecord) { // Increase our indentation level to accommodate the group. indentation = ++this._recordingIndentationLevel; entry = { type: type, indentation: indentation, beginGroup: YES, title: title, timestamp: new Date() }; this._addRecordedMessageEntry(entry); } }, /** @private Outputs and/or records a “group end” assuming the respective current log levels allow for it. This will remove one level of indentation from all further messages (of any type). @param {String} type Expected to be SC.LOGGER_LEVEL_DEBUG, etc. */ _handleGroupEnd: function(type) { // Are we configured to show this type? var shouldOutput = this._shouldOutputType(type), shouldRecord = this._shouldRecordType(type), reporter, func, indentation, entry; // If we're neither going to output nor record the "group end", then stop // now. if (!(shouldOutput || shouldRecord)) return; if (shouldOutput) { // Decrease our indentation level to accommodate the group. this._outputIndentationLevel--; if (this.get('exists')) { // Do we have reporter.groupEnd defined as a function? If not, we // simply won't output anything. reporter = this.get('reporter'); func = reporter.groupEnd; if (func) { func.call(reporter); } } } // If we're recording the “group end”, append the entry now. if (shouldRecord) { // Decrease our indentation level to accommodate the group. indentation = --this._recordingIndentationLevel; entry = { type: type, indentation: indentation, timestamp: new Date() }; this._addRecordedMessageEntry(entry); } }, /** @private Returns whether a message of the specified type ('debug', etc.) should be output to the reporter based on the current value of 'logOutputLevel'. @param {Constant} type @returns {Boolean} */ _shouldOutputType: function(type) { var logLevelMapping = this._LOG_LEVEL_MAPPING, level = logLevelMapping[type] || 0, currentLevel = logLevelMapping[this.get('logOutputLevel')] || 0; return (level <= currentLevel); }, /** @private Returns whether a message of the specified type ('debug', etc.) should be recorded based on the current value of 'logRecordingLevel'. @param {Constant} type @returns {Boolean} */ _shouldRecordType: function(type) { // This is the same code as in _shouldOutputType(), but inlined to // avoid yet another function call. var logLevelMapping = this._LOG_LEVEL_MAPPING, level = logLevelMapping[type] || 0, currentLevel = logLevelMapping[this.get('logRecordingLevel')] || 0; return (level <= currentLevel); }, /** @private Outputs the specified message to the current reporter. If the reporter does not handle the specified type of message, it will fall back to using log() if possible. @param {Constant} type @param {String} timestampStr An optional timestamp prefix for the line, or null for none @param {Number} indentation The current indentation level @param {String} message @param {Arguments} (optional) originalArguments If specified, the assumption is that the message was not automatically formatted */ _outputMessage: function(type, timestampStr, indentation, message, originalArguments) { if (!this.get('exists')) return; // Do we have reporter[type] defined as a function? If not, we'll fall // back to reporter.log if that exists. var reporter = this.get('reporter'), output, shouldIndent, func, prefix, args, arg; // If the reporter doesn't support group(), then we need to manually // include indentation for the group. (It it does, we'll assume that // we're currently at the correct group level.) shouldIndent = !reporter.group; // Note: Normally we wouldn't do the hash dereference twice, but // storing the result like this: // // var nativeFunction = console[type]; // nativeFunction(output); // // …doesn't work in Safari 4, and: // // nativeFunction.call(console, output); // // …doesn't work in IE8 because the console.* methods are // reported as being objects. func = reporter[type]; if (func) { // If we formatted, just include the message. Otherwise, include all // the original arguments. if (!originalArguments) { output = timestampStr ? timestampStr : ""; if (shouldIndent) output += this._indentation(indentation); output += message; reporter[type](output); } else { // We have arguments? Then pass them along to the reporter function // so that it can format them appropriately. We'll use the timestamp // string (if there is one) and the indentation as the first // arguments. args = this._argumentsToArray(originalArguments); prefix = ""; if (timestampStr) prefix = timestampStr; if (shouldIndent) prefix += this._indentation(indentation); if (prefix) args.splice(0, 0, prefix); if (func.apply) { func.apply(reporter, args); } else { // In IE8, passing the arguments as an array isn't ideal, but it's // pretty much all we can do because we can't call apply(). reporter[type](args); } } } else { // The reporter doesn't support the requested function? If it at least // support log(), fall back to that. if (reporter.log) { prefix = ""; if (timestampStr) prefix = timestampStr; prefix += this._LOG_FALLBACK_PREFIX_MAPPING[type] || ""; if (shouldIndent) prefix += this._indentation(indentation); // If we formatted, just include the message. Otherwise, include // all the original arguments. if (!originalArguments) { reporter.log(prefix + message); } else { args = this._argumentsToArray(originalArguments); if (prefix) args.splice(0, 0, prefix); reporter.log(args); } } } }, /** @private Outputs the specified “begin group” directive to the current reporter. If the reporter does not handle the group() method, it will fall back to simulating using log() if possible. @param {Constant} type @param {String} timestampStr An optional timestamp prefix for the line, or null for none @param {Number} indentation The current indentation level, not including what the group will set it to @param {String} (optional) title */ _outputGroup: function(type, timestampStr, indentation, title) { if (!this.get('exists')) return; // Do we have reporter.group defined as a function? If not, we'll fall // back to reporter.log if that exists. (Thankfully, we can avoid the IE8 // special-casing we have in _outputMessage() because IE8 doesn't support // console.group(), anyway.) var reporter = this.get('reporter'), func = reporter.group, output; if (func) { output = timestampStr ? timestampStr : ""; output += title; func.call(reporter, output); } else if (reporter.log) { // The reporter doesn't support group()? Then simulate with log(). // (We'll live with the duplicitous dereference rather than using // apply() to work around the IE8 issue described in _outputMessage().) output = ""; if (timestampStr) output = timestampStr; output += this._LOG_FALLBACK_PREFIX_MAPPING[type] || ""; output += this._indentation(indentation); output += SC.LOGGER_LOG_GROUP_HEADER.fmt(title); reporter.log(output); } }, /** @private This method will add the specified entry to the recorded log messages array and also prune array as necessary according to the current values of 'recordedLogMessagesMaximumLength' and 'recordedLogMessagesPruningMinimumLength'. */ _addRecordedMessageEntry: function(entry) { var recordedMessages = this.get('recordedLogMessages'), len; // Lazily create the array. if (!recordedMessages) { recordedMessages = []; this.set('recordedLogMessages', recordedMessages); } recordedMessages.push(entry); // Have we exceeded the maximum size? If so, do some pruning. len = recordedMessages.length; if (len > this.get('recordedLogMessagesMaximumLength')) { recordedMessages.splice(0, (len - this.get('recordedLogMessagesPruningMinimumLength'))); } // Notify that the array content changed. recordedMessages.enumerableContentDidChange(); }, /** @private The arguments function property doesn't support Array#unshift. This helper copies the elements of arguments to a blank array. @param {Array} arguments The arguments property of a function @returns {Array} An array containing the elements of arguments parameter */ _argumentsToArray: function(args) { var ret = [], i, len; if (args) { for (i = 0, len = args.length; i < len; ++i) { ret[i] = args[i]; } } return ret; }, /** @private Formats the arguments array of a function by creating a string with SC.LOGGER_LOG_DELIMITER between the elements. */ _argumentsToString: function() { var ret = "", delimiter = SC.LOGGER_LOG_DELIMITER, i, len; for (i = 0, len = (arguments.length - 1); i < len; ++i) { ret += arguments[i] + delimiter; } ret += arguments[len]; return ret; }, /** @private Returns a string containing the appropriate indentation for the specified indentation level. @param {Number} The indentation level @returns {String} */ _indentation: function(level) { if (!level || level < 0) { level = 0; } var ret = "", indent = SC.LOGGER_LOG_GROUP_INDENTATION, i; for (i = 0; i < level; ++i) { ret += indent; } return ret; }, /** @private The current “for output” indentation level. The reporter (browser console) is expected to keep track of this for us for output, but we need to do our own bookkeeping if the browser doesn’t support console.group. This is incremented by _debugGroup() and friends, and decremented by _debugGroupEnd() and friends. */ _outputIndentationLevel: 0, /** @private The current “for recording” indentation level. This can be different than the “for output” indentation level if the respective log levels are set differently. This is incremented by _debugGroup() and friends, and decremented by _debugGroupEnd() and friends. */ _recordingIndentationLevel: 0, /** @private A mapping of the log level constants (SC.LOGGER_LEVEL_DEBUG, etc.) to their priority. This makes it easy to determine which levels are “higher” than the current level. Implementation note: We’re hardcoding the values of the constants defined earlier here for a tiny bit of efficiency (we can create the hash all at once rather than having to push in keys). */ _LOG_LEVEL_MAPPING: { debug: 4, info: 3, warn: 2, error: 1, none: 0 }, /** @private If the current reporter does not support a particular type of log message (for example, some older browsers’ consoles support console.log but not console.debug), we’ll use the specified prefixes. Implementation note: We’re hardcoding the values of the constants defined earlier here for a tiny bit of efficiency (we can create the hash all at once rather than having to push in keys). */ _LOG_FALLBACK_PREFIX_MAPPING: { debug: SC.LOGGER_LOG_DEBUG, info: SC.LOGGER_LOG_INFO, warn: SC.LOGGER_LOG_WARN, error: SC.LOGGER_LOG_ERROR } }); // Add convenient shorthands methods to SC. SC.debug = SC.Logger.debug; SC.info = SC.Logger.info; SC.warn = SC.Logger.warn; SC.error = SC.Logger.error;