Matas Vaitkevicius Matas Vaitkevicius - 1 month ago 7
Javascript Question

How to disable console.log messages based on criteria from specific javascript source (method, file) or message contents

I am working on project that uses quite a few js libraries and one of them is outputting awful lot into console, it is polluting the airwaves so bad that it makes it hard to debug....

I know how to disable logging completely by overriding

console.log
with this,

(function (original) {
console.enableLogging = function () {
console.log = original;
};
console.disableLogging = function () {
console.log = function () {};
};
})(console.log);


but how do it do that per source(file/url) of where message originated?

Answer

Preamble

The beginning discusses how stuff works in general. If you just care for the code, skip Introduction and scroll to the Solution heading.

Introduction

Problem:

there is a lot of console noise in a web application. A significant amount of that noise is coming from third party code which we do not have access to. Some of the log noise might be coming from our code, as well.

Requirement:

reduce the noise by stopping the log. Some logs should still be kept and the decision about those should be decoupled from the code that is doing the logging. The granularity needed is "per-file". We should be able to choose which files do or do not add log messages. Finally, this will not be used in production code.

Assumption: this will be ran in a developer controlled browser. In that case, I will not focus on backwards compatibility.

Prior work:

First off logging can be enabled/disabled globally using this

(function (original) {
    console.enableLogging = function () {
        console.log = original;
    };
    console.disableLogging = function () {
        console.log = function () {};
    };
})(console.log);

(code posted in the question but also here for reference)

  • However, that does not allow for any granularity.
  • This could be modified to work on only specific modules but that cannot be done for third party code.
  • A mixed approach would be to disable logging globally but enable it in each of our modules. Problem there is that we have to modify each of our files and we will not get some potentially useful external messages.

A logging framework can be used but it might be an overkill. Although, to be honest, that's what I'd go for, I think, but it may need some integration into the product.

So, we need something light-weight-ish that has some configuration and does not need to be pretty.

Proposal:

The Loginator (title subject to change)

Let's start with the basics - we already know we can override the global log function. We'll take that and work with it. But first, let's recognise that the console object supports more than just .log. There could be various logging functions used. So-o-o, let's disable all of them.

Silence everything

//shorthand for further code. 
function noop() {}

const savedFunctions = Object.keys(console)
  .reduce((memo, key) => {
    if(typeof console[key] == "function") {
      //keep a copy just in case we need it
      memo[key] = console[key];
      //de-fang any functions 
      console[key] = noop;
    }
    
    return memo;
  }, 
  {});

console.log("Hello?");
console.info("Hello-o-o-o?");
console.warn("Can anybody hear me?");
console.error("I guess there is nobody there...");

savedFunctions.log("MUAHAHAHA!")

This can obviously be improved but it showcases how any and ll logging can be stopped. In reality, console.error should probably be left and console.warn might be also useful. But this is not the be-all-and-end-all solution.

Next, since we can override console functionality...why not supply our own?

Custom logging

const originalLog = console.log;
console.log = function selectiveHearing() {
  if (arguments[0].indexOf("die") !== -1) {
    arguments[0] = "Have a nice day!";
    }
  return originalLog.apply(console, arguments)
}

console.log("Hello.");
console.log("My name is Inigo Montoya.");
console.log("You killed my father.");
console.log("Prepare to die.");

That is all the tools we need to roll our own mini-logging framework.

How to do selective logging

The only thing missing is to determine which file something is coming from. We just need a stack trace.

// The magic
console.log(new Error().stack);

/* SAMPLE:

Error
    at Object.module.exports.request (/home/vagrant/src/kumascript/lib/kumascript/caching.js:366:17)
    at attempt (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:180:24)
    at ks_utils.Class.get (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:194:9)
    at /home/vagrant/src/kumascript/lib/kumascript/macros.js:282:24
    at /home/vagrant/src/kumascript/node_modules/async/lib/async.js:118:13
    at Array.forEach (native)
    at _each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:39:24)
    at Object.async.each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:117:9)
    at ks_utils.Class.reloadTemplates (/home/vagrant/src/kumascript/lib/kumascript/macros.js:281:19)
    at ks_utils.Class.process (/home/vagrant/src/kumascript/lib/kumascript/macros.js:217:15)
*/

(Relevant bit copied here.)

