A simple logging service for Angular 4

In the previous post, I described two Angular 4 component communication scenarios used to enable sibling component interaction.

In this post, I'll walk you through the steps I followed when adding support for a simple Angular 4 logging service that uses dependency injection.

Getting Started

You've probably heard the expression:

highly cohesive, loosely coupled

When building applications, dependency injection is one of the tools we can use to help us achieve loose coupling. Angular has its own dependency injection framework called Angular DI.

The Logger Service

Let's take a look at the LoggerService (logger.service.ts):

import { Injectable } from '@angular/core';

export abstract class Logger {

  info: any;
  warn: any;
  error: any;
}

@Injectable()
export class LoggerService implements Logger {

  info: any;
  warn: any;
  error: any;
  
  invokeConsoleMethod(type: string, args?: any): void {}  
}

We start by defining a simple logging interface with three properties: info, warn and error. Then we provide a default implementation of the logger service.

The @Injectable() decorator marks the LoggerService as available to an injector for instantiation.

The Application Module

Let's take a look at the AppModule (app.module.ts):

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ...   
  ],
  providers: [ { provide: LoggerService, useClass: ConsoleLoggerService } ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

We want our logging service to be available to all the application's components, so we need to register it in the AppModule's providers array. We also want the default implementation of the logger service to be replaced by the console logger service (see below).

The Console Logger Service

Let's take a look at the implementation of the ConsoleLoggerService (console-logger.service.ts):

import { Injectable } from '@angular/core';
import { environment } from '../../../environments/environment';

import { Logger } from './logger.service';

export let isDebugMode = environment.isDebugMode;

const noop = (): any => undefined;

@Injectable()
export class ConsoleLoggerService implements Logger {

  get info() {
    if (isDebugMode) {
      return console.info.bind(console);
    } else {
      return noop;
    }
  }

  get warn() {
    if (isDebugMode) {
      return console.warn.bind(console);
    } else {
      return noop;
    }
  }

  get error() {
    if (isDebugMode) {
      return console.error.bind(console);
    } else {
      return noop;
    }
  }
  
  invokeConsoleMethod(type: string, args?: any): void {
    const logFn: Function = (console)[type] || console.log || noop;
    logFn.apply(console, [args]);
  }
}

The ConsoleLoggerService imports the (Angular CLI managed) environment file:

import { environment } from '../../../environments/environment';

And initialises the service's debug mode flag:

export let isDebugMode = environment.isDebugMode;

Note: You could update the isDebugMode flag at runtime via a cookie or a query string.

For each of the properties (info, warn and error) we define a getter that uses the bind() method to create a bound function. When the associated property is looked up, it executes the passed-in function in the given context (so we log the correct source file name and line number) and passes along any arguments.

The Application Component

Let's take a look at the AppComponent (app.component.ts):

import { Component } from '@angular/core';

import { LoggerService } from './services/log4ts/logger.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  constructor(private logger: LoggerService) {
  
    // Incorrect source file name and line number :(
    logger.invokeConsoleMethod( 'info', 'AppComponent: logger.invokeConsoleMethod()');
    logger.invokeConsoleMethod( 'warn', 'AppComponent: logger.invokeConsoleMethod()');
    logger.invokeConsoleMethod( 'error', 'AppComponent: logger.invokeConsoleMethod()');

    // Correct source file name and line number :)
    logger.info('AppComponent: logger.info()');
    logger.warn('AppComponent: logger.warn()');
    logger.error('AppComponent: logger.error()');
  };
}

To use our simple logging service, the AppComponent needs to import our default logger service implementation:

import { LoggerService } from './services/log4ts/logger.service';

It also needs to include a constructor parameter that has the type LoggerService:

  constructor(private logger: LoggerService) {
    ...
  };

Now, because we registered the console logger service in the AppModule's providers array, the ConsoleLoggerService (and not the LoggerService) will be injected into our component:

The Sources panel:

Source Code:
References:
Resources:
Additional Resources: