Easy Angular Logging

In a project I’m working on I’m adding logging capabilities. Like many, I’m sure, I started by writing my own logging service. While working through this code, I realized that I was spending too much time on a solution that wasn’t instrumental to my business case. I use Winston on the backend, and was hoping that I could use it for the front end. Unfortunately, at the time of this writing Winston’s architecture does not allow it to be used with Angular because of the use of fs in the library and the lack of ability to write to filesystems in Angular. In the search for an alternative logging provider, I chose ngx-logger as my solution. Below is my journey during learning about, and implementing, ngx-logger in an Angular application.

Why ngx-logger?

Deciding which logging provider to use took some research. Looking on npm there are many including winston, log4js, loglevel, bunyan, and ngx-logger. The first thing I do is look at the popularity on npm for a package. Not that it directly shows the quality of a package, what I’ve found is the more popular packages are often better maintained as they have many eyes from the community on them. I then look on github at the usedby and star numbers. I also look at the github issues, not just the count but I look at what types of issues are being logged. Are they true bugs, or are these also questions on usage. Do issues seem to be closed quickly.

Dwindling down the list, I found that some logging frameworks were no better than what I had written. Others seemed too heavy for what I wanted, or in reading through the setup for the module it seemed confusing to get to work with the browser. What I was looking for was a library that allowed me to log multiple logging levels, allow formatting for each log level, and provided transports so I didn’t have to write them. But I also wanted something lightweight. I was also looking for a logging library that was not overly complex. In the end, I chose ngx-logger which had some of what I was looking for, and was also very lightweight. While the watcher and star count for the project is fairly low on github, it has 32,000 weekly downloads on npm, and a very low issue list.

“Designing” our implementation.

The design between our test implementation and our product are quite different. The test implementation is meant only to get my hands around the best way to implement, and learn the quirks of the product. My goal was for the demo was to develop logging that is available at different logging levels, logs all to console, and can be activated and inactivated through a config file. For our production use case, we will send logs to a cloud based storage solution, and do some logging to console.

We will use a service to house our logging wrapper, and use APP_INITILIZER to enable our config file. The web interface is simply four inputs with some buttons that trigger code to log at different levels, to see how our logging looks and acts. We’ve added in some debugging calls to places like our ngOnInit() method to test what the output would like like with debug mode on in production.

We originally chose a file for deeper errors based on past experience not getting these logged to a DB when having issues such as a core dump. This was short sighted, and recently have had success with logging directly to a database. In addition, Angular no longer allows filesystem access. This is the importance of not letting past experience blind you to a solution, but constantly experimenting and letting those experiments guide your decisions. Now we will log the high impact errors to a cloud data store, particularly to a documentDB.

Adding logging to our application.

Our first step in this journey is getting the logging framework added to our app and ready for us to build our service. Our first action is to import the libraries we need. If we try to use ngx-logger without imports we’ll get NullInjectorErrors, one for the logger module and one for HttpClient module. So let’s start in our app.module.ts and import those modules.

import { LoggerModule, NgxLoggerLevel, LoggerConfig } from 'ngx-logger';

import { HttpClientModule } from '@angular/common/http';

Now that we have our imports, we need to add these modules to our imports section. Here’s where we begin our configuration for the LoggerModule.

  imports: [
    HttpClientModule,
    LoggerModule.forRoot({
      level: NgxLoggerLevel.INFO,
      serverLogLevel: NgxLoggerLevel.INFO,
    }),
  ],

Our level attribute says, which of our log levels is the top level we want to log. The serverLogLevel attribute is which level do we want to log to a URL using HTTPBackend. We have left off the server log URL as we are not logging to an API for this demo.

One area that became clear as I set up and played with the library, is the use of this setup and logging levels. The log levels available in the library are

TRACE|DEBUG|INFO|LOG|WARN|ERROR|FATAL|OFF

What these settings determine is that at a level of INFO, as we show above, we will not log TRACE or DEBUG messages but all others. WARN would not log TRACE, DEBUG, INFO or LOG levels. OFF, because it is the lowest level, turns off all logging.

Another area that took some thought, was that I wanted a file configurable logging level. When I was in production I wanted to have logging at INFO as a default, but turn on TRACE or DEBUG if I needed to. Thankfully you can update the logging level on the fly and using Angular’s APP_INITIALIZER, build functionality that pulls together a configuration option in a text file that turns on debugging. I will show the config update later in our code.

Here is the full app.module.ts file, including imports we use on the front end.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import {
  MatInputModule,
  MatButtonModule,
  MatFormFieldModule,
} from '@angular/material';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppConfigService } from './services/app-config.service';

