Skip to content
🌴 A simple Node logging library for the cloud
JavaScript
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
lib
.gitignore
LICENSE
README.md
package-lock.json
package.json
yarn.lock

README.md

Canopi Codeship Status for robhaswell/canopi

🌴 A simple Node logging library for the cloud

const log = canopi('app')
log.setOutputStream(process.stdout)

log.info('something happened', { thing: value })
// {"timestamp":"...","name":"app","message":"something happened","thing":value}

const eventLog = log('events')
log.info({ kind: 'createWidget', widget: widget })
// {"timestamp":"...","name":"app:events","kind":"createWidget","widget":widget}

const widgetLog = eventLog({ widget: widget })
log.error(new Error(), { kind: 'widgetFailed' })
// {"timestamp":"...","name":"app:events","kind":"widgetFailed","widget":widget,"err":{ name, message, [code], stack }}

canopi.addErrorHandler((err) => { errors.report(err) })
// Call `errors.report` with any logged `log.<level>(err)`

Motivation

Canopi was created to support the logging needs of a microservices application deployed to Google Container Engine. In order to effectively debug a distributed application, it is required to be able to trace logs which relate to a given request, event or entity. This means that every log message needs to have some sort of context to be useful, usually supplied an object of relevant properties and values. Additionally, there is no requirement for alternative logging streams or level-based filtering, as logs are processed by log collection tooling.

Other logging solutions such as Bole or Bunyan make it difficult to log errors with some context, or are overly burdened with unnecessary functionality, so we created Canopi.

The API for Canopi is heavily inspired by Bole.

API

canopi('name')

Create a new logger with the supplied name to be attached to each output. If you keep a logger-per module you don't need to pass loggers around, keep your concerns separated.

canopi.setOutputStream(process.stdout)

Configure the output stream.

canopi({ aProperty: 'aValue'})

Create a new logger with the fields from the supplied object included in the output.

canopi('name', { aProperty: 'aValue'})

Create a new logger with the supplied name and fields to be included in the output.

logger#debug(), logger#info(), logger#warn(), logger#error()

Loggers have 4 roughly identical log methods, one for each of the supports log-levels. Log levels are recorded on the output and can be used to determine the level of detail passed to the output.

Log methods support the following types of input:

logger.<level>('message')

Providing a message string will output the message under the property message. No substitution is performed.

logger.<level>("message", { an: 'object' })

If an object is provided with a message, the behaviour is as above but with additional properties from the object included in the output.

logger.<level>({ an: 'object' })

Providing an object will log all the fields of the object.

logger.<level>(err)

Error objects: log output will include the error name, message, complete stack and also a code where there is one.

logger.<level>(err, { an: 'object' })

The error will be logged as above, but with additional properties from the object included in the output.

logger.<level>(err, 'message')

The error will be logged as above, but with the additional message property.

Note: Any other call signature will log a CanopiUsageError at the requested log level.

logger()

The logger object returned by canopi() is also a function that accepts a name or object argument. It returns a new logger whose name is the parent logger with the new name appended after a ':' character. This is useful for splitting a logger up for grouping events. Consider the HTTP server case where you may want to group all events from a particular request together:

var log = canopi('server')

http.createServer(function (req, res) {
  req.log = log(uuid.v4()) // make a new sub-logger
  req.log.info(req)

  //...

  // log an error against this sub-logger
  req.log.error(err)
})

Formatters

Canopi supports formatters, which are convenience functions the user can defined which format unweildy objects. A formatting function takes two arguments, a key and a function. The function will be called with the value of the key if anything is logged under that key.

canopi.setFormatter('upper', (value) => value.toUpperCase())
log.info({ upper: 'make me uppercase' })
// {"timestamp":"...","upper":"MAKE ME UPPERCASE"}

canopi.setFormatter('upper', null)
log.info({ upper: 'make me uppercase' })
// {"timestamp":"...","upper":"make me uppercase"}

A default formatter for request objects is available as canopi.requestFormatter. This can also be set (along with others in the future) with canopi.defaultFormatters().

Formatters are not chainable .

Error handlers

Canopi supports a list of error handlers which are called in turn with each error logged. Errors are passed to error handlers only if they are the first argument to a log method, they are an instance of Error, and the log level is error.

canopi.addErrorHandler(function callback(err) { ... })

Call callback with a single argument of the error instance whenever an error is logged as the first argument to a log method.

Other methods

canopi.quiet()

Silence the logger output by preventing it from writing to process.stdout and removing any error handlers.

You can’t perform that action at this time.