Angular isolated debug logger

Jose I Santa Cruz G
ITNEXT
Published in
4 min readMar 23, 2023

--

Photo by Fotis Fotopoulos on Unsplash

In the past weeks I've been working on @queplan/qp-suspense library, which is an Angular implementation of React's Suspense, heavily based on Netanel Basal's article and repo. To understand how some things were working I planted far to many console.logs inside the code(shame on me 😞 , as VS Code has a nice debugging interface (perhaps the linked article is a little outdated)). One of the projects that use qp-suspense has it's own logger, so in order to avoid colateral noise from one logger to another I had to isolate qp-suspense's logger.

Desirable scenario

  1. Enable/Disable library logger using a boolean flag from the app that uses the library.
  2. If the flag is not defined a default "no log" behavior must be used.
  3. Modifying the log should not overwrite log overrides made elsewhere.

Alias for the console

There are many articles that point to solutions such as:

const log = console.log.bind(console);

and repeat this same code (with some slight adjustments) for all console methods.

In out case, this will be as simple as:

const originalConsole = console;

as we don't need an alias for every method. We need an "alias" for the whole console.

When we want to disable the console, we'll just overwrite the desired methods to a noOp function:

const noOp = () => {};
const newConsole = originalConsole;
if (isConsoleDisabled) {
newConsole = {
log: noOp,
warn: noOp,
debug: noOp,
info: noOp,
trace: noOp
};
}

the error method is not overriden, as errors should be thrown.
So far so good, now, how do I use this in Angular?

InjectionTokens and DI to the rescue

One problem I faced when making @queplan/qp-suspense library was that in standalone components, services have a chance of not being singletons, even though they are declared as providedIn: 'root' singletons.

So to make sure that in every place we use a service (or in this case a function), we'll be using the same instance (a singleton), we'll use an InjectionToken:

/**
* noOp function used to disable some console methods.
*/
const _noOp = () => {};

/**
* Original console, just in case.
*/
const originalConsole = console;

/**
* InjectionToken to enable or disable the browser's console.
* True to enable, false to disable. Used as a `provider` in the app's
* `app.module`.
* ```
* providers: [
* {
* provide: DEBUG_SUSPENSE,
* useValue: false // true to enable
* },
* ],
* ```
*/
export let DEBUG_SUSPENSE = new InjectionToken<boolean>('DEBUG_SUSPENSE',{
providedIn: 'root',
factory: () => false
});

/**
* Enables or disables the browser's console.
* @param debug {boolean} true to enable the browser's console, false otherwise. Defaults to false..
* @returns {Console} the resulting console object (noOP for disabled methods).
*/
export const toggleConsole = (debug: boolean = false): Console => {
let newConsole: Console = originalConsole;
if (!debug) {
(newConsole as any) = {
log : _noOp,
warn : _noOp,
debug: _noOp,
info : _noOp,
};
}

return newConsole;
};

/**
* InjectionToken to expose the browser's console.
* This particular console will be under the library's context without affecting
* any other applications.
*/
export const SUSPENSE_LOG = new InjectionToken<Console>('SUSPENSE_LOG',
{
providedIn: 'root',
factory : () => toggleConsole(inject(DEBUG_SUSPENSE))
});

The code above is copied from the types.ts file of the library (with a freestyle translation of the documentation comments).
Important thing to take in mind:

  • DEBUG_SUSPENSE is defaulted to false, so no library logging will be shown if this provider is not defined in the app.
  • SUSPENSE_LOG will be the console you require, depending if you need logging or not.

The no likes (was How to use it)

Inside every component, service or any other Angular object, injecting this new console is required:

suspenseConsole = inject(SUSPENSE_LOG);
// suspenseConsole usage eg
constructor() {
this.suspenseConsole.log('Inside the constructor');
}

Works, for every console method (as expected as the newConsole / suspenseConsole is just a copy of the original console), keeps the line and file references, but it ain't pretty.
On a very personal preference, in a worst case I would like something like:

// suspenseConsole usage eg
constructor() {
suspenseConsole.log('Inside the constructor');
}

but that would require a static logger, and truth is I struggled a couple of hours without any success, so I applied this dirty trick (which I still don't like):

// Lots of other imports
import { EventService, SuspenseCacheService, YieldToMainService } from '@queplan/qp-suspense/services';

let suspenseConsole: Console;

// blah blah blah, documentation, decorator, etc
export class SuspenseComponent {
// more code

suspenseConsole = inject(SUSPENSE_LOG);
// suspenseConsole usage eg
constructor() {
suspenseConsole = this.suspenseConsole;
suspenseConsole.log('Inside the constructor');
}

// even more code
}

The suspenseConsole is assigned to a variable outside the class, so I could use it directly without referring to this
Not that this is a great gain, but it troubled my soul and now I may not be at ease, but the code looks nicer to my eyes.

Hope this article helps someone. Stay safe!

--

--

Polyglot senior software engineer, amateur guitar & bass player, geek, husband and dog father. Not precisely in that order.