import { LoggerModule, NgxLoggerLevel, LoggerConfig } from 'ngx-logger';

import { HttpClientModule } from '@angular/common/http';

export function initializeApp(appConfig: AppConfigService) {
  return () => appConfig.load();
}

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    ReactiveFormsModule,
    MatFormFieldModule,
    BrowserAnimationsModule,
    MatInputModule,
    MatButtonModule,
    HttpClientModule,
    LoggerModule.forRoot({
      level: NgxLoggerLevel.INFO,
      serverLogLevel: NgxLoggerLevel.INFO,
    }),
  ],

  providers: [
    AppConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: initializeApp,
      deps: [AppConfigService],
      multi: true,
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Building our logging service.

Our architecture includes building a service to hold the logging functionality. Why put this in a service? Services serve many purposes, in our case it insulates us from the library. If the library makes a breaking change, or we decide to use a different logging library, we only change our code once in the service. If there’s a bug in the way we decide to write the logging code, we fix it once.

First, let’s look at our service’s constructor. As we discussed above, we have an AppConfigService that allows us to change debugging levels on the fly. Here I’ve removed that code, and hard coded an environment and debug flag to show how we can update the configuration and change our logging level.

  constructor(private logger: NGXLogger) {
    // TRACE|DEBUG|INFO|LOG|WARN|ERROR|FATAL|OFF
    this.canDebug = true;

    if (this.canDebug) {
      console.log('Dropping into debug mode');
      this.logger.updateConfig({ level: NgxLoggerLevel.TRACE });
    }
  }

It’s simply an if statement that sets our NgxLoggerLevel to TRACE if our canDebug flag is true. Again, this is nice for production, where we would not want the overhead of logging debugging data, unless we wanted to. Of course, how it’s coded above would require a code update and recompile. In our full example below we take the information from a config file in our assets folder. This allows us to change the level between page refreshes.

Next, we write a wrapper for each logging level and determine the level of information we want at each level. Two examples are, for TRACE debugging we have more data coming in from our code, for the INFO level we only have a message. What we’ll also do in our production app is develop a standard for our parameter layout., and probably put in some boilerplate verbiage in the messages sent. We want consistency in our logging messages and our setup below does not give us that.

  sendTraceLevelMessage(message, source, error) {
    this.logger.trace(message, source, error);
  }

  sendInfoLevelMessage(message) {
    this.logger.info(message);
  }

We can also develop a custom log monitor. For example, if you need a custom transport that is not a console log or push using HTTP, you can add that code here and have it trigger for each logging level.

export class MyLoggerMonitor implements NGXLoggerMonitor {
  onLog(logObject: NGXLogInterface): void {
    console.log('logging stuff to an API if we need a custom transport ', logObject);
  }
}

To enable this, you would make the following call in the service’s constructor.

this.logger.registerMonitor(new MyLoggerMonitor());

As with the logging level above, you could also develop a few logger monitors, and decide which to use through a configuration file, which seems like it could be useful.

That’s the basics of the service. Here is the entire logging service code.

import { Injectable } from '@angular/core';
import { NGXLogger, NGXLoggerMonitor, NGXLogInterface, NgxLoggerLevel } from 'ngx-logger';
import { AppConfigService } from './app-config.service';

@Injectable({
  providedIn: 'root',
})
export class LoggingService {
  env: string;
  canDebug: boolean;

  constructor(private logger: NGXLogger) {
    // TRACE|DEBUG|INFO|LOG|WARN|ERROR|FATAL|OFF
    this.env = AppConfigService.settings.env.name;
    this.canDebug = AppConfigService.settings.logging.debug;
    console.log(
      'This environment ',
      this.env,
      ' debugging enabled ',
      this.canDebug
    );

    this.logger.registerMonitor(new MyLoggerMonitor());

    if (this.canDebug) {
      console.log('Dropping into debug mode');
      this.logger.updateConfig({ level: NgxLoggerLevel.TRACE });
    }
  }

  sendTraceLevelMessage(message, source, error) {
    this.logger.trace(message, source, error);
  }

  sendDebugLevelMessage(message, source, error) {
    this.logger.debug(message, source, error);
  }

  sendInfoLevelMessage(message) {
    this.logger.info(message);
  }

  sendLogLevelMessage(message, source, error) {
    this.logger.log(message, source, error);
  }

  sendWarnLevelMessage(message, error) {
    this.logger.warn(message, error);
  }

  sendErrorLevelMessage(message, source, error) {
    this.logger.error(message, source, error);
  }

  sendFatalLevelMessage(message, source, error) {
    this.logger.fatal(message, source, error);
  }
}

export class MyLoggerMonitor implements NGXLoggerMonitor {
  onLog(logObject: NGXLogInterface): void {
    console.log('logging stuff to an API if we need a custom transport ', logObject);
  }
}

Using our logging service.

As we have our logging enabled through a service, we need to import our service and make it available to this component. Here I’ve slimmed down the code, but show the full example later. It’s as simple as adding the import and injecting the logging service into our constructor. That’s it! Now we can log. To show when we’re at the DEBUG level, in ngOnInit() we send a DEBUG level message.

import { LoggingService } from './services/logging.service';

constructor(private fb: FormBuilder, private ls: LoggingService) {}

ngOnInit(): void {
    this.ls.sendDebugLevelMessage('ngOnInit message', this, {
        error: 'none',
    });
}

In our app, we have a form with several inputs and submit buttons. What we show here, is a button submit that would normally log an INFO level message to let us know the button click was successful. When we turn DEBUG level messages on, we can then get more information on the button click.

successfulSubmit() {
    this.ls.sendDebugLevelMessage('Debugging successful submit', this, {
      error: 'none',
    });
    this.ls.sendInfoLevelMessage('Message submitted successfully');
}

Here is our full app.component.ts file.

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { LoggingService } from './services/logging.service';
import { AppConfigService } from './services/app-config.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  title = 'easy-angular-logging';

  workingLoggedForm: FormGroup;
  brokenLoggedForm: FormGroup;
  throwErrorForm: FormGroup;
  dnsErrorForm: FormGroup;

  env: string;
  canDebug: boolean;

  constructor(private fb: FormBuilder, private ls: LoggingService) {}

  ngOnInit(): void {
    this.workingLoggedForm = this.fb.group({
      workingField: [''],
    });

    this.brokenLoggedForm = this.fb.group({
      brokenField: ['', Validators.required],
    });

    this.dnsErrorForm = this.fb.group({
      dnsErrorField: [''],
    });

    this.throwErrorForm = this.fb.group({
      thisField: [''],
    });

    this.ls.sendDebugLevelMessage('ngOnInit message', this, {
      error: 'none',
    });

    this.env = AppConfigService.settings.env.name;
    this.canDebug = AppConfigService.settings.logging.debug;
  }

successfulSubmit() {
    this.ls.sendDebugLevelMessage('Debugging successful submit', this, {
      error: 'none',
    });
    this.ls.sendInfoLevelMessage('Message submitted successfully');
}

brokenSubmit() {
this.brokenLoggedForm.get('brokenField').errors);
    this.ls.sendDebugLevelMessage('Debugging broken submit', this, {
      error: this.brokenLoggedForm.controls.brokenField.errors,
    });

    this.ls.sendWarnLevelMessage(
      'Submit went wrong',
      this.brokenLoggedForm.controls.brokenField.errors
    );
}

