@livy/group-handler

Forwards Livy log records to multiple handlers


Keywords
logging, logger, logs, log, loglevel, tools, monolog, handler, group, wrapper, multiple, parallel
License
MIT
Install
npm install @livy/group-handler@1.0.4

Documentation

Livy logo: The "Livy" lettering next to a scroll


Tests Test coverage on CodeCov

Livy is a flexible JavaScript logger heavily inspired by Monolog.

Motivation

Livy Quote: "The study of history is the best medicine."

Livy aims to be the one logger used throughout your Node.js application. Therefore, it does not assume anything about your project and how it wants to do logging, but provides all the buildings blocks to quickly assemble a logger suited to your needs. It basically is a logging construction kit.

Want to see an example?

Impatient? Try Livy out on repl.it.

const { createLogger } = require('@livy/logger')
const { SlackWebhookHandler } = require('@livy/slack-webhook-handler')
const { RotatingFileHandler } = require('@livy/rotating-file-handler')

const logger = createLogger('my-app-logger', {
  handlers: [
    // Write daily rotating, automatically deleted log files
    new RotatingFileHandler('/var/log/livy-%date%.log', {
      level: 'info',
      maxFiles: 30
    }),

    // Get instant notifications under critical conditions
    new SlackWebhookHandler('https://some-slack-webhook.url', {
      level: 'critical'
    })
  ]
})

logger.debug("I'm going nowhere. :(")
logger.info("I'm going to the log file.")
logger.emergency("I'm going to the log file and to the slack channel!")

Features

  • 🎾 Flexible: The basic logging infrastructure and the log-writing mechanisms are completely decoupled and extensible.
  • 🌌 Universal: Livy was created for Node.js, but most components work in the browser just as well.
  • ⌨️ Great IDE support: Livy is written in TypeScript for great auto completion and easier bug spotting.
  • ⚓️ Stable: Livy has a comprehensive test suite.

Table of Contents

Installation

Install the logger factory from npm:

npm install @livy/logger

Get Started

You start by installing the @livy/logger package. This package only contains the overall structure of the logger but no concrete logging functionality — those are installed separately as components.

So now think about how your logging should go. Want to write errors to a log file? Install the @livy/file-handler and set up your logger:

const { createLogger } = require('@livy/logger')
const { FileHandler } = require('@livy/file-handler')

const logger = createLogger('my-app-logger', {
  handlers: [new FileHandler('error.log', { level: 'error' })]
})

That's it, you can start using the logger instance:

logger.error('Something went wrong!', { location: 'main.js:50' })

And there you go!

Learn more:

  • Logger: Learn more about how to configure the logger factory.
  • Handlers: See what handlers are available besides writing to a file.
  • Concepts: Learn about the core concepts of Livy.

Concepts

Most of Livy's concepts (and, in fact, a lot of its source code as well) are borrowed from the Monolog library. There are some differences to make it fit nicely into the JavaScript world, but if you're coming from the PHP Monolog world, you'll probably be pretty familiar with how things work in Livy.

Most importantly, Livy adheres to the log levels defined in RFC 5424 and offers a logger interface compatible with PSR-3. This also means that you may use Livy to visualize PSR-3 compatible PHP logs in the browser. For this use case, take a look at the BrowserConsoleHandler and the DomHandler.

Log Levels

The log levels Livy uses are those defined in the syslog protocol, which are:

Level Severity Description Examples
debug 7 Detailed debug information
info 6 Interesting events User logs in, SQL logs
notice 5 Normal but significant events
warning 4 Exceptional occurrences that are not errors Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong
error 3 Runtime errors that do not require immediate action but should typically be logged and monitored
critical 2 Critical conditions Application component unavailable, unexpected exception
alert 1 Action must be taken immediately Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up.
emergency 0 System is unusable

Handlers

Handlers are the core unit of Livy. A handler is an entity which receives log records and, if it is responsible, dispenses them towards some target (for example a file). Without a handler, a logger just does nothing.

Each logger can have an arbitrary amount of handlers. They can be added through the handlers set:

logger.handlers
  .add(new FileHandler('livy.log'))
  .add(new ConsoleHandler())

Note that handlers attached to a logger are used as a stack. That means that, when logging something, handlers are executed in reversed insertion order. This allows to add temporary handlers to a logger which can prevent further handlers from being executed.

