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
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With