Angular isolated debug logger
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.log
s 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
- Enable/Disable library logger using a boolean flag from the app that uses the library.
- If the flag is not defined a default "no log" behavior must be used.
- 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!