Skip to content

Extensible logger with simple class based, fluent interface api written in typescript.

License

Notifications You must be signed in to change notification settings

christoph-fricke/logging-library

Repository files navigation

logging-library

GitHub issues npm version Dependencies BundleSize

Extensible logging module for Node, Deno and browsers. Inspired by Deno std/log and .Net Core ILogger.

Goals

  • Provide a simple, clean and extensible logging api.
  • Class based, fluent interface api.
  • Zero dependencies.
  • Support for log scoping via contexts.
  • Support for different log levels.
  • Support for custom handlers so the api can be used in a wide range of szenarios.

Features

  1. Written in TypeScript with a fully typed, class based api.
  2. Create custom log handlers. Included are a ConsoleHandler and TestHandler.
  3. Six different log levels. VERBOSE, DEBUG, INFO, WARNING, ERROR and CRITICAL.
  4. Attach any metadata to you logs for advanced, structured logging.
  5. Fluent interface for easy configuration.
  6. Manage multiple logger instances globally within the LoggerStore.

Upcoming features

  1. Exported default logger which can be overwritten.
  2. More handlers directly provided.
  3. Your idea? Let me know!

Installation

npm i logging-library
# or
yarn add logging-library

Usage

More examples can be found in the examples folder.

Basic usage:

import { Logger, ConsoleHandler, LogLevel } from "logging-library";

// Create a logger instance and attach a console handler with level INFO and above as well
// as a custom handler with level WARN.
const logger = new Logger()
  .addHandler(new ConsoleHandler(LogLevel.INFO))
  .addHandler(new CustomHandler([LogLevel.WARN, LogLevel.INFO]));
// A handler can also be configured to only work for specific levels by passing in an array like above.

// addHandler takes an optional second argument which determines where the handler is
// actually added. Might be useful to only add a logger in Development. Example:
const logger2 = new Logger().addHandler(
  new ConsoleHandler(LogLevel.INFO),
  process.env.NODE_ENV === "development"
);

// Creates a LogRecord with level INFO that is passed to all handlers.
logger.info("Some log using the default context.");
// [Default]   Some log using the default context.

// Create a scoped logger that inherits its config from `logger`. Sets the context
// to 'Authentication' in all LogRecords created by this logger.
const scoped = logger.withContext("Authentication");

scoped.warning("This log is using the 'Authentication' context");
// [Authentication]  This log is using the 'Authentication' context

scoped.debug("Some debug information");
// No log output, since the ConsoleHandler requires at log level INFO

Usage with the LoggerStore:

import { Logger, ConsoleHandler, LogLevel, LoggerStore } from "logging-library";

// Create a logger instance and attach a console handler with level INFO.
const logger = new Logger().addHandler(new ConsoleHandler(LogLevel.INFO));
LoggerStore.add("console", logger);

// Somewhere else in a file far far away...
const logger = LoggerStore.get("console");

// Start logging
logger.info("Some log using the default context.");
// [Default]   Some log using the default context.

// And/Or create scopes
const scoped = logger.withContext("Authentication");

// Nobody stops you from storing the scoped logger if needed...
LoggerStore.add("auth", scoped);

Provide a custom format:

You can change the output format of the ConsoleHandler by providing a custom format function in a second options argument.

const myFormat = (record: ILogRecord) =>
  `${record.levelName} - ${record.message}`; // Return you custom format as a string.

const logger = new Logger().addHandler(
  new ConsoleHandler(LogLevel.INFO, { format: myFormat })
);

logger.info("With custom format.");
// INFO - With custom format.

Create a custom handler:

Currently this library supports a ConsoleHandler and TestHandler with more to come.

This library is build for extensible so you are welcome to create you custom handlers to meet you needs. More exotic handlers might send the logs to analytics or an error reporting tool.

Every handler must implement the ILogHandler interface to be usable.

interface ILogHandler {
  readonly level: LogLevel | LogLevel[];
  handle(record: ILogRecord): void;
}

This library provides an abstract BaseHandler class. It takes care of implementing the interface and filters all LogRecords that does not meet the required level. If you extend this class you only have to implement a log method which is called for all passed LogRecords.

import { BaseHandler } from "logging-library";

