Chop and cut Angular logs like a professional lumberjack.
Lumberjack is a versatile Angular logging library, specially 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 bundled log drivers or create and use custom log drivers.
- ✅ 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 |
---|---|
Angular CLI | ng add @ngworker/lumberjack |
NPM CLI | npm install @ngworker/lumberjack |
Nx CLI | nx add @ngworker/lumberjack |
Yarn CLI | yarn add @ngworker/lumberjack |
Lumberjack version 2.x has verified compatibility with the following Angular versions.
Angular version | Lumberjack 2.x support |
---|---|
11.0.x | ✅ |
10.2.x | ✅ |
10.1.x | ✅ |
10.0.x | ✅ |
9.1.x | ✅ |
9.0.x | ✅ |
If the version you are using is not listed, please raise an issue in our GitHub repository.
To start using Lumberjack, import it in your root or core Angular module.
// ...
import { LumberjackModule } from '@ngworker/lumberjack';
@NgModule({
imports: [
LumberjackModule.forRoot(),
// (...)
],
// (...)
})
export class AppModule {}
You must also import log driver modules for the log drivers that you want to enable.
// ...
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: 'ForestApp',
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) {}
// (...)
}
Then you can start logging.
// (...)
export class MyComponent implements OnInit {
// (...)
ngOnInit(): void {
this.lumberjack.log({
level: LumberjackLogLevel.Info,
message: 'Hello, World!',
scope: 'MyComponent',
createdAt: Date.now(),
});
}
}
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. You 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 your application is running in development mode or production mode.
By default, all log levels are enabled in development mode.
In production mode, the following log levels are enabled by default:
- Critical
- Error
- Info
- Warning
Earlier, we briefly introduced the term log driver. This section explains in depth how to use and configure them as well as 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 {}
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 { lumberjackConsoleToken, LumberjackConsole } from '@ngworker/lumberjack/console-driver';
import { consoleDriverConfigToken } from './console-driver-config.token';
@Injectable()
export class ConsoleDriver implements LumberjackLogDriver {
constructor(
@Inject(consoleDriverConfigToken) public config: LumberjackLogDriverConfig,
@Inject(lumberjackConsoleToken) private console: LumberjackConsole
) {}
logCritical({ formattedLog }: LumberjackLogDriverLog): void {
this.console.error(formattedLog);
}
logDebug({ formattedLog }: LumberjackLogDriverLog): void {
this.console.debug(formattedLog);
}
logError({ formattedLog }: LumberjackLogDriverLog): void {
this.console.error(formattedLog);
}
logInfo({ formattedLog }: LumberjackLogDriverLog): void {
this.console.info(formattedLog);
}
logTrace({ formattedLog }: LumberjackLogDriverLog): void {
this.console.trace(formattedLog);
}
logWarning({ formattedLog }: LumberjackLogDriverLog): void {
this.console.warn(formattedLog);
}
}
There is nothing special about it. The only remarkable thing is that the config is passed down its constructor and that it is assigned to the public config
property. Lumberjack uses this configuration to determine which logs should the driver handle.
It is possible that our driver needs some extra data not provided by the LumberjackLog
.
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 { LumberjackConsole, lumberjackConsoleToken } from '@ngworker/lumberjack/console-driver';
import { consoleDriverConfigToken } from './console-driver-config.token';
export interface AnalyticsPayload extends LumberjackLogPayload {
angularVersion: string;
}
@Injectable()
export class ConsoleDriver implements LumberjackLogDriver<AnalyticsPayload> {
constructor(
@Inject(consoleDriverConfigToken) public config: LumberjackLogDriverConfig,
@Inject(lumberjackConsoleToken) private console: LumberjackConsole
) {}
logCritical({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
this.console.error(formattedLog, log.payload || '');
}
logDebug({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
this.console.debug(formattedLog, log.payload || '');
}
logError({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
this.console.error(formattedLog, log.payload || '');
}
logInfo({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
this.console.info(formattedLog, log.payload || '');
}
logTrace({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
this.console.trace(formattedLog, log.payload || '');
}
logWarning({ formattedLog, log }: LumberjackLogDriverLog<AnalyticsPayload>): void {
this.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),
});
This is possible because the ConsoleDriver
has the same configuration options as the LumberjackLogDriverConfig
. 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
Every log can be represented as a combination of its level, creation time, message, and scope. Using inline logs with the LumberjackService
can cause structure duplication and/or de-standardization.
The following practices are recommended to mitigate these problems.
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
:
import { Injectable } from '@angular/core';
import { LumberjackLevel } from '../logs/lumberjack-level';
import { LumberjackLogLevel } from '../logs/lumberjack-log-level';
import { LumberjackLogPayload } from '../logs/lumberjack-log-payload';
import { LumberjackTimeService } from '../time/lumberjack-time.service';
import { LumberjackLoggerBuilder } from './lumberjack-logger.builder';
import { LumberjackService } from './lumberjack.service';
@Injectable()
export abstract class LumberjackLogger<TPayload extends LumberjackLogPayload | void = void> {
constructor(protected lumberjack: LumberjackService<TPayload>, protected time: LumberjackTimeService) {}
protected createCriticalLogger(message: string): LumberjackLoggerBuilder<TPayload> {
return this.createLoggerBuilder(LumberjackLevel.Critical, message);
}
protected createDebugLogger(message: string): LumberjackLoggerBuilder<TPayload> {
return this.createLoggerBuilder(LumberjackLevel.Debug, message);
}
protected createErrorLogger(message: string): LumberjackLoggerBuilder<TPayload> {
return this.createLoggerBuilder(LumberjackLevel.Error, message);
}
protected createInfoLogger(message: string): LumberjackLoggerBuilder<TPayload> {
return this.createLoggerBuilder(LumberjackLevel.Info, message);
}
protected createTraceLogger(message: string): LumberjackLoggerBuilder<TPayload> {
return this.createLoggerBuilder(LumberjackLevel.Trace, message);
}
protected createWarningLogger(message: string): LumberjackLoggerBuilder<TPayload> {
return this.createLoggerBuilder(LumberjackLevel.Warning, message);
}
protected createLoggerBuilder(level: LumberjackLogLevel, message: string): LumberjackLoggerBuilder<TPayload> {
return new LumberjackLoggerBuilder<TPayload>(this.lumberjack, this.time, level, message);
}
}
By extending LumberjackLogger
, we only have to be worry about the message and scope of our pre-defined logs.
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 {
public static scope = 'Forest App';
constructor(lumberjack: LumberjackService, time: LumberjackTimeService) {
super(lumberjack, time);
}
forestOnFire = this.createCriticalLogger('The forest is on fire').withScope(AppLogger.scope).build();
helloForest = this.createInfoLogger('HelloForest').withScope(AppLogger.scope).build();
}
Now that we have defined our first Lumberjack logger, let's use it to log logs from our application.
import { 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 {
constructor(private logger: AppLogger, private forest: 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.
Alternative to the LumberjackLogger
interface, where we need to manually specify the lumberjack log scope, we could use the ScopedLumberjackLogger
.
The result AppLogger
would be
import { Injectable } from '@angular/core';
import { LumberjackService, LumberjackTimeService, ScopedLumberjackLogger } from '@ngworker/lumberjack';
@Injectable({
providedIn: 'root',
})
export class AppLogger extends ScopedLumberjackLogger {
public scope = 'Forest App';
constructor(lumberjack: LumberjackService, time: LumberjackTimeService) {
super(lumberjack, time);
}
forestOnFire = this.createCriticalLogger('The forest is on fire').build();
helloForest = this.createInfoLogger('HelloForest').build();
}
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 scenario.
import { Injectable, VERSION } from '@angular/core';
import {
LumberjackLogPayload,
LumberjackService,
LumberjackTimeService,
ScopedLumberjackLogger,
} from '@ngworker/lumberjack';
export interface LogPayload extends LumberjackLogPayload {
angularVersion: string;
}
@Injectable({
providedIn: 'root',
})
export class AppLogger extends ScopedLumberjackLogger<LogPayload> {
private static payload: LogPayload = {
angularVersion: VERSION.full,
};
public scope = 'Forest App';
constructor(lumberjack: LumberjackService<LogPayload>, time: LumberjackTimeService) {
super(lumberjack, time);
}
forestOnFire = this.createCriticalLogger('The forest is on fire').build();
helloForest = this.createInfoLogger('HelloForest').withPayload(AppLogger.payload).build();
}
The usage of the AppLogger
remains the same using a LumberjackLogger
or ScopedLumberjackLogger
, with payload or without.
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):
Nacho Vazquez 💬 🐛 💼 💻 📖 💡 🤔 🚇 🚧 📆 👀 🛡️ |
Lars Gyrup Brink Nielsen 🐛 💻 📖 💡 🤔 🧑🏫 🔌 👀 |
Santosh Yadav 💻 📖 💡 🚇 🔌 |
Alex Okrushko 💻 🤔 🧑🏫 🔬 |
Dzhavat Ushev 📖 |
This project follows the all-contributors specification. Contributions of any kind welcome!