woody

0.1.2 • Public • Published

npm version Build Status

Tiny logging combinator library for node and the browser

import woody from 'woody';
const logger = woody
    .as(woody.bracketed())
    .to(woody.console)
    .fork(woody.level())
    .fork(woody.timestamp())
    .fork('woody');
 
logger.warn('foo', 'bar'); // => [WARN][2015-06-02 ...][woody] foo bar

Installation

$ npm install --save woody

Why another logging library?

I wanted a logging library that focuses on simplicity and expressiveness over configuration, that made making module-local loggers as simple as possible.

Project goals

  • Expressive, unobstrusive logging library
  • Simple to contextualize a logger, in other words: easy to put a line of output into context.
  • Compatibility with console.log in terms of all logging functions (same semantics)
  • As small as possible developer "buy-in" - it should be easy to pack your bags and leave woody for something else
  • Consistency with existing logging projects - e.g. same log-level names and weighting order.

Usage

The idea of woody is to make it as easy as possible to contextualize logging. The application or library using woody could have a root logger and then pass "contextualized" sub-loggers into different areas of the codebase.

Logger#log | trace | debug | info | warn | error | fatal

The .log(...) and friends are semantically identical to the console.log function and have a straight forward mapping provided by woody.console.

Logger#fork | module

The .fork(...) function takes either a string or a function and creates a new logger with the new context pushed onto it's context stack. The old logger remains in tact and operationally independent; It can be used as before.

Logger#if

The .if(...) function takes either a log level to "set the bar" and cull any levels lower than the given level, or a function that is evaluated on each log application.

Logger#to

The .to(...) function takes one or more committers as input, all of which will be invoked upon logging. A comitter is nothing but a function of shape (level, message [, callback]) => { ... }, where level indicates the log-level, the message is the rendered message string and the callback allows for asynchronous processing of the message. The caller of e.g. Logger#log will then recieve a promise that is either rejected or resolved based on the callback. The callback is of the shape function([err]) => { ... }.

Note, however, that since log and friends are conceptually unaware of what a committer is doing, and a logger can have multiple committers, the promise is the output of a Promise.all on all commit calls.

Also note that .to(...) actually creates a new logger that effectfully calls it's base's commit functions upon logging.

⚠️ Note that since functions can capture state at site of definition, threading down a function may not be a great idea. It's best used for internal loggers or where the function does not reference any outer state, such as e.g. a timestamped logger.

Levels

Levels are directly taken from Log4j for consistency:

const Level = {
  FATAL: 50000 // => Logger#fatal(...)
  ERROR: 40000 // => Logger#error(...)
  WARN:  30000 // => Logger#warn(...)
  INFO:  20000 // => Logger#info(...)
  DEBUG: 10000 // => Logger#debug(...)
  TRACE:  5000 // => Logger#trace(...)
};

Application domains aka modules

The .fork(...) function lends itself very well to creating application or library domain specific loggers:

import woody from 'woody';
 
class Foo {
  constructor(logger=woody.noop) {
    logger.info('created');
  }
}
 
class Application {
  constructor() {
    const logger = woody
      .as(woody.bracketed())
      .to(woody.console)
      .fork('app');
    logger.info('created');
    const foo = new Foo(logger.fork('foo'));
  }
}

Now:

const app = new Application();

Will print the following to the console:

> [app] created
> [app][foo] created

Culling

Woody allows to conditionally cull logs from a logger. It takes either a function or a log level to determine when to cull a log request.

import woody from 'woody';
 
const logger = woody
  .as(woody.bracketed())
  .to(woody.console)
  .fork(woody.level())
  .if(woody.level.INFO);
 
logger.warn('foo')  // => [WARN] foo
logger.info('foo')  // => [INFO] foo
logger.debug('foo') // =>
logger.trace('foo') // =>

Culling works the same way && would work, consider:

import woody from 'woody';
 
let shouldlog = true;
const logger = woody
  .as(woody.bracketed())
  .to(woody.console)
  .fork(woody.level())
  .if(woody.level.INFO)
  .if(() => shouldLog);
 
logger.warn('foo')  // => [WARN] foo
logger.info('foo')  // => [INFO] foo
logger.debug('foo') // =>
logger.trace('foo') // =>
 
shouldlog = false;
 
logger.warn('foo')  // =>
logger.info('foo')  // =>
logger.debug('foo') // =>
logger.trace('foo') // =>

This could, for example, make it easy to restrict logging at the top level and add more fine grained control later, but since it's essentially a binary && operation, any consecutive if can only further restrict.

Fallback to noop

Woody ships with a noop logger, that literally does nothing but satisfies the logger interface, such that application code does not have to null-check:

// ES5
function foo(bar, logger) {
  logger = logger || woody.noop;
  logger.info('test');
}
 
// ES6+
function foo(bar, logger=woody.noop) {
  logger.info('test');
}

⚠️ noop only means it does not render or commit anything. Sequencing it with another logger using .sequence will not return a noop logger, but a logger that applies both the noop and the second logger.

Where's the stock logger X?

For now I have decided to not include "simple" resource-based loggers such as logging to file. Resource management is not the responsibility nor the intended purpose of woody. Writing a file logger is trivial, however:

import fs from 'fs';
import woody from 'woody';
 
// open the file
const fd = fs.openSync('/var/log/my-log.log', 'w');
const logger = woody
  .as(woody.bracketed())
  .to((level, message) => {
    // write to the file
    try {
      fs.writeSync(fd, `${level}${message}`);
    } catch() { /* ... */ }
  });

Since a logger instance should not have to care about the resources it happens to consume, the management of these is out-sourced. Should a committer defect, it has to handle that error itself.

Integration with debug

There is out-of-the-box integration with the debug package on npm:

import woody from 'woody';
 
const debug = woody.debug().fork('woody')
    , debugFoo = debug.fork('foo');
 
debug.log('foo');
debugFoo.log('qux');
debug.log('bar');
debugFoo.log('biz');
 

yields

woody foo +0ms
woody:foo qux +1ms
woody bar +2ms
woody:foo biz +3ms

Readme

Keywords

Package Sidebar

Install

npm i woody

Weekly Downloads

19

Version

0.1.2

License

MIT

Last publish

Collaborators

  • felixschl