DEV Community

Cover image for What is going on here? Getting Started With Logging in Angular
Pierre Bouillon for This is Angular

Posted on

What is going on here? Getting Started With Logging in Angular

Logging is a fairly common topic in the software industry. Unfortunately it is not so much in the front-end world and frequently mentioned in back-end-related articles.

However, this topic also applies to front-end projects.

In this article, we will see what logging is and various ways to implement it in an modern Angular application.

🧪 Code Along With Me

For this article, you have the option to clone the project I used. This allows you to actively follow and code along with me through the different steps.

If you decide to do so, please begin from the initial-setup tag, if not, you can simply jump to the next headings.

Each section detailing the progression of the hands-on lab will be enclosed within a collapsible section marked by 🧪.

Once the application served, you should see this page:

Project HomePage

Table of Contents


Logging, What For?

First of all, why would we like to log anything in our Angular application?

We can differentiate two aspects here:

  • The environment
  • The type of information we would like to read

The Environment

From a developer perspective, we may want to have as many log as possible from what is going on in various places of our application.

However, from a user perspective, I would prefer not seeing a bunch of logs in the devtools or many HTTP calls to a backend.

From the environment (whether it is production, UAT, or something else), the logging might change and give different insights of what for or how the application is used.

The Type of Information

Logs carry two pieces of information: their level (is it an error? a regular action? something unusual?) and their payload, the actual content.

By using those two pieces, we can provide insights of things the user does, a functional log (e.g. New todo created by John) or a technical one (e.g. Cache refreshed, 486 todo items synchronized).

By combining the types of logs and differentiating them depending of the environment, we can achieve a better observability of our application, improving our troubleshooting capability and overall development experience.

The Built-In Way

In JavaScript, the most known logging method is the call to the console.log method, which displays a log into the console:

console.log

In addition to the log method, others can also be used to show the information differently:

Other log levels

ℹ There are also many others that you might want to read about

🧪 Adding Logs

For now, we don't have any logs and knowing what happened can only be achieve by browsing the sources.

By taking advantage of the console.log method, we can now add some logs to the TodoService:

@Injectable({ providedIn: "root" })
export class TodoService {
  // ...

  delete(idToDelete: number): void {
    // ...
    console.log("Todo Item #%d deleted", idToDelete);
  }

