Programmers frequently use console.log to record errors or other informational messages in their Angular applications. Although this is fine while debugging your application, it’s not a best practice for production applications. As Angular is all about services, it’s a better idea to create a logging service that you can call from other services and components. In this logging service, you can still call console.log, but you can also modify the service later to record messages to store them in local storage or a database table via the Web API.

In this article, you’ll build up the logging service in a series of steps. First, you create a simple log service class to log messages using console.log(). Next, you add some logging levels so you can report on debug, warning, error, and other types of log messages. Then you create a generic logging service class to call other classes to log to the console, local storage, and a Web API. Finally, you create a log publishing service that reads a JSON file to choose which log service classes to use.

A Simple Logging Service

To get started, create a very simple logging service that only logs to the console. The point here is to replace all of your console.log statements in your Angular application with calls to a service. Bring up an existing Angular application or create a new one. If you don’t already have one, add a folder named shared under the \src\app folder. Create a new TypeScript file named log.service.ts. Add the code shown in the following snippet.

import { Injectable } from '@angular/core';
@Injectable()
export class LogService {
  log(msg: any) {
    console.log(new Date() + ": "
      + JSON.stringify(msg));
  }
}

This code creates an injectable service that can be created by Angular and injected into any of your Angular classes. The log() method accepts a message that can be any type. A new date is created so each message can be logged to the console with the date and time attached to it. The date/time is not that important when just logging to the console, but once you start logging to local storage or to a database, you want the date/time attached so you know when the log messages was created. Notice the use of JSON.stringify around the msg parameter. This allows you to pass an object and it can be logged as a string.

For the purpose of following along with this article, create a new folder called \log-test and add a log-test.component.html page. Add a button to test the logging service.

<button (click)="testLog()">
  Log Test
</button>

Create a log-test.component.ts TypeScript file and add the code shown in Listing 1 to respond to the button click event.

Add a logger variable to your constructor so Angular can inject this service into this component. Notice that in the testLog() method you now call the this.logger.log() instead of console.log(). The result is the same (See Figure 1) in that the message appears in the console window. However, you’ve now given yourself the flexibility to log this message to local storage, to a database table, to the console, or to all three. And, the best part is, you don’t have to change any code in your application, other than the code in the LogService class. To use this service in your Angular application, you need to import it in your app.module.ts file. Also import the LogTestComponent you created as well.

import { LogService }
        from './shared/log.service';
import { LogTestComponent }
  from './log-test/log-test.component';

Add the LogService to the providers property in the @NgModule statement. Add the LogTestComponent class to the declarations property, as shown in the code snippet below.

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent,
                 LogTestComponent],
  bootstrap: [AppComponent],
  providers: [LogService]
})
export class AppModule { }

Add the <log-test></log-test> selector on one of your pages to display the Log Test button. Run the application and click on the Log Test button and you should see the message appear in the console window in the F12 tools of your browser, as shown in Figure 1.

Figure 1: Sample of using the LogService

Different Types of Logging

There are times when you might want only certain types of logging turned on when running your application. Many logging systems in other languages allow you to log debug messages, informational messages, warning messages, etc. Add this same ability to your LogService class by adding an enumeration and a property that you can set to control which messages to display. First, add a LogLevel enumeration in the log.service.ts file to keep track of what kind of logging to perform. Add this enumeration just after the import statement within the log.service.ts the file. Don’t add it within the LogService class.

export enum LogLevel {
  All = 0,
  Debug = 1,
  Info = 2,
  Warn = 3,
  Error = 4,
  Fatal = 5,
  Off = 6
}

Add a property to the LogService class named level that’s of the type LogLevel. Default the value to the All enumeration. While you are adding properties, add a Boolean property named logWithDate to specify whether you wish to add the date/time to the front of your messages or not.

level: LogLevel = LogLevel.All;
logWithDate: boolean = true;

Instead of having to set the level property prior to calling your logger.log() method, add the new methods debug, info, warn, error, and fatal to the LogService class (Listing 2). Each one of these methods calls a writeToLog() method passing in the message, the appropriate enumeration value and an optional parameter array. Delete the log() method you wrote previously and replace that one method with all the methods from Listing 2.

The optional parameter array means you can pass any parameters you want to be logged. For example, any of the following calls are valid.

this.logger.log("Test 2 Parameters",
                "Paul", "Smith");
this.logger.debug("Test Mixed Parameters",
                  true, false, "Paul", "Smith");
let values = ["1", "Paul", "Smith"];
this.logger.warn("Test String and Array",
                 "Some log entry", values);

The writeLog() method, Listing 3, checks the level passed by one of the methods against the value set in the level property. This level property is checked in the shouldLog() method. Both of these methods should now be added to your LogService class.

The shouldLog() method determines if logging should occur based on the level property set in the LogService class. This service is created as a singleton by Angular, so once this level property is set, it remains that value until you change it in your application. The shouldLog() checks the parameter passed in against the level property set in the LogService class. If the level passed in is greater than or equal to the level property, and logging is not turned off, then a true value is returned from this method. A true return value tells the writeToLog() method to log the message.

private shouldLog(level: LogLevel): boolean {
  let ret: boolean = false;
  if ((level >= this.level &&
       level !== LogLevel.Off) ||
       this.level === LogLevel.All) {
    ret = true;
  }
  return ret;
}

There’s one more method call in the writeToLog() method called formatParams(). This method is used to create a comma-delimited list of the parameter array. If all parameters in the array are simple data types and not an object, then the local variable named ret is returned after the join() method is used to create a comma-delimited list from the array. If there is one object, loop through each of the items in the params array and build the ret variable using the JSON.stringify() method to convert each parameter to a string, and then append a comma after each.

private formatParams(params: any[]): string {
  let ret: string = params.join(",");
  // Is there at least one object in the array?
  if (params.some(p => typeof p == "object")) {
    ret = "";
    // Build comma-delimited string
    for (let item of params) {
      ret += JSON.stringify(item) + ",";
    }
  }
  return ret;
}

Create Log Entry Class

Instead of building a string of the log information, and formatting the parameters in the writeToLog() method, create a class named LogEntry to do all this for you. Place this new class within the log.service.ts file. The LogEntry class, shown in Listing 4, has properties for the date of the log entry, the message to log, the log level, an array of extra info to log, and a Boolean you set to specify to include the date with the log message.

The buildLogString() method is similar to what you wrote in the writeToLog() method earlier. This method gathers the values from the properties of this class and returns them in one long string that can be used to output to the console window. Remove the formatParams() method from the LogService class after you’ve built the LogEntry class. You’re going to use this LogEntry class from each of the different logging classes you build in the rest of this article.

Now that you have this LogEntry class built, and have removed the formatParams() from the LogService class, rewrite the writeToLog() method to look like the code below.

private writeToLog(msg: string,
                   level: LogLevel,
                   params: any[]) {
  if (this.shouldLog(level)) {
    let entry: LogEntry = new LogEntry();
    entry.message = msg;
    entry.level = level;
    entry.extraInfo = params;
    entry.logWithDate = this.logWithDate;
    console.log(entry.buildLogString());
  }
}

After modifying the writeToLog() method, rerun the application and you should still see your log messages being displayed in the console window.

Log Publishing System

When logging exceptions, or any kind of message, it’s a good idea to write those log entries to different locations in case one of those locations isn’t accessible. In this way, you stand a better chance of not losing any messages. To do this, you need to create three different classes for logging. The first publisher is a LogConsole class that logs to the console window. The second publisher is LogLocalStorage to log messages to Web local storage. The third publisher is LogWebApi for calling a Web API to log messages to a backend table in a database.

Instead of hard-coding each of these classes in the LogService class, you’re going to create a publishers property (Figure 2), which is an array of an abstract class called LogPublisher. Each of the logging classes you create extends this abstract class.

Figure 2: Create an abstract class from which all your log publishers inherit.

The LogPublisher class contains one property named location. This property is used to set the key for local storage and the URL for the Web API. This class also needs two methods: log() and clear(). The log() method is overridden in each class that extends LogPublisher and is responsible for performing the logging. The clear() method removes all log entries from the data store.