True, there are some better ways to do it but not a lot. It would either require a framework or it's browser specific - error stacks are not officially supported but they work in Chrome, Edge, and Firefox. Also, come on - it's literally one line - we want simple and don't mind dirty, so I'm happy for the tradeoff.

Solution

Putting it all together. Warning: Do NOT use this in production

(function(whitelist = [], functionsToPreserve = ["error"]) {
  function noop() {}

  //ensure we KNOW that there is a log function here, just in case
  const savedFunctions = { log: console.log }
        
  //proceed with nuking the rest of the chattiness away
  Object.keys(console)
    .reduce((memo, key) => {
      if(typeof console[key] == "function" && functionsToPreserve.indexOf(key) != -1 ) {
        memo[key] = console[key];
        console[key] = noop;
      }
    
      return memo;
    }, 
    savedFunctions); //<- it's a const so we can't re-assign it. Besides, we don't need to, if we use it as a seed for reduce()
  
  console.log = function customLog() {
    //index 0 - the error message
    //index 1 - this function
    //index 2 - the calling function, i.e., the actual one that did console.log()
    const callingFile = new Error().stack.split("\n")[2];
    
    if (whitelist.some(entry => callingFile.includes(entry))) {
      savedFunctions.log.apply(console, arguments)
    }
  }

})(["myFile.js"]) //hey, it's SOMEWHAT configurable

Or a blacklist

(function(blacklist = [], functionsToPreserve = ["error"]) {
  function noop() {}

  //ensure we KNOW that there is a log function here, just in case
  const savedFunctions = { log: console.log }
    
  //proceed with nuking the rest of the chattiness away
  Object.keys(console)
.reduce((memo, key) => {
  if(typeof console[key] == "function" && functionsToPreserve.indexOf(key) != -1 ) {
    memo[key] = console[key];
    console[key] = noop;
  }

  return memo;
}, 
savedFunctions); //<- it's a const so we can't re-assign it. Besides, we don't need to, if we use it as a seed for reduce()
  
  console.log = function customLog() {
//index 0 - the error message
//index 1 - this function
//index 2 - the calling function, i.e., the actual one that did console.log()
const callingFile = new Error().stack.split("\n")[2];

if (blacklist.some(entry => callingFile.includes(entry))) {
 return;
} else {
 savedFunctions.log.apply(console, arguments);
}
  }

})(["myFile.js"])

So, this is a custom logger. Sure, it's not perfect but it will do the job. And, hey, since the whitelisting is a bit loose, it could be turned to an advantage:

  • to whitelist a bunch of files that share a substring, say, all myApp can include myApp1.js, myApp2.js, and myApp3.js.
  • although if you want specific files, you can just pass the full name, including extension. I doubt there would be a bunch of duplicate filenames.
  • finally, the stack trace will include the name of the calling function, if any, so you can actually just pass that and that will whitelist on per-function basis. However, it relies on the function having a name and it's more likely for function names to clash, so use with care

Other than that, there can certainly be improvements but that is the basis of it. The info/warn methods can also be overriden, for example.

So, this, if used, should only be in dev builds. There are a lot of ways to make it not go into production, so I won't discuss them but here is one thing I can mention: you can also use this anywhere if you save it as a bookmarklet

javascript:!function(){function c(){}var a=arguments.length<=0||void 0===arguments[0]?[]:arguments[0],b=arguments.length<=1||void 0===arguments[1]?["error"]:arguments[1],d={log:console.log};Object.keys(console).reduce(function(a,d){return"function"==typeof console[d]&&b.indexOf(d)!=-1&&(a[d]=console[d],console[d]=c),a},d),console.log=function(){var c=(new Error).stack.split("\n")[2];a.some(function(a){return c.includes(a)})&&d.log.apply(console,arguments)}}(["myFile.js"]);

This is it minified (although I passed it through Babel first, to use ES5 minification) and still configurable, to an extent, as you can change the very end where you can pass the whitelist. But other than that, it will work the same and is completely decoupled from the codebase. It will not run at pageload but if that's needed you can either use this as a userscript (still decoupled) or include it before other JS files in dev/debug builds only.

A note here - this will work in Chrome, Edge and Firefox. It's all the latest browsers, so I assume a developer will use at least one of them. The question is tagged as Chrome but I decided to widen the support. A Chrome only solution could work slightly better but it's not really a big loss of functionality.