Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add dynamic values to the console methods at run-time with preservation of original call position and line number intact

I made the following class to 'hijack' the console.log function. The reason behind this is that I want to add and remove values dynamically. It will be used for debug purposes, so the origin of the function call console.log() is important. In the following code I will explain my logic in the comments.

export class ConsoleLog {
  private _isActive = false;
  private _nativeLogFn: any;

  constructor() {

    // ----------------------
    // Store the native console.log function, so it can be restored later 
    // ----------------------

    this._nativeLogFn = console.log;

  }

  public start() {
    if (!this._isActive) {

      // ----------------------
      // Create a new function as replacement for the native console.log 
      // function. *** This will be the subject of my question ***
      // ----------------------

      console.log = console.log.bind(console, Math.random());

      this._isActive = true;
    }
  }

  public stop() {
    if (this._isActive) {
      // Restore to native function
      console.log = this._nativeLogFn;
      this._isActive = false;
    }
  }
}

The problem with this setup is, that the new function is assigned in a static form.

// Function random() generates a number at the moment I assign the function. 
// Let's say it's the number *99* for example sake. 

console.log.bind(console, Math.random());

Every time the console.log(...) is called, it will output 99. So it's pretty much static. (To be ahead of you: no my goal isn't outputting a random number, lol, but I just use it to test if the output is dynamic or not.).

The annoying part is, using the function with the console.log.bind is the only way I found that actually preserves the origin caller and line number.

I wrote the following simple test.

    console.log('Before call, active?', 'no'); // native log

    obj.start();  // Calls start and replaces the console.log function

    console.log('foo'); // This will output 'our' 99 to the console.
    console.log('bar'); // This will output 'our' 99 again.

    obj.stop(); // Here we restore the native console.log function

    console.log('stop called, not active'); // native log again

    // Now if I call it again, the random number has changed. What is 
    // logical, because I re-assign the function.

    obj.start();  // Calls start and replaces the console.log function
    console.log('foo'); // This will output N to the console.
    // But then I have to call start/log/stop all the time. 

Question: How can I add values to the console.log at run time without losing the origin caller filename and line number... AND without bothering the library consumer once this class is initiated with start().

EDIT: Added a plkr: https://embed.plnkr.co/Zgrz1dRhSnu6OCEUmYN0

like image 964
therebelcoder Avatar asked May 11 '17 23:05

therebelcoder


1 Answers

Cost me the better part of the weekend and a lot of reading and fiddling, but I finally solved it leveraging the ES6 proxy object. Pretty powerful stuff I might add. Explanation is in the code. Please don't hesitate to improve on it or ask questions.

(EDITED based on @Bergi's comments) Here is the class:

export class ConsoleLog {
  private _isActive = false;
  private _nativeConsole: any;
  private _proxiedConsole: any;

  /**
   * The Proxy constructor takes two arguments, an initial Object that you
   * want to wrap with the proxy and a set of handler hooks.
   * In other words, Proxies return a new (proxy) object which wraps the
   * passed in object, but anything you do with either effects the other.
   *
   * ref: https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-3-proxies
   * ref: http://exploringjs.com/es6/ch_proxies.html#_intercepting-method-calls
   */

  /**
   * Challenge:
   * When we intercept a method call via a proxy, you can intercept the
   * operation 'get' (getting property values) and you can intercept the
   * operation 'apply' (calling a function), but there is no single operation
   * for method calls that you could intercept. That’s why we need to treat
   * them as two separate operations:
   *
   * First 'get' to retrieve a function, then an 'apply' to call that
   * function. Therefore intercepting 'get' and return a function that
   * executes the function 'call'.
   */

  private _createProxy(originalObj: Object) {

    const handler = {

      /**
       * 'get' is the trap-function.
       * It will be invoked instead of the original method.
       * e.a. console.log() will call: get(console, log) {}
       */
      get(target: object, property: string) {

        /**
         * In this case, we use the trap as an interceptor. Meaning:
         * We use this proxy as a sort of pre-function call.
         * Important: This won't get invoked until a call to a the actual
         * method is made.
         */

        /**
         * We grab the native method.
         * This is the native method/function of your original/target object.
         * e.a. console.log = console['log'] = target[property]
         * e.a. console.info = console['info'] = target[property]
         */
        const nativeFn: Function = target[property];

        /**
         * Here we bind the native method and add our dynamic content
         */
        return nativeFn.bind(
          this, `%cI have dynamic content: ${Math.random()}`, 'color:' +
          ' #f00;'
        );
      }
    };
    return new Proxy(originalObj, handler);
  }

  constructor() {
    // Store the native console.log function so we can put it back later
    this._nativeConsole = console;
    // Create a proxy for the console Object
    this._proxiedConsole = this._createProxy(console);
  }

  // ----------------------
  // (Public) methods
  // ----------------------

  public start() {
    if (!this._isActive) {
      /**
       * Replace the native console object with our proxied console object.
       */
      console = <Console>this._proxiedConsole;
      this._isActive = true;
    }
  }

  public stop() {
    if (this._isActive) {
      // Restore to native console object
      console = <Console>this._nativeConsole;
      this._isActive = false;
    }
  }
}

And here the code to see for yourself:

const c: ConsoleLog = new ConsoleLog();

console.log('Hi, I am a normal console.log', ['hello', 'world']);

c.start(); // Start - replaces the console with the proxy

console.log('Hi, I am a proxied console.log');
console.log('I have dynamic content added!');
console.log('My source file and line number are also intact');

c.stop(); // Stop - replaces the proxy back to the original.

console.log('I am a normal again');

Cheers!

like image 194
therebelcoder Avatar answered Nov 16 '22 00:11

therebelcoder