2.2.0 • Published 24 days ago

typescript-logging-log4ts-style v2.2.0

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
24 days ago

TypeScript Logging Log4TS style

Logging library for Typescript projects. This is one of the available flavors, please see here for all available styles.

This flavor is well known and used by many logger solutions (like e.g. log4j). It allows creation of loggers based on a name, where each unique name represents a Logger. Often namespaces (packages/paths) and/or logically named groups are used to structure the output of the logging, like for example: "service.Account", "model.SomeObject", "view.Component" etc.

Using typescript-logging version 1? Please visit https://github.com/vauxite-org/typescript-logging/tree/release-1.x for more details. Consider upgrading to the latest version.

Getting started

This section describes how to get started quickly.

Installation

To install this flavor issue the following commands:

npm install --save typescript-logging
npm install --save typescript-logging-log4ts-style

The first command installs the "core" of typescript logging and is a required dependency. The second command installs the flavor (log4ts) to use.

Quick start

The following code defines a configuration file which creates a single Log4TSProvider, which is configured with a log level of Debug (default is Error). It also specifies a single 'group', which in this case matches everything based on namespace. More information about groups follows below.

Note that we give the provider a unique name, this is useful for external control ( see Dynamic control) for more details about that.

/*--- config/LogConfig.ts ---*/
import {LogLevel} from "typescript-logging";
import {Log4TSProvider} from "typescript-logging-log4ts-style";

export const log4TSProvider = Log4TSProvider.createProvider("AwesomeLog4TSProvider", {
  level: LogLevel.Debug,
  groups: [{
    expression: new RegExp(".+"),
  }]
});

The modules below use the provider created in LogConfig.ts in various ways.

/*--- model/Account.ts ---*/
import {log4TSProvider} from "../config/LogConfig";

/* Creates a logger called "model.Account" */
const log = log4TSProvider.getLogger("model.Account");

export interface Account {
  name: string;
}

export function createAccount(name: string): Account {
  log.debug(() => `Creating new account with name '${name}'.`);
  // Something fancy here to create it.
  return {name};
}
/*--- service/AccountService.ts ---*/
import {log4TSProvider} from "../config/LogConfig";
import {Account} from "../model/Account";

/* Creates a logger called "service.AccountService" */
const log = log4TSProvider.getLogger("service.AccountService");

export async function saveAccount(account: Account) {
  log.debug(() => `Will save account '${account.name}'.`);
  // ... Update code
  try {
    await someUpdateHere();
  }
  catch (e) {
    log.error(() => `Failed to save account '${account.name}'.`, e);
    throw e;
  }
}
/*--- Main.ts ---*/
import {log4TSProvider} from "../config/LogConfig";
import {createAccount} from "./model/Account";
import {saveAccount} from "./service/Account";

const log = log4TSProvider.getLogger("main");

/* Create an account and save it - log on success/error */
const account = createAccount("My Account");
saveAccount(account)
  .then(() => {
    log.debug("Successfully created account.", account.name);
  })
  .catch(e => {
    log.error("Ooops...", e);
  });

Now let's assume Main.ts is executed above. It successfully creates and saves the account. The logging will then roughly end up like below.

2021-12-31 23:14:26,273 DEBUG [model.Account] Creating new account with name 'My Account'.
2021-12-31 23:14:26,275 DEBUG [service.Account] Will save account 'My Account'.
2021-12-31 23:14:27,192 DEBUG [main] Successfully created account. ["My Account"] 

Logging

The quick start above gives an overview on how to create a provider, get loggers from it and then log with them. The Logger you use is a CoreLogger (extends it), the latter is from the core (basis) project typescript-logging.

A logger can log on different LogLevels:

  • Trace
  • Debug
  • Info
  • Warn
  • Error
  • Fatal
  • Off

Each level except 'Off' has a matching function on the Logger, for example trace for Trace and debug for Debug. The signatures of the function are the same for all of them. Note that the level 'Off' turns logging off completely.

This shows the signatures for debug.

interface CoreLogger {
  debug(message: LogMessageType, ...args: unknown[]): void;

  debug(message: LogMessageType, error: ExceptionType, ...args: unknown[]): void;
}