Add a new TypeScript file named log-publishers.ts to the \shared folder in your project. You’re going to need a few import statements at the top of this file. In fact, you’re going to add more later, but for now, just add Observable, the Observable of, and LogEntry classes. Write the code shown in the next snippet to create your abstract LogPublisher class.

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { LogEntry } from './log.service';
export abstract class LogPublisher {
  location: string;
  abstract log(record: LogEntry):
             Observable<boolean>
  abstract clear(): Observable<boolean>;
}

Now that you have the template for creating each log publishing class, let’s start building them.

Log to Console

The first class you create to extend the LogPublisher class writes messages to the console. You’re eventually going to remove the call to console.log() from the LogService class you created earlier. LogConsole is a very simple class that displays log data to the console window using console.log(). Add the following code below the LogPublisher class in the log-publisher.ts file.

export class LogConsole extends LogPublisher {
  log(entry: LogEntry): Observable<boolean> {
    // Log to console
    console.log(entry.buildLogString());
    return Observable.of(true);
  }
  clear(): Observable<boolean> {
    console.clear();
    return Observable.of(true);
  }
}

Notice that the log() method in this class accepts an instance of the LogEntry class. This parameter coming in, named entry, calls the buildLogString() method to create a string of the complete log entry data to be displayed to the console window. Each log() method needs to return an observable of the type Boolean back to the caller. Because nothing can go wrong with logging to the console, just hard-code a True return value.

The clear() method must also be overridden in any class that extends the LogPublisher class. For the console window, call the clear() method to clear all messages published to the console window.

The Log Publishers Service

As you saw in Figure 2, there’s a publishers array property in the LogService class. You need to populate this array with instances of LogPublisher classes. The only class you’ve built so far is LogConsole, but soon you’ll build LogLocalStorage and LogWebApi classes too. Instead of building the list of publishers in the LogService class, create another service class to build the list of log publishers. This service class, named LogPublishersService, is responsible for building the array of log publishing classes. This service is passed into the LogService class so the publishers array can be assigned from the LogPublishersService (Figure 3). At first, you’re going to just hard-code each of the log classes, but later in this article, you’re going to read the list publishers from a JSON file.

Figure 3: The LogPublishersService class builds the list of publishers that’s consumed by the LogService class.

The LogPublishersService class (Listing 5) needs to be defined as an injectable service so Angular can inject it into the LogService class. In the constructor of this class, you call a method named buildPublishers(). This method creates each instance of a LogPublisher and adds each instance to the publishers array. For now, just add the code to create new instance of the LogConsole class and push it onto the publishers array.

Update AppModule Class

Like any Angular service, once you create it, you must register it in the app.module.ts file by importing it and adding it to the providers property of the @NgModule. Open app.module.ts and add the import near the top of the file.

import { LogPublishersService }
   from "./shared/log-publishers.service";

Next, add the service to the providers property in the @NgModule decorator function.

@NgModule({
  imports: [BrowserModule, FormsModule,
            HttpModule],
  declarations: [AppComponent,
                 LogTestComponent],
  bootstrap: [AppComponent],
  providers: [LogService, LogPublishersService]
})

Modify the LogService Class

It’s now time to modify the LogService class to use this LogPublishersService class. Open the log.service.ts TypeScript file and add two import statements near the top of the file.

import { LogPublisher } from "./log-publishers";
import { LogPublishersService }
   from "./shared/log-publishers.service";

Add a property named publishers that is an array of LogPublisher types.

publishers: LogPublisher[];

Add a constructor to the LogService class so Angular injects the LogPublishersService. Within this constructor, take the publishers property from the LogPublishersService and assign the contents to the publishers property in the LogService class.

constructor(private publishersService:
                    LogPublishersService) {
  // Set publishers
  this.publishers =
    this.publishersService.publishers;
}

Locate the writeToLog() method and remove the following line of code from this method.

console.log(entry.buildLogString());

Where you removed the above line of code, add a for loop to iterate over the list of publishers. Each time through the loop, invoke the log() method of the logger, passing in the LogEntry object. Because the log() method returns an observable, you should subscribe to the result and write the Boolean return value to the console window.