  setComplete(idToSet: number, isDone: boolean): void {
    // ...
    console.log(
      "Todo Item #%d status set to %s",
      idToSet,
      isDone ? "done" : "pending"
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

ℹ If you type log + TAB, Visual Studio Code will automatically write the console.log() for you:
Console Log Shortcut

We can now see a bit more clearly what is going on when using our application:

Demo With Console Logs

⚗ Why not try to add your own? We could use a warning when an unknown id is provided!

💡 Once done, you can check out the solution to compare your results

Limitations

Although having logs is great, those will be shipped to production when the site will be deployed.

Worse, console.xxx are not centralized, making them harder to spot and easier to forget when reviewing or pushing code.

Those logs also lack of some features we might want such as logging to a different provider (HTTP, console, why not even another Angular service?).

For all of those reasons, it could be preferable to centralized them within an dedicated (software) component.

Leveraging Angular Services

We can create a simple abstraction to wrap the console calls:

@Injectable({ providedIn: "root" })
export class LoggerService {
  info(template: string, ...optionalParams: any[]): void {
    console.log(template, ...optionalParams);
  }

  warning(template: string, ...optionalParams: any[]): void {
    console.warn(template, ...optionalParams);
  }

  error(template: string, ...optionalParams: any[]): void {
    console.error(template, ...optionalParams);
  }
}
Enter fullscreen mode Exit fullscreen mode

Just by using this new layer, we can now modify the behavior of our logs in one place.

For example, we might want to disable all information logs if we are in production:

export class LoggerService {
  info(template: string, ...optionalParams: any[]): void {
+   if (!isDevMode()) return;
    console.log(template, ...optionalParams);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

We could also enforce a specific format, like prepending the current time:

export class LoggerService {
+ #withDate(template: string): string {
+   return `${new Date().toLocaleTimeString()} | ${template}`;
+ }

  info(template: string, ...optionalParams: any[]): void {
    if (!isDevMode()) return;
-   console.log(template, ...optionalParams);
+   console.log(this.#withDate(template), ...optionalParams);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

🧪 Using the LoggerService

Create the LoggerService in a new logger.service.ts file with and add the previous code to log the date alongside the message.

Once done, replace any occurrences of console. from the TodoService and replace them with a call to our new LoggerService.

Once those changes has been made, you should now see your logs with the time they were emitted at:

Logs With Service

💡 You can check out the solution to compare your results

Going Further

While the foundations has been setup through our service, there is now much we can do if we would like to strengthen our logs.

Restricting the Log Level

Usually, there are six log levels:

Name Meaning Example
TRACE Tracing of the execution flow Starting DoStuff()
DEBUG Information helpful for debugging purposes Value of x: 42
INFO General information about program execution Application started
WARNING Indication of potential issues or anomalies Id not found
ERROR Describes an error that occurred Unable to connect to the database
FATAL Indicates a critical failure in the program System crashed

We can represent those levels by an enum:

export enum LogLevel {
  NEVER = Number.MAX_SAFE_INTEGER,

  TRACE = 0,
  DEBUG = 1,
  INFO = 2,
  WARNING = 3,
  ERROR = 4,
  FATAL = 5,
}
Enter fullscreen mode Exit fullscreen mode

And expose an InjectionToken for our application to provide a default log level:

export const MIN_LOG_LEVEL = new InjectionToken<LogLevel>("Minimum log level");

bootstrapApplication(AppComponent, {
  providers: [
    {
      provide: MIN_LOG_LEVEL,
      useValue: isDevMode() ? LogLevel.INFO : LogLevel.NEVER,
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

ℹ This can also be set directly from the environment variables instead

By consuming this token, we can restrict the behavior of the LoggerService based on it:

export class LoggerService {
+  readonly #minLogLevel = inject(MIN_LOG_LEVEL) ?? LogLevel.NEVER;

+ #canLog(logLevel: LogLevel): boolean {
+   return logLevel >= this.#minLogLevel;
+ }

  info(template: string, ...optionalParams: any[]): void {
-   if (!isDevMode()) return;
+   if (!this.#canLog(LogLevel.INFO)) return;
    console.log(this.#withDate(template), ...optionalParams);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode
🧪 Using the LogLevel

For our LoggerService to use log levels instead of a check with isDevMode, create the LogLevel enum in a new loglevel.enum.ts.

Then, define an InjectionToken "MIN_LOG_LEVEL" as described before and provide it in the main.ts file.

Finally, take the previous LoggerService snippet to consume the token, define the #canLog method and replace the calls to isDevMode with calls to the newly defined #canLog method.

Once done, you should see no difference in the behavior of the application, but try changing the MIN_LOG_LEVEL to LogLevel.NEVER: nothing should be logged anymore.

💡 You can check out the solution to compare your results

Logging to Other Providers

Angular has a very powerful dependency injection system, even more since the introduction of the inject function.

Using it, we can seamlessly compose services by providing them appropriately.

In the context of our logger, we can take advantage of it to easily alter the behavior of the logger depending of the environment without spreading the logic everywhere.

To do so, we could define an interface exposing a provider for our logging system:

export interface LoggerProvider {
  info(template: string, ...optionalParams: any[]): void;
  warning(template: string, ...optionalParams: any[]): void;
  error(template: string, ...optionalParams: any[]): void;
}
Enter fullscreen mode Exit fullscreen mode

A straightforward implementation could then be one relying on the console:

@Injectable()
export class ConsoleProvider implements LoggerProvider {
  info(template: string, ...optionalParams: any[]): void {
    console.log(template, ...optionalParams);
  }

  warning(template: string, ...optionalParams: any[]): void {
    console.warn(template, ...optionalParams);
  }

  error(template: string, ...optionalParams: any[]): void {
    console.error(template, ...optionalParams);
  }
}
Enter fullscreen mode Exit fullscreen mode

ℹ Since this is an Injectable, we could absolutely use the HttpClient for example, to send logs to a dedicated back-end too

We can then leverage the flexibility of the DI system to define a new InjectionToken, providing all the registered LoggerProvider:

export const LOGGER_PROVIDERS = new InjectionToken<LoggerProvider[]>(
  "Providers for the logger"
);
Enter fullscreen mode Exit fullscreen mode

And register our implementation:

+ function registerLoggerProviders(): EnvironmentProviders {
+  return makeEnvironmentProviders(
+    isDevMode()
+      ? [{ provide: LOGGER_PROVIDERS, useClass: ConsoleProvider, multi: true }]
+      : []
+  );
+}

bootstrapApplication(AppComponent, {
  providers: [
+   registerLoggerProviders(),
    {
      provide: MIN_LOG_LEVEL,
      useValue: isDevMode() ? LogLevel.INFO : LogLevel.NEVER,
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

ℹ Note that the registration is composed based on the current environment: changing the providers used by the application at runtime only needs a change here!

Our logger may now consume the token and rely on the underlying implementations of the LoggerProvider instead of implementing its own logic, and solely take care of when to call them:

export class LoggerService {
  readonly #minLogLevel = inject(MIN_LOG_LEVEL) ?? LogLevel.NEVER;
+ readonly #providers = inject(LOGGER_PROVIDERS) ?? [];

  #canLog(logLevel: LogLevel): boolean {
    return logLevel >= this.#minLogLevel;
  }

  info(template: string, ...optionalParams: any[]): void {
    if (!this.#canLog(LogLevel.INFO)) return;
+   this.#providers.forEach((provider) =>
+     provider.info(template, ...optionalParams)
+   );
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

🧪 Adding a custom LoggerProvider

Follow the previous part in order for the LoggerService to rely on the provided LoggerProvider.

Once again, no changes should be seen from the application, aside from the fact that we do no longer have the time prepended on our logs: let's bring that back!

Create a new implementation of the LoggerProvider named TimedConsoleProvider. This implementation should use the previous #withDate method to format the template.

When implemented, provide it at the root level, in the main.ts file so it can be injected along with the ConsoleProvider.

ℹ Don't forget to set multi: true to register several classes for the same token

If you followed those steps, you should now see two logs for each action: one written by the ConsoleProvider, the other by the TimedConsoleProvider:

Application With Logger Providers

💡 You can check out the solution to compare your results

Takeaways

In this article, we saw what logging means for front-end application and examined a range of techniques to integrate logging in Angular, spanning from straightforward methods to more sophisticated approaches.

In real-life application, you might prefer to rely on a standardized third party library, offering more options in terms of configuration such as @ngworker/lumberjack or ngx-logger

Logging can help you achieve a better observability of your application. However, maintaining a user-centric approach is essential.

Excessive logging of technical details in a production environment or excessive use of HTTP calls can potentially degrade the overall user experience. Striking the right balance between informative logging and user experience is the key to ensuring the seamless operation of your application.


I hope you learned something useful there!


Photo by CHUTTERSNAP on Unsplash

Top comments (9)

Collapse
 
wanoo21 profile image
Ion Prodan

Thanks, good article btw

Collapse
 
pbouillon profile image
Pierre Bouillon

Thanks Ion, glad you enjoyed it!

Collapse
 
ajitzero profile image
Ajit Panigrahi

Great write-up, and I love that you used Pico CSS for this!

For others looking for existing solutions, have a look at Lumberjack!

Collapse
 
aboudard profile image
Alain Boudard

Nice take, you also can add that according to many who benchmarked it, console.log is far from being either fast or cheap in memory.
Cheers :)

Collapse
 
caioragazzi profile image
Caio Ragazzi

Great article!
Thanks

Collapse
 
marinipavingmasonry profile image
Robert Smith

Amazing and very informative

Collapse
 
diegodcp profile image
Diego da Costa Porto

Great article! Detailed and super useful! thanks a lot

Collapse
 
marinipavingmasonry profile image
Robert Smith

One of the greatest piece of knowldge .

Collapse
 
david2tm profile image
David Ichilov

I wander if it's possible to write a logging service (with console driver) which will preserve the line number of "form where it was called form".
e.g. in your gif "see your logs with the time they were emitted at", it writes that the code is at: "logger.server.ts:#..:#", and I would want to see the file#L of TodoService file instead