Implementing A $log-Inspired Logging Service In Angular 2 RC 4
As I've been digging into Angular 2 over the past several months, one of the features that seems oddly absent is a basic logging service. In Angular 1.x, we had the $log service that could be safely invoked whether or not the underlying "console" object existed. In Angular 2, now that we have a multi-platform experience (think NativeScript, think Universal JavaScript), it would seem that a basic platform-safe logging service is more important than ever. As such, I wanted to take a run at implementing one for Angular 2 RC 4.
Run this demo in my JavaScript Demos project on GitHub.
Unlike many business-oriented services that you might build in an Angular 2 application, basic logging is tied to the platform, not the application. As such, it has to be provided at the platform level, not the application level. And, in an Angular 2 application, that means that we have to provide it during the bootstrapping process.
One byproduct of this configuration is that the application can't know which implementation it's receiving during dependency-injection (DI); is it getting the one that works in the browser? the one that works in Node.js? the one that works in NativeScript? It doesn't care - it just asks for the "logger" and the one provided to the platform is the one that gets injected.
In order to keep the dependency-injection simple in NG2 TypeScript, the type annotation for the dependency has to be a Class. This means that we have to have some default implementation for the logger class that can be used both as the DI token and as the override-hook during bootstrapping. For this, I've created a shell class that does nothing but implement empty methods:
// Define the interface that all loggers must implement.
export interface ILogger {
assert( ...args: any[] ) : void;
error( ...args: any[] ) : void;
group( ...args: any[] ) : void;
groupEnd( ...args: any[] ) : void;
info( ...args: any[] ) : void;
log( ...args: any[] ) : void;
warn( ...args: any[] ) : void;
}
// Set up the default logger. The default logger doesn't actually log anything; but, it
// provides the Dependency-Injection (DI) token that the rest of the application can use
// for dependency resolution. Each platform can then override this with a platform-
// specific logger implementation, like the ConsoleLogService (below).
export class Logger implements ILogger {
public assert( ...args: any[] ) : void {
// ... the default logger does no work.
}
public error( ...args: any[] ) : void {
// ... the default logger does no work.
}
public group( ...args: any[] ) : void {
// ... the default logger does no work.
}
public groupEnd( ...args: any[] ) : void {
// ... the default logger does no work.
}
public info( ...args: any[] ) : void {
// ... the default logger does no work.
}
public log( ...args: any[] ) : void {
// ... the default logger does no work.
}
public warn( ...args: any[] ) : void {
// ... the default logger does no work.
}
}
As you can see, this default-log module defines both the logger interface and the default implementation of the logger service. Once we have this class, we can use it as the DI-token during bootstrapping. For example, in the following main.ts, we're telling Angular to use our custom, browser-specific logging implementation as the class for the Logger DI token:
// Import the core angular services.
import { bootstrap } from "@angular/platform-browser-dynamic";
// Import the application components and services.
import { AppComponent } from "./app.component";
import { ConsoleLogService } from "./log.service";
import { Logger } from "./default-log.service";
bootstrap(
AppComponent,
// In the browser platform, we're going to use the ConsoleLogService as the
// implementation of the Logger service. This way, when application components
// inject "Logger" DI token, they'll actually receive "ConsoleLogService".
[
{
provide: Logger,
useClass: ConsoleLogService
}
]
);
This tells Angular to provide the cached "ConsoleLogService" class instance any time a component or service in the application requests "Logger" as a dependency. Our ConsoleLogService class adheres to the Logger interface but provides an implementation that logs to the browser console:
// Declare the console as an ambient value so that TypeScript doesn't complain.
declare var console: any;
// Import the application components and services.
import { ILogger } from "./default-log.service";
// I log values to the ambient console object.
export class ConsoleLogService implements ILogger {
public assert( ...args: any[] ) : void {
( console && console.assert ) && console.assert( ...args );
}
public error( ...args: any[] ) : void {
( console && console.error ) && console.error( ...args );
}
public group( ...args: any[] ) : void {
( console && console.group ) && console.group( ...args );
}
public groupEnd( ...args: any[] ) : void {
( console && console.groupEnd ) && console.groupEnd( ...args );
}
public info( ...args: any[] ) : void {
( console && console.info ) && console.info( ...args );
}
public log( ...args: any[] ) : void {
( console && console.log ) && console.log( ...args );
}
public warn( ...args: any[] ) : void {
( console && console.warn ) && console.warn( ...args );
}
}
Even though we know this is the implementation for the browser platform, I'm still including guard-statements for the existence of the "console" and the log-level methods. This way, we can ensure that the logging service never throws an error regardless of random cross-browser differences in the console implementation.
And, once we have this ConsoleLogService being provided as the Logger implementation, we can now inject it into our application using the Logger DI token:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application components and services.
import { Logger } from "./default-log.service";
@Component({
selector: "my-app",
template:
`
<ul>
<li><a (click)="test( 'log' )">Log something.</a></li>
<li><a (click)="test( 'error' )">Error something.</a></li>
<li><a (click)="test( 'info' )">Info something.</a></li>
<li><a (click)="test( 'warn' )">Warn something.</a></li>
<li><a (click)="testGroup()">Group something.</a></li>
</ul>
`
})
export class AppComponent {
private logger: Logger;
// I initialize the component.
// --
// NOTE: Even though we are requesting the class of TYPE "Logger", we're actually
// going to receive the instance of ConsoleLogService since that is being overridden
// at the platform level (ie, in the bootstrapping).
constructor( logger: Logger ) {
this.logger = logger;
}
// ---
// PUBLIC METHODS.
// ---
// I test the basic log levels of the logger.
public test( level: string ) : void {
this.logger[ level ]( "Dang, logService.%s() is kind of cool!", level );
}
// I test the grouping of log output.
public testGroup() : void {
this.logger.group( "Group Test" );
this.logger.log( "Inside a group." );
this.logger.error( "Inside a group." );
this.logger.info( "Inside a group." );
this.logger.warn( "Inside a group." );
this.logger.groupEnd();
}
}
Here, the root component is requiring the Logger service, which the platform bootstrapper associates with our custom ConsoleLogService. Therefore, when we go to invoke the method on the user-interface (UI), it ends up logging to the browser console:
I'm still very much trying to wrap my head around a multi-platform, or should I say platform-agnostic architecture; when stuff just needed to work "in the browser," it was a much more simple mental model. Now that stuff has to work "everywhere," you need to start thinking about what is application-specific and what is platform-specific. Something like a logging service seem, at least ot me, a platform-specific concern. That said, I'm still very unsure about many aspects of this facet of Angular 2 development; so, take this post from that perspective.
Want to use code from this post? Check out the license.
Reader Comments
Ooh, very cool. Can't wait to dig into this a bit more :)
@Sam,
Awesome - hope you find it interesting :)
Angular 2 has the built-in ExceptionHandler class. https://angular.io/docs/ts/latest/api/core/index/ExceptionHandler-class.html
Thanks for the post! I think it's a matter of preference, but with TypeScript you could use abstract classes to define the Logger (https://goo.gl/YEQOWG).
The abstract Logger class can serve two roles at the same time - define a contract for the concrete logger implementations, and be a token for Angular DI.
Awesome. I opened up an issue nearly a year ago just about this: https://github.com/angular/angular/issues/5458
Nice article!
@Jefferson,
True; but, the ExceptionHandler class just handles... exceptions :) A logging service would let you log anything safetly.
@Anton,
Ah, very interesting! I'm relatively new to TypeScript -- just learning with Angular 2; so, I don't really have the best handle on the breathe of the functionality yet. This is a great suggestion.
That said, this may cause an issue then if someone goes to require the logger without providing a concrete implementation. At least, with a non-abstract base-class, it's always safe to require it (although it may do nothing). So, maybe that's an invalid use-case anyway :P
Anyway, great idea and TS tip.
@Juri,
Ahh, great minds think alike :D I have to admit I was pretty shocked when this wasn't part of the NG2 core!
Hi Ben,
Thanks for the post. I want to know if this work with the Angular2 beta 15 version. I tried it but it doesnot print the message to the browser console ?
@Ben: Great inspiration.
There is actually a log example on the official angular.io website.
https://angular.io/docs/ts/latest/cookbook/dependency-injection.html#!#class-interface
They use a concept which is called class-interfaces. By that you can use the strong typing of a logger interface as a provider token and you do not have to implement a default logger class. The concrete logger type does not have to extend from a base class, it just has to implement the abstract class . So there is a slot free for a further base class.
As a former java developer that sounds crazy, but in Typescript there seem to be many possible ways ;-)
@Ben Simple but very practical solution , thank you for this !. We can also push the logs to the server side via this approach.
Hey Ben,
I am using angular version 2.4.
I am getting an unhandled promise rejection error in the service where I inject the Logger.
Is there a work around.
Appreciate the help.
Thanks,
Kaushal Shah
Update,
When you try to use the Logger inside a catch method the 'this' response method becomes null. So it throws an error.
You can use a static instance of the Logger in such cases.
Awesome post, thank you. I am relatively new to typescript, and used this approach to implement the logging service (with some modifications for log levels) however it is still weird that angular doesn't support it in its libs.
@Milad,
Glad you like. And I agree, seems odd that it's not provided. Perhaps it has to do with the fact that Angular can run in different environments now? That said, Node.js has a "console" logger; so, not really sure what the thinking is.
@Kaushal,
If you're losing the "this" context in a catch-handler, consider using a fat-arrow method:
.catch( ( error ) => { .... this should work here .... } )
That should maintain the "this" context.
@Tarun,
Yo, 100%. You can pipe them to the server; or, you can even pipe them so something like NewRelic, RayGun, etc. I have another article you might like on creating a custom ErrorHandler for that kind of situation:
www.bennadel.com/blog/3138-creating-a-custom-errorhandler-in-angular-2-rc-6.htm
... which can hook into the Angular error-handling workflow and then propagate errors elsewhere.
Your blog is very nice... Thanks for sharing your information...
Your blog is very nice... Thanks for sharing your information...
your blog is very nice thanks for sharing information.
That said, this may cause an issue then if someone goes to require the logger without providing a concrete implementation.