The above may look confusing, but we merge the functions in the implementation (and this is how to declare this in TypeScript).

Essentially it allows us to log messages in various formats like this:

// Assume 'log' is our Logger here.
log.debug("This is a simple message");
log.debug("This is a simple message and we log an Error (normally you'd catch it and then log it)", new Error("Some Exception"));
log.debug(() => "Simple message as lambda");
log.debug(() => "Simple message as lambda with Error", () => new Error("SomeOther"));
log.debug("Simple message with some random arguments", 100, "abc", ["some", "array"], true);
log.debug(() => "Simple message as lambda with Error and some random arguments", new Error("Some Exception"), 100, "abc", ["some", "array"], true);

The example of debug applies to any of the available functions (trace, debug, info, warn, error and fatal). What is logged exactly how will depend on the configuration and channel in use.

Configuration

This section provides more details on how to configure the log4ts style logging.

Options

A Log4TSProvider created without any options logs on LogLevel.Error and uses a default channel that logs to the console. Messages are formatted in a default sane format (see above for an example).

This code snippet creates a Log4TSProvider with default settings, the groups property is required to be specified and in this case we match every namespace. Note we also specify an identifier, it is recommended to specify the identifier if you intend to make use of the dynamic control (see in later sections).

import {Log4TSProvider} from "typescript-logging-log4ts-style";

const provider = Log4TSProvider.createProvider("ExampleProvider", {
  groups: [{
    identifier: "matchAll",
    expression: new RegExp(".+"),
  }]
});

The following configuration options are available and can be passed as optional argument when creating the Log4TSProvider. The documentation for each option can be read below (this is from the source code so your IDE will help you with that too).

type Log4TSConfigOptional = {
  /** Groups registered, a group is used to match loggers by name/expression path, at least 1 group is required */
  readonly groups: ReadonlyArray<Log4TSGroupConfig>;

  /**
   * Default LogLevel.
   */
  readonly level?: LogLevel;

  /**
   * What kind of channel to log to (a normal or raw channel).
   * In some cases the flavor maybe it to be changed in a more
   * fine-grained control where different channels can be used.
   *
   * However, by default this is the channel that is used without
   * any specif configuration.
   *
   * The default channel logs to console.
   */
  readonly channel?: LogChannel | RawLogChannel;

  /**
   * The argument formatter to use.
   */
  readonly argumentFormatter?: ArgumentFormatterType;

  /**
   * The date formatter to use to format a timestamp in the log line.
   */
  readonly dateFormatter?: DateFormatterType;
}

type Log4TSGroupConfigOptional = {
  /**
   * Expression to match a namespace (package/paths). For example new RegExp(".+") matches all, while
   * new RegExp("service.+") matches all within the service namespace.
   */
  readonly expression?: RegExp;

  /**
   * Identifier, this is used to make your life easier when you need to dynamically
   * control the log levels of this group and easily wish to recognize it.
   *
   * It is used in Log4TSProvider.updateRuntimeSettingsGroup(..) for example as the identifier there.
   *
   * If not set externally, it will be set to the expression's representation as string instead.
   */
  readonly identifier?: string;
}

With this knowledge we can create for example a provider that logs on a different log level, uses a custom date formatter and logs to a custom LogChannel like the following code snippet.

import {LogLevel} from "typescript-logging";
import {Log4TSProvider} from "typescript-logging-log4ts-style";

const provider = Log4TSProvider.createProvider("ProviderWithCustomSettings", {
  groups: [{
    identifier: "matchAll",
    expression: new RegExp(".+"),
  }],
  level: LogLevel.Info,
  dateFormatter: millisSinceEpoch => `${millisSinceEpoch}`, // Silly example, normally you'd e.g. create a Date and do custom formatting for that.
  channel: {
    type: "LogChannel",
    write: logMessage => console.log(`This is a custom channel: ${logMessage.message}`),
  },
});

The example is a bit silly to keep it simple. It writes the millis out literally as a string (you probably want to convert it to a Date and then return a custom string instead). It also specifies a custom channel which writes to the console pre-fixed by some text. However, all the options can be fully customized to your needs, so you can for example write to something else completely.

