What is going on here? Getting Started With Logging in Angular
Tags: angular, webdev, javascript, programming
Published: 2023-09-04 04:30:00

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](https://github.com/pBouillon/DEV.GettingStartedWithLoggingInAngular). 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](https://github.com/pBouillon/DEV.GettingStartedWithLoggingInAngular/releases/tag/initial-setup), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ta7gck7z6ydphsmryth9.png)

## Table of Contents

- [Logging, What For?](#logging-what-for)
- [The Built-In Way](#the-built-in-way)
- [Limitations](#limitations)
- [Leveraging Angular Services](#leveraging-angular-services)
- [Going Further](#going-further)
- [Takeaways](#takeaways)

---

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/144ht5j0wkb0rmpsuvv2.png)

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

![Other log levels](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oyz4wfukfzftbw7xnuy2.png)

> ℹ There are also [many others](https://developer.mozilla.org/en-US/docs/Web/API/console) 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`:

```ts
@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"
    );
  }
}
```

> ℹ If you type `log` + <kdb>TAB</kdb>, Visual Studio Code will automatically write the `console.log()` for you:
> ![Console Log Shortcut](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2dcxtvsqc17xn4398mm4.png)

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

![Demo With Console Logs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dw4r8i1kfcro11xa1rj6.gif)

⚗ 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](https://github.com/pBouillon/DEV.GettingStartedWithLoggingInAngular/releases/tag/adding-logs) 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:

```ts
@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);
  }
}
```

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:

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

  // ...
}
```

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

```diff
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);
  }

  // ...
}
```

### 🧪 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqpeuz3xgemat0byqcdg.gif)

> 💡 You can check out the [solution](https://github.com/pBouillon/DEV.GettingStartedWithLoggingInAngular/releases/tag/adding-logger-service) 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:

```ts
export enum LogLevel {
  NEVER = Number.MAX_SAFE_INTEGER,

  TRACE = 0,
  DEBUG = 1,
  INFO = 2,
  WARNING = 3,
  ERROR = 4,
  FATAL = 5,
}
```

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

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

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

> ℹ 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:

```diff
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);
  }

  // ...
}
```

##### 🧪 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](https://github.com/pBouillon/DEV.GettingStartedWithLoggingInAngular/releases/tag/adding-log-levels) 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](https://angular.io/api/core/inject).

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:

```ts
export interface LoggerProvider {
  info(template: string, ...optionalParams: any[]): void;
  warning(template: string, ...optionalParams: any[]): void;
  error(template: string, ...optionalParams: any[]): void;
}
```

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

```ts
@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);
  }
}
```

> ℹ 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`:

```ts
export const LOGGER_PROVIDERS = new InjectionToken<LoggerProvider[]>(
  "Providers for the logger"
);
```

And register our implementation:

```diff
+ 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,
    },
  ],
});
```

> ℹ 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:

```diff
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)
+   );
  }

  // ...
}
```

#### 🧪 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yklweplymy18pdwww8t5.gif)

> 💡 You can check out the [solution](https://github.com/pBouillon/DEV.GettingStartedWithLoggingInAngular/releases/tag/adding-providers) 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](https://github.com/ngworker/lumberjack) or [ngx-logger](https://www.npmjs.com/package/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!

{% embed https://dev.to/pbouillon %}

---

> Photo by <a href="https://unsplash.com/@chuttersnap?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">CHUTTERSNAP</a> on <a href="https://unsplash.com/photos/Zf64Osndqvc?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>