Skip to main content

Best practices

Every log can be represented as a combination of its level, creation time, message, scope and payload. Using inline logs with the LumberjackService can cause structure duplication and/or denormalization.

Continue reading to know more about the recommended best practices designed to tackle this issue.

Loggers

The LumberjackLogger service is an abstract class that wraps the LumberjackService to help us create structured logs and reduce boilerplate. At the same time, it provides testing capabilities since we can easily spy on logger methods and control timestamps by replacing the LumberjackTimeService.

LumberjackLogger is used as the base class for any other logger that we need.

This is the abstract interface of LumberjackLogger:

/**
* A logger holds methods that log a predefined log.
*
* Implement application- and library-specific loggers by extending this base
* class. Optionally supports a log payload.
*
* Each protected method on this base class returns a logger builder.
*/
@Injectable()
export abstract class LumberjackLogger<TPayload extends LumberjackLogPayload | void = void> {
protected lumberjack: LumberjackService<TPayload>;
protected time: LumberjackTimeService;

/**
* Create a logger builder for a critical log with the specified message.
*/
protected createCriticalLogger(message: string): LumberjackLoggerBuilder<TPayload>;

/**
* Create a logger builder for a debug log with the specified message.
*/
protected createDebugLogger(message: string): LumberjackLoggerBuilder<TPayload>;

/**
* Create a logger builder for an error log with the specified message.
*/
protected createErrorLogger(message: string): LumberjackLoggerBuilder<TPayload>;

/**
* Create a logger builder for an info log with the specified message.
*/
protected createInfoLogger(message: string): LumberjackLoggerBuilder<TPayload>;

/**
* Create a logger builder for a trace log with the specified message.
*/
protected createTraceLogger(message: string): LumberjackLoggerBuilder<TPayload>;

/**
* Create a logger builder for a warning log with the specified message.
*/
protected createWarningLogger(message: string): LumberjackLoggerBuilder<TPayload>;

/**
* Create a logger builder for a log with the specified log level and message.
*/
protected createLoggerBuilder(level: LumberjackLogLevel, message: string): LumberjackLoggerBuilder<TPayload>;

By extending LumberjackLogger, we only have to worry about our pre-defined logs' message and scope.

All logger factory methods are protected as it is recommended to create a custom logger per scope rather than using logger factories directly in a consumer.

As an example, let's create a custom logger for our example application.

import { Injectable } from '@angular/core';

import { LumberjackLogger, LumberjackService, LumberjackTimeService } from '@ngworker/lumberjack';

@Injectable({
providedIn: 'root',
})
export class AppLogger extends LumberjackLogger {
static scope = 'Forest App';

forestOnFire = this.createCriticalLogger('The forest is on fire!').withScope(AppLogger.scope).build();

helloForest = this.createInfoLogger('Hello, Forest!').withScope(AppLogger.scope).build();
}

Logger usage

Now that we have defined our first Lumberjack logger let's use it to log logs from our application.

import { inject, Component, OnInit } from '@angular/core';
import { LumberjackLogger } from '@ngworker/lumberjack';

import { AppLogger } from './app.logger';
import { ForestService } from './forest.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
readonly #logger = inject(AppLogger);
readonly #forest = inject(ForestService);

ngOnInit(): void {
this.#logger.helloForest();

this.#forest.fire$.subscribe(() => this.#logger.forestOnFire());
}
}

The previous example logs Hello, Forest! when the application is initialized, then logs The forest is on fire! if a forest fire is detected.

Simplifying with ScopedLumberjackLogger

An alternative to the LumberjackLogger interface, where we need to specify the lumberjack log scope manually, we could use the ScopedLumberjackLogger.

The ScopedLumberjackLogger is a convenient Logger and an excellent example of how to create custom Loggers according to your situation.

/**
* A scoped logger holds methods that log a predefined log sharing a scope.
*
* Implement application- and library-specific loggers by extending this base
* class. Optionally supports a log payload.
*
* Each protected method on this base class returns a logger builder with a
* predefined scope.
*/
@Injectable()
export abstract class ScopedLumberjackLogger<
TPayload extends LumberjackLogPayload | void = void
> extends LumberjackLogger<TPayload> {
abstract readonly scope: string;

/**
* Create a logger builder for a log with the shared scope as well as the
* specified log level and message.
*/
protected createLoggerBuilder(level: LumberjackLogLevel, message: string): LumberjackLoggerBuilder<TPayload> {
return new LumberjackLoggerBuilder<TPayload>(this.lumberjack, this.time, level, message).withScope(this.scope);
}
}

The resulting AppLogger after refactoring to using the ScopedLumberjackLogger would be:

import { Injectable } from '@angular/core';

import { LumberjackService, LumberjackTimeService, ScopedLumberjackLogger } from '@ngworker/lumberjack';

@Injectable({
providedIn: 'root',
})
export class AppLogger extends ScopedLumberjackLogger {
scope = 'Forest App';

forestOnFire = this.createCriticalLogger('The forest is on fire!').build();

helloForest = this.createInfoLogger('Hello, Forest!').build();
}

Notice that now every log written using the AppLogger will have the 'Forest App' scope

Using Loggers with a LumberjackLog payload

As seen in the Log drivers section, we can send extra info to our drivers using a LumberjackLog#payload.

The LumberjackLogger and ScopedLumberjackLogger provide a convenient interface for such a scenario.

import { Injectable, VERSION } from '@angular/core';

import {
LumberjackLogPayload,
LumberjackService,
LumberjackTimeService,
ScopedLumberjackLogger,
} from '@ngworker/lumberjack';

export interface LogPayload extends LumberjackLogPayload {
readonly angularVersion: string;
}

@Injectable({
providedIn: 'root',
})
export class AppLogger extends ScopedLumberjackLogger<LogPayload> {
static readonly #payload: LogPayload = {
angularVersion: VERSION.full,
};

scope = 'Forest App';

forestOnFire = this.createCriticalLogger('The forest is on fire!').build();

helloForest = this.createInfoLogger('Hello, Forest!').withPayload(AppLogger.#payload).build();
}

The AppLogger usage remains the same using a LumberjackLogger or ScopedLumberjackLogger, with payload or without.

LumberjackLogFactory

Lumberjack's recommended way of creating logs is by using a LumberjackLogger.

However, there are some times that we want to create logs manually and pass them to the LumberjackService.

The LumberjackLogFactory provides a robust way of creating logs. It's also useful for creating logs in unit tests.

This is how we create logs manually:

import {inject, Component, OnInit, VERSION} from '@angular/core';

import {LumberjackLogFactory, LumberjackService} from '@ngworker/lumberjack';

import {LogPayload} from './log-payload';

@Component({
selector: 'ngworker-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
readonly #logFactory: inject<LumberjackLogFactory<LogPayload>>
(
LumberjackLogFactory
);
readonly #lumberjack = inject<LumberjackService<LogPayload>>(LumberjackService);
readonly #payload: LogPayload = {
angularVersion: VERSION.full,
};
readonly #scope = 'Forest App';

ngOnInit(): void {
const helloForest = this.#logFactory
.createInfoLog('Hello, Forest!')
.withScope(this.scope)
.withPayload(this.payload)
.build();

this.#lumberjack.log(helloForest);
}
}