Better JS Logger for Debugging

Category:

As web developers we really like putting console.log all over the place when debugging our applications, although the Chrome dev tools come with an actual debugger that can be started by simply writing debugger in your code. This gets messy rather fast, especially if you simply log the objects without an accompanying message. What I like to do is prepend the message with the class and function name so I can easily filter the message I'm looking for. It turns out you can actually automate that when using Chrome or Firefox and use colors on top!

stacklogger console chrome

#Stacklogger

I created an npm package called Stacklogger for everyone to use by running npm install stacklogger --save. You just import it, and call its own log function as you would with console.log. If you already have logging with console.log in place, just call its hookConsoleLog function and every console.log is redirected to stacklogger's custom log function. Check the npm readme if you want to use it, or see its source code on GitHub I included a small example that produces the above output in chrome:

import log, { hookConsoleLog } from 'stacklogger'

class ExampleLog {
  constructor () {
    this.obj = {hello: 'world', anotherKey: [0, 1]}
    this.arr = [1, 3, 5, 7, 9]
  }
  hello () {
    log('Logging some text with log()', this.obj, this.arr)
  }
}

class ExampleConsoleLog {
  hello () {
    console.log('Called with console.log')
  }
}

let e1 = new ExampleLog()
let e2 = new ExampleConsoleLog()

e1.hello()
console.log('standard console.log without the hook')
hookConsoleLog()
console.log('console.log hooked now')
e2.hello()

#How does it work?

#Stack trace

Remember how when you throw an Error in JS, it prints the whole stack trace? What we do in the log function is to simply create a new Error object. Unfortunately, the stacktrace is not a well structured object, but just a string. The concrete stack trace string is even different for each JS engine, so I created two regex(es?) to parse them in Chrome's V8 engine and in Firefox. If you use Firefox be aware that it uses the file name as its class name as that's the only available information there. stacklogger console firefox

#Hooking console.log()

Redirecting the calls from console.log to log is really easy in JS as it allows you to just overwrite every property of objects. First we save a reference to the original console.log and then redefine console.log:

const consoleLog = console.log.bind(console)
function log () {
  consoleLog('Hooked.', ...arguments)
}
function hookConsoleLog () {
  console.log = log
}

(Firefox is sad when a logging function doesn't run in the console context, so we bind(console) the reference.)

#Unique colors for each class

You can use CSS properties in the log to change the foreground/background color by simply passing them as an additional argument to console.log. It would also be nice if each class had its own color, so when looking for a specific debug message of a class, you only have to quickly look through the console's output and pattern match with that color. For this, I implemented a simple function to hash a string to an integer and use that hash as an index in a color array.

#Source Code in 27 lines

The source code of the whole stacklogger is really short:

const consoleLog = console.log.bind(console)

const chromeRegex = new RegExp('^\\s*?at\\s*(\\S*?)\\s')
const firefoxRegex = new RegExp('^\\s*(\\S*?)@\\S*\\/(\\S*)\\.')
export default function log () {
  let stackframe = (new Error()).stack.split('\n')
  // try to match chrome first
  let match = chromeRegex.exec(stackframe[2])
  let callee = match ? match[1] : null
  if (!callee) { // try firefox
    match = firefoxRegex.exec(stackframe[1])
    callee = match ? `${match[2]}.${match[1]}` : ''
  }
  let className = callee.split('.')[0]
  // make a certain className always have the same background color by computing a hash on it
  let hash = getHashCode(className) % colors.length
  consoleLog(`%c${callee}`, `color: #000; background: ${colors[hash]}`, ...arguments)
}

const getHashCode = s => s.split('').reduce((prevHash, curChar) => prevHash * 31 + curChar.charCodeAt(0), 0)

export function hookConsoleLog () {
  console.log = log
}

// colors created by enumerating HSL color wheel from 0...360 in 30 degree steps, with luminosity 75 and 90
const colors = ['#ff8080', '#ffcccc', '#ffbf80', '#ffe6cc', '#ffff80', '#ffffcc', '#bfff80', '#e6ffcc', '#80ff80', '#ccffcc', '#80ffbf', '#ccffe6', '#80ffff', '#ccffff', '#80bfff', '#cce5ff', '#8080ff', '#ccccff', '#bf80ff', '#e5ccff', '#ff80ff', '#ffccff', '#ff80bf', '#ffcce6']