Formatters

It's the job of formatters to turn log records into strings of all sorts — plain text, HTML, CSV, JSON, you name it.

Formatters are used by most handlers and can be injected through their options:

const { JsonFormatter } = require('@livy/json-formatter')

// Make the file handler write newline-delimited JSON files
const fileHandler = new FileHandler('/var/log/livy.log', {
  formatter: new JsonFormatter()
})

// You may also set the formatter afterwards:
fileHandler.formatter = new JsonFormatter()

All handlers that accept an optional formatter also have a default formatter which is available at handler.defaultFormatter.

Processors

Processors take a log record and modify it. This usually means that they add some metadata to the record's extra property.

A processor can be added to a logger directly (and is subsequently applied to log records before they reach any handler), but many handlers support handler-specific processors as well. In both cases, processors are accessed via the processors set:

logger.processors.add(record => {
  record.extra.easter_egg = '🐣'
  return record
})

Log Records

The log record is an internal concept that you'll mostly only need to understand if you intend to write your own handlers/formatters/loggers.

Explained by example: When having this logger...

const logger = require('@livy/logger').createLogger('my-app-logger')

...then calling a log method like this...

logger.debug('Hello World!', { from: 'Germany' })

...would internally create this object:

{
  level: 'debug',
  severity: 7,
  message: 'Hello World!',
  context: { from: 'Germany' },
  extra: {}, // Any extra data, usually attached by processors
  channel: 'my-app-logger'
  datetime: {...} // A Luxon DateTime object of the current date and time
}

That object above is an example of a log record. Log records are passed around many places and if you're not writing your own components, you can just ignore their existence most of the time.

However, for some components it's useful knowing the concept of log records to understand how the component can be configured.

Take the very basic LineFormatter: It allows you to specify an include option which is nothing but an object telling the formatter which properties of the log record to include in the output:

const { createLogger } = require('@livy/logger')
const { ConsoleHandler } = require('@livy/console-handler')
const { LineFormatter } = require('@livy/line-formatter')

const logger = createLogger('my-app-logger', {
  handlers: [
    new ConsoleHandler({
      formatter: new LineFormatter({
        include: { datetime: false, context: false }
      })
    })
  ]
})

logger.debug('Hello World!')
// Prints: DEBUG Hello World!

Bubbling

Handlers may indicate that they completely handled the current log record with no need for further handlers in the logger to take action. They do so by returning Promise<true> from the handle method (or true from handleSync, respectively).

Because handlers usually don't interact with each other, bubbling prevention is rather exceptional. Most handlers will only do it if you explicitly instruct them to do so (by setting the bubble option to true) with some notable exceptions (e.g. the NullHandler which always prevents bubbling after handling a log record).

Since handlers operate as a stack (as explained in the handlers section), the concept of bubbling allows for easy temporary overriding of a logger's behavior. You may, for example, make a logger ignore all its configured handlers and only log to the terminal by doing this:

const { ConsoleHandler } = require('@livy/console-handler')

logger.handlers.add(new ConsoleHandler({ bubble: false }))

Synchronous and Asynchronous Logging

A logger is often run in many different (synchronous and asynchronous) contexts. Therefore, Livy allows handlers to implement both a synchronous and an asynchronous way to do their jobs and tries to invoke them appropriately. However, since Node.js is an inherently concurrent environment, there are cases where a synchronous way is simply not available (especially for anything network-related).

That's why by default, Livy runs in so-called "mixed mode". That is: handlers are instructed to do their work synchronously wherever possible, but asynchronous actions are allowed as well. However, this behavior comes with a grave tradeoff: Since the logger cannot wait for asynchronous handlers to finish their work, it has no insight into the bubbling behavior intended by asynchronous handlers or whether or not they even completed successfully.

Mixed mode therefore certainly is a suboptimal way to run Livy and you may want to use one of the more clear-cut alternatives: sync mode or async mode. Both modes do have the advantage that they properly invoke all their handlers in-order and handle their errors, but of course these modes have tradeoffs as well:

Sync Mode

You can create a sync mode logger by setting the factory's mode option to "sync":

const { createLogger } = require('@livy/logger')
const { SlackWebhookHandler } = require('@livy/slack-webhook-handler')

const logger = createLogger('my-app-logger', {
  mode: 'sync'
})

