Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Justin Mclean
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Justin Mclean@JustinMclean )

Implementing A $log-Inspired Logging Service In Angular 2 RC 4

By Ben Nadel on

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:


 
 
 

 
 Creating a log service in Angular 2 RC 4. 
 
 
 

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.




Reader Comments

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.

Reply to this Comment

@Jefferson,

True; but, the ExceptionHandler class just handles... exceptions :) A logging service would let you log anything safetly.

Reply to this Comment

@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.

Reply to this Comment

@Juri,

Ahh, great minds think alike :D I have to admit I was pretty shocked when this wasn't part of the NG2 core!

Reply to this Comment

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 ?

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.