Building Angular logger in a scalable way
Tags: javascript, angular, typescript
Published: 2022-09-17 12:06:58

We often need to add logs to our applications. Some of them are for debugging on the client and some needs to be sent to a server to track bugs remotely.

For this purpose we will build a Logger class which will log our events to the console, then we will upgrade it to be able to log messages to the server and all of this by using strategy pattern!

---

The most straightforward way is to create a class called Logger with logging methods. Let’s create one with just one method called `log` for brevity.

```typescript
@Injectable({
  providedIn: 'root'
})
export class Logger {
  public log(message: string): void {
    console.log(message);
  }
}
```
Now within the app whenever we need to log a message we would use not `console.log`, but instead `this.logger.log()`. For now it just uses console.log under the hood, nevertheless this is an abstraction in the first place which gives us some space for scaling.

##Implementing strategy pattern

First let’s figure out what the strategy pattern is. To put simply the strategy pattern tells us to have one interface which can be implemented by N classes, so that we could replace the implementations on the fly and still get the application working. This way we can achieve different behaviors that have similar handles in common and do same job differently.

Let’s add some logging to a remote server, so we could keep track of user interactions with our app. For this we create another class called `ServerLogger` with the same methods implemented as in `Logger`.

```typescript
@Injectable({
  providedIn: 'root'
})
export class ServerLogger {
  constructor(private http: HttpClient) {}  public log(message: string): void {
    this.http.post('/log', {
      level: 'log',
      message,
    }).subscribe();
  }
}
```

Good. Now we can use the `ServerLogger` in our classes to post messages to the server. However, it’s not convenient to use multiple loggers separately and furthermore we would have more cases when we need to log messages both to the server and the console.

To handle that we will need to follow next steps:
1. Create a `BaseLogger` which is an abstract class which will be implemented by all loggers and will also work as an injection token
2. Move logic from `Logger` to `ConsoleLogger` for console logging implementation
3. Make Logger work with implementations of `BaseLogger`

```typescript
export abstract class BaseLogger {
  public abstract log(message: string): void;
}
```

Here we only declare methods and declare the class as abstract so we don’t create an instance of it by accident.

Now, let’s update our `ServerLogger` by imlementing the class:

```typescript
export class ServerLogger implements BaseLogger
```

After this we create a `ConsoleLogger` and move the `LoggerLogic` there:

```typescript
@Injectable({
  providedIn: 'root'
})
export class ConsoleLogger {
  public log(message: string): void {
    console.log(message);
  }
}
```

As for the `Logger` we will do the following:

```typescript
@Injectable({
  providedIn: 'root'
})
export class Logger {
  private loggerProviders: BaseLogger[];

  public log(message: string): void {
    this.loggerProviders.forEach(provider => provider.log(message));
  }
}
```

Now all we have left is to fill `loggerProviders` array with actual providers we created.

The first thing that comes into mind is to simply instantiate them with the `new` keyword, however this isn’t the best approach as we would need to come to the class and modify it each time we need a new logger, perhaps add a new dependency to instantiate the logger provider. Also, this would break the **I** in SOLID as we would create instances manually in the consuming class, meanwhile it has to be outside of it.

---

Remember, when used Angular’s injection tokens like `APP_INITIALIZER` we also pass `multi: true`? Ever wondered what this is for and how it works?

This is exactly what we will use to make our `Logger` even more scalable and robust.

For this we need to replace `Injectable` object configuration in `ServerLogger` and `ConsoleLogger` with `Injectable()` as we don’t want those classes to be available as separate providers in DI system.

Then, we update the `Logger` to use the `BaseLogger` as a dependency

```typescript
@Injectable({
  providedIn: 'root'
})
export class Logger {
  constructor(
    @Inject(BaseLogger)
    private loggerProviders: BaseLogger[]
  ) {}

  public log(message: string): void {
    this.loggerProviders.forEach(provider => provider.log(message));
  }
}
```

We declare the type as `BaseLogger[]`, however this is not a valid declaration for Angular, therefore we need to add `@Inject(BaseLogger)` to tell the DI that we want to inject this class. DI does see that we provided `multi: true` and therefore will give us an array instead of an object.

Let’s update the `app.module.ts` file

```typescript
const loggerProviders: Provider[] = [
  { provide: BaseLogger, useClass: ConsoleLogger, multi: true },
  { provide: BaseLogger, useClass: ServerLogger, multi: true },
];

@NgModule({
  ...
  imports: [
    ...,
    HttpClient,
  ],
  providers: [
    ...
    loggerProviders
  ]
})
export class AppModule {
}
```

From now on, whenever we need a new provider we just need create another class which implements the `BaseLogger` and update providers array. The Logger is safe as we don’t touch it to add new providers.

---

If we wanted even more flexibility we could make `BaseLogger` instances to accept a configuration using `deps` array, there we could pass `logLevel` for each provider separately. This way we could log everything to the console and only errors to the server.

Resulting code can be found here: https://stackblitz.com/edit/angular-ivy-pqspk7?embed=1&file=src/app/app.module.ts