class CustomHandler extends BaseHandler {
  constructor(level: LogLevel | LogLevel[]) {
    super(level);
  }

  // Method can also be public. I recommend protected as it does not need to be
  // visible when instantiated.
  protected log(record: ILogRecord) {
    // ... Do whatever you like with the record. See src/handlers for inspiration.
  }
}

Attaching metadata:

This library attaches a metadata object to every created log record. Metadata can store any information you need. Add multiple metadata objects will merge the objects together with "new" objects overwriting keys of "old" objects.

// Metadata is provided to all handlers in the metadata property of a record
class MetaHandler extends BaseHandler {
  constructor(level: LogLevel | LogLevel[]) {
    super(level);
  }

  protected log(record: ILogRecord) {
    console.log(record.metadata);
  }
}

const logger = new Logger()
  .addMetadata({ pid: 123, hostname: "example.com" })
  .addMetadata({ key1: "Some more meta" })
  .addHandler(new MetaHandler(LogLevel.INFO));

// Allows you to check the current metadata in case you need to.
logger.metadata; // { pid: 123, hostname: "example.com", key1: "Some more meta" }

logger.info("Test message");
// Log from MetaHandler: { pid: 123, hostname: "example.com", key1: "Some more meta" }

Api (Also see reference)

Loglevel

Enum containing all valid log levels. Handlers that require a higher level than provided in a log record will not handle the record. All enum values from low to high:

VERBOSE = 10
DEBUG = 20
INFO = 30
WARNING = 40
ERROR = 50
CRITICAL = 60

ILogger

Interface that every logger implements.

interface ILogger {
  verbose(...args: unknown[]): void;
  debug(...args: unknown[]): void;
  info(...args: unknown[]): void;
  warning(...args: unknown[]): void;
  error(...args: unknown[]): void;
  critical(...args: unknown[]): void;

  withContext(context: string): ILogger;
  addHandler(
    handler: ILogHandler,
    condition?: boolean | (() => boolean)
  ): ILogger;
  addMetadata(metadata: Record<string, unknown>): ILogger;

  readonly context: string;
  readonly metadata: Record<string, unknown>;
}

LoggerStore

Little helper class that uses a static map to store loggers globally. Using the same key twice, overrides a logger.

class LoggerStore {
  static add(key: string, logger: ILogger): void;
  static get(key: string): ILogger | undefined;
  static remove(key: string): boolean;
}

ILogRecord

Every message logged by a logger is transformed into an object that implements ILogRecord. The constructed record is passed to every handler.

interface ILogRecord {
  /** Level used to create this record. */
  readonly level: LogLevel;
  /** String representation of the level. */
  readonly levelName: keyof typeof LogLevel;
  /** Context used for this record. */
  readonly context: string;
  /** Arguments that were passed to the logger. */
  readonly args: unknown[];
  /** Arguments formatted as a string. */
  readonly message: string;
  /** Timestamp at which the log record was created. */
  readonly date: Date;
  /** Extra meta data configured with a logger. */
  readonly metadata: Record<string, unknown>;
}

ConsoleHandler

Logs records the the corresponding method on the console object.

Requires a min. LogLevel when initialized. Everything below this level will not be logged. Accepts an optional options object as a second argument.

interface IConsoleHandlerOptions {
  format?: (record: ILogRecord) => string;
}

You can change the output format by providing a format function as an option. Default output format is: level: [context] - message.

The ConsoleHandler classed has a static toggle method with might be used in a browser to toggle of all logs in production but toggle them back on with a global function if needed. The toggle method returns a boolean, indicating whether console handlers are on or off after the call. Example for a global function in Typescript:

declare global {
  function toggleConsoleLogging(active?: boolean): void;
}

globalThis.toggleConsoleLogging = ConsoleHandler.toggle;

TestHandler

Handler that writes all logs into a message queue which. Allows for assertions about logs during testing.

class TestHandler extends BaseHandler {
  public readonly records: ILogRecord[] = [];

  constructor(level: LogLevel | LogLevel[] = LogLevel.VERBOSE) {
    super(level);
  }

  protected log(record: ILogRecord) {
    this.records.push(record);
  }
}