tush / lumberjack

Chop and cut Angular logs like a professional lumberjack.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Chop and cut Angular logs like a professional lumberjack.

Logo by Felipe Zambrano


MIT commitizen PRs styled with prettier All Contributors ngworker spectator Wallaby.js

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.

Features

  • ✅ Configurable multilevel logging
  • ✅ Plugin-based log driver architecture
  • ✅ Robust error handling
  • ✅ Console driver
  • ✅ HTTP driver
  • ✅ Logger base class
  • ✅ Lumberjack service
  • ✅ Best practices guide

Table of Contents

Installation

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

Compatibility

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.

Usage

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 {}

Using the LumberjackService

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(),
    });
  }
}

LumberjackModule

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.

Default options

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.

Default log levels

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

Log drivers

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 levels

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 {}

Creating a custom log driver

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.

Using a LumberjackLogPayload

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 || '');
  }
}

Creating a custom log driver module

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.

Using a custom log driver

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 {}

HTTP driver

For a more advanced log driver implementation, see LumberjackHttpDriver

Best practices

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.

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:

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();
}

Logger usage

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.

Simplifying with ScopedLumberjackLogger

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();
}

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 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.

Wallaby.js

Wallaby.js

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.

Contributors

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!

About

Chop and cut Angular logs like a professional lumberjack.

License:MIT License


Languages

Language:TypeScript 82.9%Language:HTML 14.6%Language:JavaScript 2.4%Language:SCSS 0.0%