51

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/

Paul Weber
  • 6,518
  • 3
  • 43
  • 52
  • 1
    What do you mean by `line numbers`? I am using Chrome for debugging and I don't see any line numbers. Or do you mean that it should show in what line (of the script) debuger was called? – freakish Jul 03 '12 at 10:00
  • 1
    I mean the line numbers and file names shown after the log statement by most browsers, for example: http://postimage.org/image/i00xm3nmz/ Whats a log message worth when you do not know where it occured? – Paul Weber Jul 03 '12 at 10:23
  • 1
    The thing with logging is, you normally leave it in the application. So we have 2500 log statements in our code. The screenshot I gave you showed the problem that the info is not accurate. – Paul Weber Jul 03 '12 at 11:37
  • 2500 log statements?? That's a lot! I don't think I've ever seen so JavaScript heavy page. – freakish Jul 03 '12 at 11:42
  • Is there no other solution to keeping line numbers for console.logs? I've written my own logging wrapper, to selectively include/exclude certain modules or log levels that you are not interested in debugging atm. How to keep line numbers? No way? – bjornl Sep 13 '12 at 08:15
  • Possible duplicate of [A proper wrapper for console.log with correct line number?](http://stackoverflow.com/questions/13815640/a-proper-wrapper-for-console-log-with-correct-line-number) – Arctelix Oct 04 '15 at 01:40

8 Answers8

18

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");
Tim Down
  • 318,141
  • 75
  • 454
  • 536
8

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.

LeoR
  • 666
  • 1
  • 6
  • 20
8

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.

mpr
  • 3,250
  • 26
  • 44
  • Perfect. Needed a way to easily turn off debug logs, keep line numbers, and something that works both on site and within a service worker (which doesn't have 'window' which a lot of the libs like log4javascript uses). – Wollan Mar 24 '17 at 14:17
6

Crossposting from related question (A proper wrapper for console.log with correct line number?) 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
Community
  • 1
  • 1
drzaus
  • 24,171
  • 16
  • 142
  • 201
  • extractLineNumberFromStack() needs to be changed for Safari, because Safari positions it differently than Chrome. `var extractLineNumberFromStack = function (stack) { var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent); var line = stack.split('\n')[isSafari ? 1 : 3]; if (line && !isSafari) { line = (line.indexOf(' (') >= 0 ? line.split(' (')[1].substring(0, line.length - 1) : line.split('at ')[1]); } return line; };` – baohouse Jun 03 '15 at 23:27
  • Need to add 'trace' to the list of methods to emulate. – hiddentao May 16 '16 at 03:26
  • 1
    @hiddentao if you need it, tack it on to the last line. The `console` API actually has a bunch more stuff -- https://developer.chrome.com/devtools/docs/console-api. I just went with the "usuals". – drzaus May 18 '16 at 17:46
3

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

Frison Alexander
  • 3,228
  • 2
  • 29
  • 32
  • Hmm. That should work, as long as the JavaScript files are not compiled together in one big file. But it seems like they are on this too. – Paul Weber Sep 11 '14 at 07:55
2

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

manikanta
  • 8,100
  • 5
  • 59
  • 66
2

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!')
Community
  • 1
  • 1
Arctelix
  • 4,478
  • 3
  • 27
  • 38
  • Little late to the question, but is it possible to (in the debug wrapper) inject a line or characters to the output of the core console.log? – Barry Chapman Mar 19 '19 at 05:15
  • You most certainly can. In the example above i am prefixing all console methods (including log) with whatever is passed to the klass parameter... – Arctelix Mar 22 '19 at 03:34
  • Prefixing console methods the way you do it makes string substitution stop working, as they will be contained in the SECOND argument to the function, not the first. Don't know well how to avoid this using the `bind()` method. – Raúl Núñez de Arenas Coronado Feb 09 '23 at 20:11
-2

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)