throwDNSError() {
    this.ls.sendDebugLevelMessage('Debugging DNS error on submit', this, {
      error: 'something went wrong',
    });

    this.ls.sendErrorLevelMessage(
      'Need to create a web call to URL that errors',
      this,
      { error: 'something went wrong' }
    );
}

 throwsFatalError() {
    const fatalError = new Error('Something went very wrong');
    this.ls.sendFatalLevelMessage('This button is broken', this, fatalError);
    // here we would throw fatalError;
  }
}

What I learned.

Once I got over the initial hurdles of looking at a lot of logging libraries, and determining how to actually get logging to function at the basic level, using ngx-logger was a breeze. There aren’t a lot of options or built in transports, which was a plus for me. I wanted a very light weight library, easy to get going, that doesn’t have a lot of unneeded complexity. I like that I can also write my own transports using the registerMonitor call.

What I still have to figure out, is if I can set a different transport for a specific logging level. For example, if I want to only console.log for INFO level messages, but send to a cloud data store for the rest of the levels, can I do that? Honestly I haven’t gone that deep into the library yet. I also haven’t played with custom logging colors yet, which is another option available. What I’ll do now is dive into the github repo and look at the library code to gain a deeper understanding of the functionality available. I like to know how the libraries I use work, and reading their code is a great way to get there.

This is one way to set up ngx-logger, and only touches on the setup and a portion of the functionality available. How you use it is up to you, but I hope this gets you over the initial hurdle of setting it up and understanding some of what’s available. A full example based on this article is available on our github repo.