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:
- GitHub: angular-wijmo-flexsheet
References:
- Angular Docs: Angular Style Guide
- Angular Docs: Architecture Overview
- Angular Docs: Dependency Injection
- Apache [dot] org: Log4j 2 API
- Developer Mozilla [dot] org: getter
- Developer Mozilla [dot] org: bind()
Resources:
- GitHub Angular: feat(core): introduce Logger Service #17399
- stackoverflow: angularjs $log - show line number
- Plunker: AngularJS: $log and Actual Line Number With Explanantion
- GitHub kaop-ts: Simple Yet Powerful Library of ES2016 Decorators
- TypeScript Handbook: Functions
Additional Resources:
- GitHub: loglevel - Minimal lightweight logging for JavaScript
- GitHub: Log4js - The Logging Framework for JavaScript
- GitHub: debug - A tiny JavaScript debugging utility
- GitHub: winston - A logger for just about everything
- GitHub: node-bunyan - A simple and fast JSON logging module
- GitHub: roarr - The perfect JSON logger