console.log wrapper that keeps line numbers and supports most methods?

JavascriptLogging

Javascript Problem Overview


How can i write a console log wrapper that:

  • Keeping the recorded line number and file name of the log statement intact
  • Provides access to all log severity methods (error, log, debug, ...) and shows them in the console as they where logged
  • does provide some fallback (for example calls the log method when the browser does not support error)
  • can be switched off in a central location, so I can switch off logging for production
  • does handle the case that no console exists, and does not throw errors

Since logging in Java Script is so inconsistent, there must be some solution. Implementing it myself is a little bit tedious, but there seems to be no good library.

I currently found this logger that provides all the features, but it does mess up the line numbers. http://benalman.com/projects/javascript-debug-console-log/

Javascript Solutions


Solution 1 - Javascript

There is my own log4javascript, which has its own logging console but also provides a wrapper around console.log. It fulfils all your criteria except keeping line numbers intact, which is impossible to achieve if you're wrapping calls to console.log() etc. in another function.

var log = log4javascript.getLogger("main");
var appender = new log4javascript.BrowserConsoleAppender();
log.addAppender(appender);
log.debug("Hello world");

Solution 2 - Javascript

I would also recommend log4javascript and explain how you can still keep the information about the printed filename and line, at least in Chrome.

I am not talking about changing the filename and line printed by Chrome but you can get to the information you are interested in and append it to the log statement. My solution has been a quick hack but I think with a little more work you can get nicely formatted log statements. It probably has also a heavy performance-impact, but since you won't leave your logs activated in production this shouldn't be too much of a problem.

The Concept

In Chrome you can create an Error object which provides a stack property that shows you your current stack location and somewhere in the stack string you find the file and line number of your calling script.

  > new Error().stack
  "Error
    at eval at <anonymous> (eval at evaluate (unknown source))
    at eval at evaluate (unknown source)
    at FrameMirror.evaluate (native)
    at Object.evaluate (unknown source)
    at Object._evaluateOn (unknown source)
    at Object._evaluateAndWrap (unknown source)
    at Object.evaluateOnCallFrame (unknown source)
    at meinAjaxAufruf (http://localhost:8080/numberajax.js:21:9)
    at HTMLInputElement.onkeyup (http://localhost:8080/numberajax.html:15:188)"

For a log4javascript call the stack trace might look something like this:

"Error
    at Object.append (http://localhost:8080/log4javascript_uncompressed.js:1921:17)
    at Object.doAppend (http://localhost:8080/log4javascript_uncompressed.js:1047:9)
    at Object.callAppenders (http://localhost:8080/log4javascript_uncompressed.js:647:27)
    at Object.log (http://localhost:8080/log4javascript_uncompressed.js:640:10)
    at Object.debug (http://localhost:8080/log4javascript_uncompressed.js:748:9)
    at meinAjaxAufruf (http://localhost:8080/numberajax.js:36:16)
    at HTMLInputElement.onkeyup (http://localhost:8080/numberajax.html:16:188)"

And the file and line that made the log4javascript call and that i am interested in is

at meinAjaxAufruf (http://localhost:8080/numberajax.js:36:16)

The Solution

I am guessing that the stack depth from the script your interested in to where the actual console call happens is always the same. So now you simply have to find out where the BrowserConsoleAppender makes its window.console access and add the line you are interested in to the formatted string. I did the following changes to log4javascript_uncompressed.js (version 1.4.2 line 1913):

} else if (window.console && window.console.log) { // Safari and Firebug
		var formattedMesage = getFormattedMessage();
		
		//---my additions
		var isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
		if(isChrome){
			var stack = new Error().stack;
			var lineAccessingLogger = stack.split("\n")[6];
			formattedMesage += "\n" + lineAccessingLogger;
		}
		//---
		
		// Log to Firebug using its logging methods or revert to the console.log
		// method in Safari
		if (window.console.debug && Level.DEBUG.isGreaterOrEqual(loggingEvent.level)) {
			window.console.debug(formattedMesage);
		} else if (window.console.info && Level.INFO.equals(loggingEvent.level)) {
		...

Now instead of

17:53:22,872 DEBUG - sending /NumberServlet?zahl=1&text=
                                                 log4javascript.js:154

I get

17:55:53,008 DEBUG - sending /NumberServlet?zahl=1&text=

    at meinAjaxAufruf (http://localhost:8080/numberajax.js:36:16) log4javascript_uncompressed.js:1930

It sure isn't a nice solution :), but I get what I need.

With a little more knowledge of the framework I suppose one could change the PatternLayout in a way that you can define how to print the file name/location and line number.

edit Instead of my prior solution I made some modifications to the PatternLayout.prototype.format function, so now I can use the additional option %l to define where and how I want to output the calling file and its line. I published my changes and a usage example as a Gist.

Solution 3 - Javascript

We had this issue with our log wrapper also and it turns out there is a fantastic, simple workaround using partial function application:

if(DEBUG_ENABLED && (typeof console != 'undefined')) {
	this.debug = console.log.bind(console);
}
else {
	this.debug = function(message) {};
}

With this, your browser will detect the correct line number and file of the source you wanted to log.

Solution 4 - Javascript

Crossposting from related question (https://stackoverflow.com/questions/13815640/a-proper-wrapper-for-console-log-with-correct-line-number/14841411#14841411) but with updated solution to address multiple methods.


I liked @fredrik's answer, so I rolled it up with another answer which splits the Webkit stacktrace, and merged it with @PaulIrish's safe console.log wrapper. "Standardizes" the filename:line to a "special object" so it stands out and looks mostly the same in FF and Chrome.

Testing in fiddle: http://jsfiddle.net/drzaus/pWe6W/9/

_log = (function (methods, undefined) {

	var Log = Error; // does this do anything?  proper inheritance...?
	Log.prototype.write = function (args, method) {
		/// <summary>
		/// Paulirish-like console.log wrapper.  Includes stack trace via @fredrik SO suggestion (see remarks for sources).
		/// </summary>
		/// <param name="args" type="Array">list of details to log, as provided by `arguments`</param>
		/// <param name="method" type="string">the console method to use:  debug, log, warn, info, error</param>
		/// <remarks>Includes line numbers by calling Error object -- see
		/// * http://paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/
		/// * https://stackoverflow.com/questions/13815640/a-proper-wrapper-for-console-log-with-correct-line-number
		/// * https://stackoverflow.com/a/3806596/1037948
		/// </remarks>

		// via @fredrik SO trace suggestion; wrapping in special construct so it stands out
		var suffix = {
			"@": (this.lineNumber
					? this.fileName + ':' + this.lineNumber + ":1" // add arbitrary column value for chrome linking
					: extractLineNumberFromStack(this.stack)
			)
		};

		args = args.concat([suffix]);
		// via @paulirish console wrapper
		if (console && console[method]) {
			if (console[method].apply) { console[method].apply(console, args); } else { console[method](args); } // nicer display in some browsers
		}
	};
	var extractLineNumberFromStack = function (stack) {
		/// <summary>
		/// Get the line/filename detail from a Webkit stack trace.  See https://stackoverflow.com/a/3806596/1037948
		/// </summary>
		/// <param name="stack" type="String">the stack string</param>

		// correct line number according to how Log().write implemented
		var line = stack.split('\n')[3];
		// fix for various display text
		line = (line.indexOf(' (') >= 0
			? line.split(' (')[1].substring(0, line.length - 1)
			: line.split('at ')[1]
			);
		return line;
	};

	// method builder
	var logMethod = function(method) {
		return function (params) {
			/// <summary>
			/// Paulirish-like console.log wrapper
			/// </summary>
			/// <param name="params" type="[...]">list your logging parameters</param>
	
			// only if explicitly true somewhere
			if (typeof DEBUGMODE === typeof undefined || !DEBUGMODE) return;
	
			// call handler extension which provides stack trace
			Log().write(Array.prototype.slice.call(arguments, 0), method); // turn into proper array & declare method to use
		};//--	fn	logMethod
	};
	var result = logMethod('log'); // base for backwards compatibility, simplicity
	// add some extra juice
	for(var i in methods) result[methods[i]] = logMethod(methods[i]);

	return result; // expose
})(['error', 'debug', 'info', 'warn']);//--- _log

Solution 5 - Javascript

To keep it simple, I've the below wrapper for console methods:

var noop = function () {};
window.consolex = {
	debug : window.console && window.console.debug && console.debug.bind(console) || noop,
    log : window.console && window.console.log && console.log.bind(console) || noop,
    warn: window.WARN = window.console && window.console.warn && console.warn.bind(console) || noop,
    error: window.ERROR = window.console && window.console.error && console.error.bind(console) || noop
};

Also, for better logs in IE and older browsers, please read: [Detailed console logging][1]

[1]: http://patik.com/blog/detailed-console-logging/ "Detailed console logging"

Solution 6 - Javascript

Google Chrome will soon have a feature that will be of interest to this thread.

You can enable it now by:

  1. Enable chrome://flags/#enable-devtools-experiments
  2. Click on cog in dev tools
  3. Go to Experiments Tab
  4. Check "Javascript frameworks debugging"
  5. Go to General Tab
  6. Under the Sources section
  7. Check "Skip stepping through sources with particular names"
  8. In the pattern inputbox: type in the file name that you see now (app.log.js)

Restart and enjoy :)

References:

Tests from chrom devtools

devtools Issues thread

devtools code review

Solution 7 - Javascript

I answered this question here, but in short see the codepen for full implementation. However, this does everything you want, cross browser, no errors, correct line numbers, all available console methods, global and local control:

var Debugger = function(gState, klass) {
  this.debug = {}
  if (!window.console) return function(){}
  if (gState && klass.isDebug) {
    for (var m in console)
      if (typeof console[m] == 'function')
        this.debug[m] = console[m].bind(window.console, klass.toString()+": ")
  }else{
    for (var m in console)
      if (typeof console[m] == 'function')
        this.debug[m] = function(){}
  }
  return this.debug
}

And use it like this:

isDebug = true //global debug state

debug = Debugger(isDebug, this)

debug.log('Hello Log!')

Solution 8 - Javascript

I found a solution (requires jquery) somehwere on the Web but it does not work in most browsers. I changed it and it works in Firefox (Mac, Linux. Android), Chrome (Mac, Linux. Android) and Safari and other Android webkit browsers.

Just write the following code to a file called e.g. debug.js and include it after the inclusion of 'jquery.js' in the <head> section of your webpage and it will work after the page has loaded (document.ready). I still have to find out to allow debugging before everything is loaded (e.g. only the <head>...</head> ). The webpage has to be called with ?d=1 in the URL and when using Safari ?d=1s as I cannot make a distinction between Safari and another Webkit browser in the user agent and Safari has a different behavior in line number and file name handling than other Webkit browsers.

The function p_r(expression) logs to the window of the id #js_debug and to the console (if opened) with the file name and line number.

var g_d = null;


function sortObj(theObj)
{
  var sortable = [];
  for (var i in theObj) {
    sortable.push(i);
  }
  sortable.sort();

  var copy = new Object;
  for (var i in sortable) {
    var ind = sortable[i];
    copy[ind] = theObj[ind];
  }

  return copy;

}

function p_r(s, comment, level)
{
  if (!g_d) return;
  var res = s;
  var pre = new Array("","  " , "    ", "      ", "        ");
  if (comment) comment += ' : ';
  if (arguments.length<2) comment='';
  if (arguments.length<3) level = 0;
//  if (console) console.log(s);
  if (typeof(s) == 'object') {
    var copy = sortObj(s);
    comment += '\n';
    res = '[object]\n';
    if (level < 2) {
      for (var i in copy) {
        if (typeof(copy[i]) != "function")
          res += pre[level] + (i) + " : " + p_r(copy[i], '', level+1) +  " : " + typeof(copy[i]) + "\n";
      }
      res += pre[level] + "[/object]\n";
    }
  }
  else if (typeof(s) == 'function')
    res = 'function';
  else if (typeof(s) != 'string')
    res = '' + s;
  res = res.replace(/&/g, '&amp;');
  res = res.replace(/\x3C/g, '&lt;');
  res = res.replace(/>/g, '&gt;');
  if (level == 0) {
window.LOG=res;
console.log(window.LOG + comment + res);
    g_d.innerHTML += (window.LOG + comment + res + '\n');
  }
  return res;
}

if (location.href.match(/d\=[1-9]/)) {

  $(document).ready(function() {
    $("body").prepend("<div id=\"js_debugclick\" onclick=\"$('#js_debug').toggle();\">JS DEBUG</div>\
  <pre onclick=\"$('#js_debug').toggle();\" id='js_debug'></pre>\
");

    $("head").append("<style type=\"text/css\">\
pre#js_debug {\
border: solid black 1px; background-color: #1CF; color: #000; display:none; position:absolute; top: 20px;\
font-family: Lucida Console, monospace; font-size: 9pt; height: 400px; overflow:scroll; width:100%;\
z-index:100;\
} \
#js_debugclick { \
  color:red; font-weight:bold; \
} \
</style>\
");
    g_d = document.getElementById('js_debug');
  });

  var __moredebug = location.href.match(/d\=[2-9]/);

	var __issafari = /safari/.test(navigator.userAgent.toLowerCase()) && location.href.match(/d\=[1-9]s/);
	var __iswebkit = /webkit/.test(navigator.userAgent.toLowerCase());
	var __isopera  = /opera/.test(navigator.userAgent.toLowerCase());
  if (__moredebug) console.log(__issafari, __iswebkit);

/*@const*/ //for closure-compiler
//DEBUG=2 // 0=off, 1=msg:file:line:column, 2=msg:stack-trace

/*@const @constructor*/
Object.defineProperty(window,'__stack__',{get:function(){
    try{i.dont.exist()}catch(e){
if (__moredebug)  var x=e.stack.split(":"); for (i in x){console.log(i,x[i]);}
//    console.log(e.stack.split(":")[13].match(/(\d+)/)[1]);
    return e.stack.split(":")}
}})

/*@const @constructor*/
Object.defineProperty(window,'__file__',{get:function(){
    var s=__stack__,l=s.length
    var f= __issafari ? s[9] : (__isopera ? s[12] : (__iswebkit ? s[14] : s[9]));
    return f.replace(/^.+?\/([^\/]+?)\?.+?$/, "$1");
}})

/*@const @constructor*/
Object.defineProperty(window,'__line__',{get:function(){
    var s=__stack__,l=s.length
    return __issafari ? s[10].match(/(\d+)/)[1] :(__isopera ? s[13].match(/(\d+)/)[1] : (__iswebkit ? s[15] : s[10].replace(/\n/, " ").replace(/(\d+).+?$/, "$1")));
}})

/*@const @constructor*/
Object.defineProperty(window,'__col__',{get:function(){
    var s=__stack__,l=s.length
    return (isNaN(s[l-2]))?"NA":s[l-1]
}})

/*@const @constructor*/
Object.defineProperty(window,'LOG',{
    get:function(){return out},
    set:function(msg){if(0)out=msg+"\t-\t"+__stack__
        else out=__file__+" "+__line__+": ";
        }
})



}//end if(DEBUG)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPaul WeberView Question on Stackoverflow
Solution 1 - JavascriptTim DownView Answer on Stackoverflow
Solution 2 - JavascriptLeoRView Answer on Stackoverflow
Solution 3 - JavascriptmprView Answer on Stackoverflow
Solution 4 - JavascriptdrzausView Answer on Stackoverflow
Solution 5 - JavascriptmanikantaView Answer on Stackoverflow
Solution 6 - JavascriptFrison AlexanderView Answer on Stackoverflow
Solution 7 - JavascriptArctelixView Answer on Stackoverflow
Solution 8 - Javascriptweb site makerView Answer on Stackoverflow