Chop and cut Angular logs like a professional lumberjack.
Lumberjack is a versatile Angular logging library, specifically designed to be extended and customized. It provides a few simple log drivers (logging mechanisms, transports, log drivers) out-of-the-box. It's easy to enable the built-in log drivers or create and use custom log drivers.
For support, please refer to the
#lumberjack
channel in the Angular Discord server.
- β Configurable multilevel logging
- β Plugin-based log driver architecture
- β Robust error handling
- β Console driver
- β HTTP driver
- β Logger base class
- β Lumberjack service
- β Best practices guide
Lumberjack is published as the @ngworker/lumberjack
package.
Toolchain | Command |
---|---|
NPM CLI | npm install @ngworker/lumberjack |
PNPM CLI | pnpm add @ngworker/lumberjack |
Yarn CLI | yarn add @ngworker/lumberjack |
Refer to the following table to determine which version of Lumberjack is compatible with your Angular version.
Angular version | Lumberjack version |
---|---|
14.x | 14.x |
13.x | 2.x |
12.x | 2.x |
11.x | 2.x |
10.x | 2.x |
9.x | 2.x |
For a complete walkthrough video please visit @ngworker/lumberjack v2 - Show & Tell BLS024
To register Lumberjack, add LumberjackModule.forRoot()
to your root or core Angular module.
// (...)
import { LumberjackModule } from '@ngworker/lumberjack';
@NgModule({
imports: [
// (...)
LumberjackModule.forRoot(),
// (...)
],
// (...)
})
You must also register the log driver modules for the log drivers that you want to enable.
If you want to add the LumberjackHttpDriver
and the LumberjackConsoleDriver
, add the following code
// (...)
import { LumberjackModule } from '@ngworker/lumberjack';
import { LumberjackHttpDriverModule } from '@ngworker/lumberjack/http-driver';
import { LumberjackConsoleDriverModule } from '@ngworker/lumberjack/console-driver';
@NgModule({
imports: [
// (...)
LumberjackModule.forRoot(),
LumberjackConsoleDriverModule.forRoot(),
LumberjackHttpDriverModule.withOptions({
origin: '<app-name>',
storeUrl: '/api/logs',
retryOptions: { maxRetries: 5, delayMs: 250 },
}),
// (...)
],
// (...)
})
export class AppModule {}
For quick or simple use cases, you can use the LumberjackService
directly by passing logs to its log
method. However, we recommend implementing application-specific logger services instead. See the Best practices section.
First, inject the LumberjackService
where you want to use it.
import { Component } from '@angular/core';
import { LumberjackService } from '@ngworker/lumberjack';
@Component({
// (...)
})
export class MyComponent implements OnInit {
constructor(private lumberjack: LumberjackService) {}
// (...)
}
or using the inject
function
import { inject, Component } from '@angular/core';
import { LumberjackService } from '@ngworker/lumberjack';
@Component({
// (...)
})
export class MyComponent implements OnInit {
private readonly lumberjack = inject(LumberjackService);
// (...)
}
Then we can start logging. However, you'll also want to inject LumberjackTimeService
to maintain a high level of testability.
// (...)
import { LumberjackService, LumberjackTimeService } from '@ngworker/lumberjack';
// (...)
export class MyComponent implements OnInit {
private readonly lumberjack = inject(LumberjackService);
private readonly time = inject(LumberjackTimeService);
// (...)
ngOnInit(): void {
this.lumberjack.log({
level: LumberjackLevel.Info,
message: 'Hello, World!',
scope: 'MyComponent',
createdAt: this.time.getUnixEpochTicks(),
});
}
}
Optionally, we can pass one or more options to LumberjackModule.forRoot
.
Option | Type | Optional? | Description |
---|---|---|---|
format |
(log: LumberjackLog) => string | Yes | Pass a custom formatter to transform a log into a log message. |
levels |
LumberjackConfigLevels |
Yes | The root log levels defining the default log levels for log drivers. |
Lumberjack's configuration is flexible. We can provide a full configuration object, a partial option set, or no options at all.
Lumberjack replaces omitted options with defaults.
When the format
option is not configured, Lumberjack will use the following default formatter.
function lumberjackFormatLog({ scope, createdAt: timestamp, level, message }: LumberjackLog) {
return `${level} ${utcTimestampFor(timestamp)}${scope ? ` [${scope}]` : ''} ${message}`;
}
Where utcTimestampFor
is a function that converts Unix Epoch ticks to UTC 0 hours offset with milliseconds resolution.
When the levels
setting is not configured, log levels are configured depending on whether our application runs in development mode or production mode.
By default, in development mode, all log levels are enabled.
By default, in production mode, the following log levels are enabled:
- Critical
- Error
- Info
- Warning
Earlier, we briefly introduced the term log driver. This section explains in depth how to use and configure them and how to create custom log drivers.
A log driver is the conduit used by the Lumberjack to output or persist application logs.
Lumberjack offers basic log drivers out-of-the-box, namely the LumberjackConsoleDriver
and the LumberjackHttpDriver
.
Every log driver implements the LumberjackLogDriver
interface.
export interface LumberjackLogDriver<TPayload extends LumberjackLogPayload | void = void> {
readonly config: LumberjackLogDriverConfig;
logCritical(driverLog: LumberjackLogDriverLog<TPayload>): void;
logDebug(driverLog: LumberjackLogDriverLog<TPayload>): void;
logError(driverLog: LumberjackLogDriverLog<TPayload>): void;
logInfo(driverLog: LumberjackLogDriverLog<TPayload>): void;
logTrace(driverLog: LumberjackLogDriverLog<TPayload>): void;
logWarning(driverLog: LumberjackLogDriverLog<TPayload>): void;
}
The LumberjackLogDriverLog
holds a formatted string representation of the LumberjackLog
and the LumberjackLog
itself.
export interface LumberjackLogDriverLog<TPayload extends LumberjackLogPayload | void = void> {
readonly formattedLog: string;
readonly log: LumberjackLog<TPayload>;
}
Log drivers should make it possible to configure the logging levels on a per driver basis.
For example, we could use the default logging levels for the console driver, but only enable the critical and error levels for the HTTP driver as seen in the following example.
import { NgModule } from '@angular/core';
import { LumberjackLevel, LumberjackModule } from '@ngworker/lumberjack';
import { LumberjackConsoleDriverModule } from '@ngworker/lumberjack/console-driver';
import { LumberjackHttpDriverModule } from '@ngworker/lumberjack/http-driver';
@NgModule({
imports: [
LumberjackModule.forRoot({
levels: [LumberjackLevel.Verbose],
}),
LumberjackConsoleDriverModule.forRoot(),
LumberjackHttpDriverModule.forRoot({
levels: [LumberjackLevel.Critical, LumberjackLevel.Error],
origin: 'ForestApp',
storeUrl: '/api/logs',
retryOptions: { maxRetries: 5, delayMs: 250 },
}),
// (...)
],
// (...)
})
export class AppModule {}
Note, you can use the ngworker/lumberjack-custom-driver template Git repository to start a separate Lumberjack log driver workspace.
Let's create a simple log driver for the browser console.
import { inject, Injectable } from '@angular/core';
import { LumberjackLogDriver, LumberjackLogDriverConfig, LumberjackLogDriverLog } from '@ngworker/lumberjack';
import { consoleDriverConfigToken } from './console-driver-config.token';
@Injectable()
export class ConsoleDriver implements LumberjackLogDriver {
readonly config = inject(consoleDriverConfigToken);
logCritical({ formattedLog }: LumberjackLogDriverLog): void {
console.error(formattedLog);
}
logDebug({ formattedLog }: LumberjackLogDriverLog): void {
console.debug(formattedLog);
}
logError({ formattedLog }: LumberjackLogDriverLog): void {
console.error(formattedLog);
}
logInfo({ formattedLog }: LumberjackLogDriverLog): void {
console.info(formattedLog);
}
logTrace({ formattedLog }: LumberjackLogDriverLog): void {
console.trace(formattedLog);
}
logWarning({ formattedLog }: LumberjackLogDriverLog): void {
console.warn(formattedLog);
}
}
In the above snippet, the config is injected and assigned to the public config
property. Lumberjack uses this configuration to determine which logs the log driver should handle.
We might want to add some extra data not present in the LumberjackLog
to our log driver.
For such cases, Lumberjack exposes the LumberjackLog#payload
property.
/**
* A Lumberjack log entry
*/
export interface LumberjackLog<TPayload extends LumberjackLogPayload | void = void> {
/**
* Scope, for example domain, application, component, or service.
*/
readonly scope?: string;
/**
* Unix epoch ticks (milliseconds) timestamp when log entry was created.
*/
readonly createdAt: number;
/**
* Level of severity.
*/
readonly level: LumberjackLogLevel;
/**
* Log message, for example describing an event that happened.
*/
readonly message: string;
/**
* Holds any payload info
*/
readonly payload?: TPayload;
}
We can modify the ConsoleDriver
to handle such payload information
import { inject, Injectable } from '@angular/core';
import {
LumberjackLogDriver,
LumberjackLogDriverConfig,
LumberjackLogDriverLog,
LumberjackLogPayload,
} from '@ngworker/lumberjack';
import { consoleDriverConfigToken } from './console-driver-config.token';
export interface AnalyticsPayload extends LumberjackLogPayload {
angularVersion: string;
}
@Injectable()
export class ConsoleDriver implements LumberjackLogDriver<AnalyticsPayload> {
readonly config = inject(consoleDriverConfigToken);
logCritical({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
console.error(formattedLog, log.payload);
}
logDebug({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
console.debug(formattedLog, log.payload);
}
logError({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
console.error(formattedLog, log.payload);
}
logInfo({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
console.info(formattedLog, log.payload);
}
logTrace({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
console.trace(formattedLog, log.payload);
}
logWarning({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
console.warn(formattedLog, log.payload);
}
}
A driver module provides configuration and other dependencies to a log driver. It also provides the log driver, making it available to Lumberjack.
import { ModuleWithProviders, NgModule } from '@angular/core';
import { LumberjackLogDriverConfig, lumberjackLogDriverToken } from '@ngworker/lumberjack';
import { consoleDriverConfigToken } from './console-driver-config.token';
@NgModule({
providers: [
{
provide: lumberjackLogDriverToken,
useClass: ConsoleDriver,
multi: true,
},
],
})
export class ConsoleDriverModule {
static forRoot(config?: LumberjackLogDriverConfig): ModuleWithProviders<ConsoleDriverModule> {
return {
ngModule: ConsoleDriverModule,
providers: (config && [{ provide: consoleDriverConfigToken, useValue: config }]) || [],
};
}
}
The static forRoot()
method provides the consoleDriverConfigToken
.
If no configuration is passed, then the root LogDriverConfig
is used.
import { InjectionToken } from '@angular/core';
import { LumberjackLogDriverConfig, lumberjackLogDriverConfigToken } from '@ngworker/lumberjack';
export const consoleDriverConfigToken = new InjectionToken<LumberjackLogDriverConfig>('__CONSOLE_DRIVER_CONFIG__', {
factory: () => inject({ ...lumberjackLogDriverConfigToken, identifier: 'ConsoleDriver' }),
});
This is possible because the ConsoleDriver
has the same configuration options as the LumberjackLogDriverConfig
. We only have to include the driver identifier since it cannot be predefined.
For adding custom settings, see LumberjackHttpDriver.
The most important thing about the LumberjackConsoleDriverModule
is that it provides the LumberjackConsoleDriver
using the lumberjackLogDriverToken
with the multi
flag on. This allows us to provide multiple log drivers for Lumberjack at the same time.
The last step is to import this module at the root module of our application, as seen in the first Usage section.
@NgModule({
imports: [
LumberjackModule.forRoot(),
ConsoleDriverModule.forRoot(),
// (...)
],
// (...)
})
export class AppModule {}
For a more advanced log driver implementation, see LumberjackHttpDriver
Note, you can use the ngworker/lumberjack-custom-driver template Git repository to start a separate Lumberjack log driver workspace.
If you want your driver listed here, open a PR and follow the same format.
- @ngworker/lumberjack-firestore-driver, community log driver using Cloud Firestore as a log store.
- @ngworker/lumberjack-applicationinsights-driver, community log driver using Azure Application Insights as a log store.
The ngworkers teams offers hosting of a community log driver in the ngworker GitHub and @ngworker NPM organizations.
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.
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();
}
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 {
private readonly logger = inject(AppLogger);
private 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.
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
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> {
private 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.
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 {
private readonly logFactory: inject<LumberjackLogFactory<LogPayload>>(LumberjackLogFactory);
private readonly lumberjack = inject<LumberjackService<LogPayload>>(LumberjackService);
private readonly payload: LogPayload = {
angularVersion: VERSION.full,
};
private readonly scope = 'Forest App';
ngOnInit(): void {
const helloForest = this.logFactory
.createInfoLog('Hello, Forest!')
.withScope(this.scope)
.withPayload(this.payload)
.build();
this.lumberjack.log(helloForest);
}
}
Contributors to this repository are welcome to use the Wallaby.js OSS License to get test results immediately as you type, and see the results in your editor right next to your code.
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!