Channels

In the previous section we already briefly touched on the concept of channels. In this section we will look into more detail what it means and how they can be used to customize your logging if needed.

Using the standard settings a provider uses a predefined LogChannel, which logs to console (from DefaultChannels to be exactly).

There are two type of channels that can be used for configuration:

  • LogChannel
  • RawLogChannel

Use a custom LogChannel if you want to write a pre-formatted message (and possibly error stack) to another place than the default console. The LogChannel is defined as follows.

export interface LogChannel {

  readonly type: "LogChannel";

  /**
   * Write a complete LogMessage away, the LogMessage is
   * ready and formatted.
   */
  readonly write: (msg: LogMessage) => void;
}

Essentially it expects you to provide the write function. The type ( a discriminating union) is solely there to distinguish between the different channels and is always "LogChannel" for a LogChannel. The write function is called with for each LogMessage, and contains a message and optional error (formatted as stack). We have already seen how to create a custom LogChannel in the previous section (have a look above if you didn't read it yet).

The RawLogChannel can be used for more advanced scenarios. It allows us to completely format the message in any way we want as well as writing it elsewhere. The RawLogChannel is defined as follows.

export interface RawLogChannel {

  readonly type: "RawLogChannel";

  /**
   * Write the RawLogMessage away. The formatArg function can be used to format
   * arguments from the RawLogMessage if needed. By default the formatArg function will
   * use JSON.stringify(..), unless a global format function was registered
   * in which case it uses that.
   */
  readonly write: (msg: RawLogMessage, formatArg: (arg: unknown) => string) => void;
}

Similar to LogChannel it expects you to provide the write function. The main difference here is that the passed RawLogMessage contains a lot of information on what is supposed to be logged. It is up to the implementer of the function what to do with it and how to output it.

The RawLogMessage is defined as follows:

export interface RawLogMessage {

  /**
   * The level it was logged with.
   */
  readonly level: LogLevel;

  /**
   * Time of log statement in millis (since epoch)
   */
  readonly timeInMillis: number;

  /**
   * Contains the log name involved when logging (logger name or category name), by default it's always 1
   * but can be more in some advanced cases.
   */
  readonly logNames: string | ReadonlyArray<string>;

  /**
   * Original user message, and only that. No log level, no timestamp etc.
   */
  readonly message: string;

  /**
   * Error if present.
   */
  readonly exception?: Error;

  /**
   * Additional arguments when they were logged, else undefined.
   */
  readonly args?: ReadonlyArray<unknown>;
}

As can be seen there are various data fields present. There are some utility format functions present to help out if needed. These are formatArgument and formatDate which respectively format an argument and a time stamp as the library does for a LogChannel.

The following code snippet creates a provider which uses a RawLogChannel.

import {LogLevel} from "typescript-logging";
import {Log4TSProvider} from "typescript-logging-log4ts-style";

const provider = Log4TSProvider.createProvider("ProviderWithCustomSettings", {
  channel: {
    type: "RawLogChannel",
    write: (msg, formatArg) => {
      const date = new Date(msg.timeInMillis);
      const dateStr = `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`;
      console.log(`${dateStr} ${LogLevel[msg.level]} ${msg.message}`);
    },
  },
});

The code above logs only the LogLevel and message of the incoming RawLogMessage and creates a custom date format for it (this logs the date in dd-MM-yyyy format). The example is of course simplified, but what you should pick up from this is that you can fully control how to format the raw data and where to write to.

For clarity, the examples inline the channels for clarity, but you can of course write separate functions and/or classes for them as well if they get complex.

Dynamic control

This section gives an overview on how to dynamically control the log levels of your application. This means to change the levels after the normal setup/configuration, at a certain point in time.

This is very useful if your application for a user encounters an issue. In order to find out what is wrong you can ask for more information and tell the customer to enable certain parts of the logging and reproduce their problem with debug logging enabled.

There are two ways to achieve this.

Programmatic control

This allows your application to take matters in its own hands. You could add some option to change logging for example from the user interface of the application. You would do that then by changing the runtime settings of various LogGroups for a provider.

The Log4TSProvider exposes two functions for this purpose:

interface Log4TSProvider {
  // Docs left out for clarity (see your IDE code insight when using Log4TSProvider).
  readonly updateRuntimeSettingsGroup: (identifier: string, settings: Omit<RuntimeSettings, "channel">) => void;
  readonly updateRuntimeSettings: (settings: RuntimeSettings) => void;
}

The function updateRuntimeSettings allows changing the level for all groups and loggers of this provider. This applies to already created loggers and new ones by default.

The function updateRuntimeSettingsGroup allows a change of log level only, for given identifier of a group. The identifier of a group can be set when configured.

The following snippet will change the log level to Trace for all existing loggers and new loggers.

// Assume provider is a Log4TSProvider
provider.updateRuntimeSettings({level: LogLevel.Trace});

External control

This allows you to control the logging dynamically, but externally through the browser console running your client application.

This is available as function LOG4TS_LOG_CONTROL and can be imported from the library. This can then be used like:

import {LOG4TS_LOG_CONTROL} from "typescript-logging-log4ts-style";

const control = LOG4TS_LOG_CONTROL();
control.help(); // Shows help on how to get started
const controlProvider = control.getProvider("MyProviderName");
controlProvider.help(); // Shows help for control provider
controlProvider.showSettings(); // Will print current settings of the provider
controlProvider.update("debug"); // Updates all existing loggers (and new ones) to debug
controlProvider.update("trace", 5); // Updates 5th group of showSettings() to trace (can also pass identifier of group)
controlProvider.save(); // Save current state of loggers state to localStorage
controlProvider.reset(); // Resets to state of when control provider was fetched first

/* Restores to the saved state, this can be done at any time also after a browser restart, provided localStorage is available */
controlProvider.restore();

This is by default not available to the client as it needs to be your choice whether to expose this in the browser console or not. In order to do this, it comes down to one of:

  • Expose it in the index.ts(x) of your application and make sure your bundler exports it by prefixing with a variable for example (recommended).
  • Expose it on the window in some fashion

Expose using Webpack example

The following section shows how to export the LOG4TS_LOG_CONTROL function using Webpack, and make it available in the browser console. This allows it to be used in a client application when you need to debug something that is already delivered to a customer.

/* Your src/index.ts(x) */
export {LOG4TS_LOG_CONTROL} from "typescript-logging-log4ts-style";

Now make sure Webpack creates a bundle that exposes the index (simplified as ts-loaders etc. are left out, but it shows the relevant part on how to expose it).

/* webpack.config.js */
const path = require("path");

module.exports = {
  entry: "./src/index.ts",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "myapp.js",
    library: "myapp",
  },
};

