Taking Over console.log

Let's say you want to intercept all calls to console.log, console.warn, and console.error, do something sneaky, and then proxy the call back to the original methods so that the messages get printed out as normal and no one ever has to notice. How would you do that?

Attempt #1

If you are a seasoned Javascript programmer, you would probably go to monkeypatching and maybe write something like

function takeOverConsole(){
    var original = window.console
    window.console = {
        log: function(){
            // do sneaky stuff
            original.log.apply(original, arguments)
        }
        , warn: function(){
            // do sneaky stuff
            original.warn.apply(original, arguments)
        }
        , error: function(){
            // do sneaky stuff
            original.error.apply(original, arguments)
        }
    }
}

This works on all browsers except for IE. On IE, console.log is implemented as a native method, and as such doesn't support the apply method. This is illustrated if you try to run the following in IE

> console.log.apply(console, ['blah']) 
"Object doesn't support property or method 'apply'" 

Attempt #2

What to do? Well, for the case of console.log, we can punt on passing through the variable length arguments exactly as they are, because we already know what the console.log is going to do with them: join them. Actually the way the join happens varies by browser, but let's just do everyone a favor and make them consistent here by joining them with a space as the separator. Long story short, I ended up with

function takeOverConsole(){
    var original = window.console
    function handle(method, args){
        var message = Array.prototype.slice.apply(args).join(' ')
        // do sneaky stuff
        if (original) original[method](message)
    }
    window.console = {
        log: function(){
            handle('log', arguments)
        }
        , warn: function(){
            handle('warn', arguments)
        }
        , error: function(){
            handle('error', arguments)
        }
    }
}

This is works everywhere that I have tested, but there is one big flaw - the console object in Chrome and Firebug has more features than just log, warn and error: there're useful things like console.profile, console.timeStamp, console.trace, and lots more. My code effectively removes these extra features - not very gentlemanly.

Attempt #3

So, perhaps instead of replacing the console object, we should just replace the individual methods we want to intercept. I came up with this

function takeOverConsole(){
    var console = window.console
    if (!console) return
    function intercept(method){
        var original = console[method]
        console[method] = function(){
            var message = Array.prototype.slice.apply(arguments).join(' ')
            // do sneaky stuff
            original.call(console, message)
        }
    }
    var methods = ['log', 'warn', 'error']
    for (var i = 0; i < methods.length; i++)
        intercept(methods[i])
}

But this broke on IE again, on the line original.call(console, message). The function's call method, like apply, is not supported by console.log. However, curiously - unlike the other browsers - it can be called directly without having its context set to console, so we can say

original(message) // this works on IE but breaks on Chrome

So the solution as is so often the case is to do one thing on normal browsers, and do another on IE.

function takeOverConsole(){
    var console = window.console
    if (!console) return
    function intercept(method){
        var original = console[method]
        console[method] = function(){
            var message = Array.prototype.slice.apply(arguments).join(' ')
            // do sneaky stuff
            if (original.call){
                // Do this for normal browsers
                original.call(console, message)
            }else{
                // Do this for IE
                original(message)
            }
        }
    }
    var methods = ['log', 'warn', 'error']
    for (var i = 0; i < methods.length; i++)
        intercept(methods[i])
}

So that's how it's done! Or not: if you find a flaw in my code, please let me know in the comments!

Update: Attempt #4

Thanks to Jordan Reiter's comment

console.log does not really just concatenate; at least, it doesn't in Firefox, Safari, and Chrome. It keeps them as the objects, so you can expand them and see their children. It'll be immediately obvious that something has been changed. Fix is really easy, though -- IE does concatenate, so use original(message) for IE and original.apply(console, arguments) for Safari, Firefox, Chrome, etc. and then it will be undetectable.

Great catch, Jordan! The improved version is:

function takeOverConsole(){
    var console = window.console
    if (!console) return
    function intercept(method){
        var original = console[method]
        console[method] = function(){
            // do sneaky stuff
            if (original.apply){
                // Do this for normal browsers
                original.apply(console, arguments)
            }else{
                // Do this for IE
                var message = Array.prototype.slice.apply(arguments).join(' ')
                original(message)
            }
        }
    }
    var methods = ['log', 'warn', 'error']
    for (var i = 0; i < methods.length; i++)
        intercept(methods[i])
}
blog comments powered by Disqus