for (let logger of this.publishers) {
  logger.log(entry)
    .subscribe(response =>
               console.log(response));
}

Once again, run the application and click the Test Log button to see a log entry written to the console window. You have added a few classes and a service only to publish to the console window; you should be able to see the advantages of this kind of approach. You can now add new LogPublisher classes, add them to the array in the LogPublishersService class, and you’re now publishing to an additional location.

Log to Local Storage

The next publisher to add is one that stores an array of LogEntry objects into your Web browser’s local storage. Open the log-publishers.ts file and add the code shown in Listing 6 to this file. The LogLocalStorage class needs to set a key value for setting the items into local storage. Use the location property to set the key value to use. In this case, the location is set in the constructor. When you have a constructor in a derived class, you always need to call the super() method in order to invoke the constructor of the base class.

Local storage allows you to store quite a bit of data, so let’s add each log entry each time the log() method is called. The setItem() method is used to set a value into local storage. If you call setItem() and pass in a value, any old value in the key location is replaced with the new value. Read the previous values first from local storage using the getItem() method. Parse that into an array of LogEntry objects, or if there was no value stored in that key location, return an empty array. Push the new LogEntry object onto the array, stringify the new array, and place the stringified array into local storage.

One note of caution on local storage; there’s a limit set by each browser to how much data can be stored. The limits vary between browsers, and as of this writing, it varies from 2MB to 10MB. You might want to consider writing some additional code in the catch block of the log() method to remove the oldest values from the array prior to storing the new log entry.

The clear() method is used to clear local storage at the specified key location. Call the removeItem() method of the localStorage object to clear all values within this location.

Now that you have your new class to store log entries into local storage, you need to add this to the publishers array. Open the log-publishers.service.ts file and modify the import statement to include your new LogLocalStorage class.

import { LogPublisher, LogConsole,
         LogLocalStorage }
  from "./log-publishers";

Modify the buildPublishers() method of the LogPublishersService class to create an instance of the LogLocalStorage class and push it onto the publishers array as shown in the following code.

buildPublishers(): void {
  // Create instance of LogConsole Class
  this.publishers.push(new LogConsole());
  // Create instance of LogLocalStorage Class
  this.publishers.push(new LogLocalStorage());
}

You can now re-run the application and you should be logging to both the console window and to local storage. To test this, set a break point in the log() method of the LogLocalStorage class and see if it retrieves the previous values that you logged into local storage.

Log to Web API

The last logging class you are going to create is one to send an instance of the LogEntry class to a Web API method. From this Web API you could then write code to store the log entry into a database table. I’m not going to provide you with a table—I’ll leave that to you. I’m using Visual Studio and C# to create my Web API calls, so I’m going to add a C# class to my project that’s the same name and has the same properties as the LogEntry class I created in Angular.

public class LogEntry
{
  public DateTime EntryDate { get; set; }
  public string Message { get; set; }
  public LogLevel Level { get; set; }
  public object[] ExtraInfo { get; set; }
}

I’m also adding a C# enumeration to my project to map to the TypeScript LoggingLevel enumeration.

public enum LogLevel
{
  All = 0,
  Debug = 1,
  Info = 2,
  Warn = 3,
  Error = 4,
  Fatal = 5,
  Off = 6
}

Finally, I’m going to add a Web API controller class (Listing 7) to my project. This class has one method at this point to allow Angular to post the LogEntry record to this method. It’s in this Post() method that you write the code to store the log data into a database table. For purposes of this article, I’m just going to return a result of OK (true) back to the caller.

Now that you have the Web API classes created, you can go back to the log-publishers.ts file and add some import statements for calling a Web API. Add the following import statements near the top of this file.

import { Http, Response,
         Headers, RequestOptions }
  from '@angular/http';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

Add the LogWebApi class shown in Listing 8 at the bottom of the log-publishers.ts file. The constructor for this class is very similar to the one you wrote for the LogLocalStorage class. You do need to include the HTTP service as you’re going to need this to call the Web API. You also must call super() to execute the constructor of the base class. Finally, set the location property to the URL of the Web API call.