Now when you'd open your application in your browser, open the console - you can type:

/* Console of your application */
const control = myapp.LOG4TS_LOG_CONTROL();
control.help();
// etc.

Expose using Rollup example

The following section shows how to export the LOG4TS_LOG_CONTROL function using Rollup, and make it available in the browser console. This allows it to be used in a client application when you need to debug something that is already delivered to a customer.

/* Your src/index.ts(x) */
export {LOG4TS_LOG_CONTROL} from "typescript-logging-log4ts-style";

Now make sure Rollup creates a bundle that exposes the index (simplified as plugins are left out, but it shows the relevant part on how to expose it).

/* rollup.config.js */
export default {
  input: "src/index.ts",
  output: [
    {
      file: "dist/myapp.js",
      name: "myapp",
      format: "iife",
    },
  ],
};

Now when you'd open your application in your browser, open the console - you can type:

/* Console of your application */
const control = myapp.LOG4TS_LOG_CONTROL();
control.help();
// etc.

Expose on window manually

/* Your src/index.ts(x) */
import {LOG4TS_LOG_CONTROL} from "typescript-logging-log4ts-style";

if (window !== undefined) {
  (window as any).appLogControl = LOG4TS_LOG_CONTROL;
}

The above code exposes "appLogControl" on the global window object if it exists. When it does the logging can be accessed in the browser's console as follows:

/* Console of your application */
const control = appLogControl();  // or: window.appLogControl()
control.help();
// etc.