// ==========================================================================
// Project: SproutCore - JavaScript Application Framework
// Copyright: ©2006-2011 Strobe Inc. and contributors.
// Portions ©2008-2010 Apple Inc. All rights reserved.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
// ..........................................................
// 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.
@property {String}
*/
SC.LOGGER_LOG_DELIMITER = ", ";
/**
If {@link SC.Logger.error} falls back onto {@link SC.Logger.log}, this will be
prepended to the output.
@property {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.
@property {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.
@property {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.
@property {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.
@property {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.
@property {String}
*/
SC.LOGGER_LOG_GROUP_INDENTATION = " ";
/**
When reporting recorded log messages, the timestamp is included with this
prefix.
@property {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
{@link Firebug Logging Reference}
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({
// ..........................................................
// PROPERTIES
//
/**
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 output to the reporter object
(usually your browser’s console). 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.
@property: {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}
}
@property {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.
@property {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.
@property {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
@property: {Boolean}
*/
debugEnabled: NO,
/**
Computed property that checks for the existence of the reporter object.
@property {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.
@property {Boolean}
*/
fallBackOnAlert: NO,
/**
The reporter is the object which implements the actual logging functions.
@default The browser’s console
@property {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 ouput/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 ouput/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 ouput/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 ouput/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.utcFormat());
}
// 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.utcFormat());
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 stringfy 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'),
ret = NO;
// 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;
// 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 (shouldOutput) {
// 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, message, args);
}
// If we're recording the log, append the message now.
if (shouldRecord) {
entry = {
type: type,
message: 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 = "";
if (timestampStr) output = 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 = "",
delimeter = SC.LOGGER_LOG_DELIMITER,
i, len;
for (i = 0, len = (arguments.length - 1); i < len; ++i) {
ret += arguments[i] + delimeter;
}
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;