The log() method accepts a LogEntry object that’s sent to the Web API method. Because you’re performing a POST to the Web API, you need to create the appropriate headers to specify the content type you’re sending as application/json. The post() method on the Angular HTTP service is called to pass the LogEntry object to the Web API class you created.

You also need a clear() method in this class to override the abstract method in the base class. In order to keep the length of this article shorter, I’m not showing how to do clear log entries, but it’s similar to the log() method. You call a Web API method that writes the appropriate SQL to delete all rows from your log table in your database.

Open the log-publishers.service.ts file and modify the import statement to include your new LogWebApi class.

import { LogPublisher, LogConsole,
         LogLocalStorage, LogWebApi }
  from "./log-publishers";

Open your log-publishers.service.ts file and add an import for the HTTP service near the top of the file.

import { Http } from '@angular/http';

Next, add the HTTP service to the constructor of this class.

constructor(private http: Http) {
  // Build publishers arrays
  this.buildPublishers();
}

Finally, in the buildPublishers() method, create a new instance of the LogWebApi class and pass in the HTTP service as shown in the code below.

buildPublishers(): void {
  // Create instance of LogConsole Class
  this.publishers.push(new LogConsole());
  // Create instance of LogLocalStorage Class
  this.publishers.push(new LogLocalStorage());
  // Create instance of LogWebApi Class
  this.publishers.push(
          new LogWebApi(this.http));
}

You need to register the HTTP service with your AppModule. Open app.module.ts and add the following import near the top of this file.

import { HttpModule } from '@angular/http';

Add the HttpModule to the imports property in the @NgModule() function decorator.

imports: [BrowserModule, HttpModule],

Now that you have this new publisher added to the array, you should be able to run your logging application, and when you click on the Log Test button, you should see that it’s making the call to your Web API method.

Read Publishers from JSON File

Open the log-publishers.ts file and add a new class called LogPublisherConfig. This class is going to hold the individual objects read from a JSON file you see in Listing 9.

class LogPublisherConfig {
  loggerName: string;
  loggerLocation: string;
  isActive: boolean;
}

Build the JSON file by adding an \assets folder underneath the \src\app folder. Add a JSON file called log-publishers.json in the \assets folder and add the code from Listing 9. Each of the object literals in this JSON array relate to one of the classes you created for logging to the console, local storage, and the Web API.

Add a few more import statements to your log-publishers.service.ts file.

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

Add a constant just after these imports, to point to this file.

const PUBLISHERS_FILE =
  "/src/app/assets/log-publishers.json";

In the LogPublishersService class, add a new method named handleErrors (Listing 10) to take care of any errors that might happen during any HTTP service calls. Also, in the LogPublishersService class, create a new method to read the data from this JSON file. Let’s call this method getLoggers(). Because you already injected the HTTP service into this class, you can use this service to read from the JSON file.

getLoggers(): Observable<LogPublisherConfig[]> {
  return this.http.get(PUBLISHERS_FILE)
    .map(response => response.json())
    .catch(this.handleErrors);
}

Now that you have this method to return the array from this file, modify the buildPublishers() method to subscribe to this Observable array of LogPublisherConfig object. The new code for the buildPublishers() method is shown in Listing 11.

The buildPublishers() method calls the getLoggers() method and subscribes to the output from this method. The output is an array of LogPublisherConfig objects. The array of configuration objects is filtered to only loop through those that have their isActive property set to a True value. For each iteration through the loop, check the loggerName property and compare that value with those listed in each case statement. If a match is found, a new instance of the corresponding LogPublisher class is created. The loggerLocation property is set to the location property of each LogPublisher class. The newly instantiated publisher object is then added to the publishers array property. As all of this happens in the constructor of this service class; the publishers array is already set to the list of publishers to use by the time it is injected into the LogService class. You should be able to run the Angular application and click on the Log Test button and see the log messages published to all publishers marked as isActive in the JSON file.

Summary

It’s always a best practice to log messages as you move throughout your Angular applications. Exceptions should always be logged, but you may also wish to log the debug, warning, and informational messages as well. Creating a flexible logging system like the one presented in this article assists with this best practice. Using a configuration JSON file to store which publishers you wish to log to saves having to hard-code publishers within your log service class.