// This will throw an error:
logger.handlers.add(new SlackWebhookHandler('https://some-slack-webhook.url'))

SlackWebhookHandler is an exclusively asynchronous handler and can not be used in a synchronous environment. This is the tradeoff of a synchronous handler.

Therefore, sync mode is recommended if you have no need for exclusively asynchronous handlers.

Async Mode

You can create a fully async mode logger by setting the factory's mode option to "async":

const { createLogger } = require('@livy/logger')

const logger = createLogger('my-app-logger', {
  mode: 'async'
})

This allows any handler to be used with the logger. However, you now have to await every logging action you perform:

// This is correct:
await logger.debug('foo')
await logger.info('bar')

// This is correct as well:
logger.debug('foo').then(() => logger.info('bar'))

// This is incorrect:
logger.debug('foo')
logger.info('bar')

// Oops! Now we have no guarantee that handlers of the "foo" log *actually* ran before the "bar" log.

This is the tradeoff of asynchronous handlers: You'll have to use highly contagious async/await constructs or any other way to handle the Promises returned by an asynchronous logger, which might be undesirable in your codebase.

To sum up: Use async mode if all places where the logger is used can afford to be asynchronous.

Usage in Browsers

Many Livy components work in the browser. Some are even explicitely created for a browser environment.

However, please take notice that these components still use a Node.js-style module format which is not natively supported by browsers. You'll need a bundler like Parcel, Webpack, Rollup or Browserify to make them browser-ready.

Alternatively, you can use a CDN like Pika to try out Livy without using npm.

Components

These are the components (handlers, formatters, processors) officially maintained by the Livy team. They are not "included" in Livy because each component we provide each resides in a separate package. This makes them a little more cumbersome to install, but it helps us properly decoupling our code and keeps your node_modules folder a whole lot cleaner.

Our components library is by far not as comprehensive as that of Monolog. If you're missing any component, feel free to open an issue and explain your use case!

Handlers

What are handlers?

Write to Screen or Files

  • ConsoleHandler: Writes log records to a terminal console.
  • FileHandler: Writes log records to a file.
  • RotatingFileHandler: Stores log records to files which are rotated by date/time or file size. Discards oldest files when a certain configured number of maximum files is exceeded.

Network

Browser

Utility

  • ArrayHandler: Pushes log records to an array.
  • NoopHandler: Handles anything by doing nothing and does not prevent other handlers from being applied. This can be used for testing.
  • NullHandler: Does nothing and prevents other handlers in the stack to be applied. This can be used to put on top of an existing handler stack to disable it temporarily.

Wrappers

Wrappers are a special kind of handler. They don't dispense log records themselves but they modify the behavior of the handler(s) they contain.

  • FilterHandler: Restrict which log records are passed to the wrapped handler based on a test callback.
  • GroupHandler: Distribute log records to multiple handlers.
  • LevelFilterHandler: Restrict which log records are passed to the wrapped handler based on a minimum and maximum level.
  • RestrainHandler: Buffers all records until an activation condition is met, in which case it flushes the buffer to its wrapped handler.

Formatters

What are formatters?

Processors

What are processors?

Contributing

When contributing code, please consider the following things:

  • Make sure yarn test passes.
  • Add tests for new features or for bug fixes that have not previously been caught by tests.
  • Use imperative mood for commit messages and function doc blocks.
  • Add doc blocks to interfaces, classes, methods and public properties.
    • You may consider omitting them for constructors or for interfaces whose purpose is very obvious (e.g. a MyThingOptions interface next to a MyThing class).
    • Parameter descriptions of function doc blocks should be aligned:
      /**
       * @param bar           This description is indented very far
       * @param longParameter just to be aligned with this description.
       */
      function foo(bar: number, longParameter: string) {}
    • Return annotations are only needed where the function's description does not make it obvious what's returned.

To Do

  • Find a good alternative for Luxon to support timezone-related record timestamps. Luxon is great, but it's pretty heavyweight for a core dependency just doing date handling in a library that is potentially run in the browser.
  • Native Node.js ES module support is not ready yet. While there are compiled .mjs files, this will only work once all dependencies (and first and foremost Luxon as the only core dependency) support them. There's also some minor tweaking to do to get OS-specific EOL characters to function correctly which might require support for top-level await in Node.js.

Credit