Skip to main content
Ben Nadel at the NYC Node.js Meetup (Sep. 2018) with: Manon Metais
Ben Nadel at the NYC Node.js Meetup (Sep. 2018) with: Manon Metais

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

By
Published in Comments (20)

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.

Want to use code from this post? Check out the license.

Reader Comments

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

15,902 Comments

@Jefferson,

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

15,902 Comments

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

1 Comments

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 ?

1 Comments

@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 ;-)

2 Comments

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

2 Comments

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.

1 Comments

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.

15,902 Comments

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

15,902 Comments

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

1 Comments

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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel