1.1.1 • Published 2 years ago

@schopp/log v1.1.1

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

Schopp Log

Logging library with a flexible, functional API.

This library offers a functional approach to logging which makes few assumptions about requirements and can handle a wide variety of use cases.

The logging interface provides five functions corresponding to severity levels:

  • logger.trace(message, data, context)
  • logger.debug(message, data, context)
  • logger.info(message, data, context)
  • logger.warn(message, data, context)
  • logger.error(message, data, context)

Beyond this, nothing is prescribed and the functionality offered by this library enables you to construct the exact logger you need.

Usage Examples

Here's a basic logger that writes JSON to stdout:

import log, { stream } from '@schopp/log'

const logger = stream.logger()

// {"level":"trace","message":"abc"}
logger.trace('abc')

// {"level":"debug","message":"def"}
logger.debug('def')

// {"level":"info","message":"ghi"}
logger.info('ghi')

// {"level":"warn","message":"jkl"}
logger.warn('jkl')

// {"level":"error","message":"mno"}
logger.error('mno')

You can pass data and context objects as the second and third arguments respectively to any logger call. Data pertains to the log event, while context reflects information about the logger itself:

import log, { stream } from '@schopp/log'

const logger = stream.logger()

// {"data":{"b":true,"d":"2021-10-14T08:16:36.918Z","n":2,"s":"data_message"},"level":"info","message":"abc"}
logger.info('abc', { b: true, d: new Date(), n: 2, s: 'data_message' })

// {"level":"info","message":"def","name":"app"}
logger.info('def', undefined, { name: 'app' })

// {"data":{"inContext":false,"inData":true},"inContext":true,"inData":false,"level":"info","message":"ghi"}
logger.info('ghi', { inContext: false, inData: true }, { inContext: true, inData: false })

Note that the default formatter for the stream logger copies context to the log event proper, while confining data to the data property. If you want different behaviour, you can supply your own formatter to the stream logger, or use a different logger.

The only compatibility requirement to use a logger with this library is that it implements the Logger type, so it is fairly easy to integrate your own custom logger. You can even use the functional logger to write a logger based on a single function.

Currying Data and Context

The with... library functions alter the behaviour of the logger itself through currying. For example, instead of passing the logger name manually as seen above, you can use withName to add it to all log events' context:

import log, { stream } from '@schopp/log'

const logger = log.withName('app', stream.logger())

// {"level":"debug","message":"abc","name":"app"}
logger.debug('abc')

// {"data":{"n":5},"level":"debug","message":"def","name":"app"}
logger.debug('def', { n: 5 })

You can add a timestamp to the context automatically using the withTime function:

import log, { stream } from '@schopp/log'

const logger = log.withTime(stream.logger())

// {"level":"debug","message":"abc","time":"2021-10-14T08:16:36.918Z"}
logger.debug('abc')

withName and withTime are convenience functions based on an underlying withContext function. You can use withContext directly to define custom context:

import log, { stream } from '@schopp/log'

const logger = log.withContext({ env: 'test' }, stream.logger())

// {"env":"test","level":"debug","message":"abc"}
logger.debug('abc')

You can also use withData in a similar fashion to automatically add event data to every log event.

Note that information added to log events via withContext or withData is overwritten if you pass the same keys with the log event itself. For example:

import log, { stream } from '@schopp/log'

const logger = log.withData({ n: 1 }, stream.logger())

// {"data":{"n":1},"level":"debug","message":"abc"}
logger.debug('abc')

// {"data":{"n":2},"level":"debug","message":"abc"}
logger.debug('abc', { n: 2 })

Filtering by Log Level

The withMaxLevel and withMinLevel filters enable you to block log events based on their log level. For example, to silence debug messages:

import log, { stream } from '@schopp/log'

const logger = log.withMinLevel('info', stream.logger())

// {"level":"info","message":"def"}
logger.info('def')

// (nothing)
logger.debug('abc')

Here's a more advanced example that routes log output to stdout or stderr based on severity:

import log, { stream } from '@schopp/log'

const logger = log.group([
  log.withMinLevel('error', stream.logger({ out: process.stdout })),
  log.withMaxLevel('warn', stream.logger())
])

// (to stderr) {"level":"error","message":"abc"}
logger.error('abc')

// (to stdout) {"level":"warn","message":"def"}
logger.warn('def')

Advanced Effects and Filtering

Other currying functions provide more control over the behaviour of the logger using input functions. For instance, if for some reason you wanted to reverse all log messages, you could use withEffect with your own EffectFn input:

import log, { stream } from '@schopp/log'

const logger = log.withEffect(
  (_level, msg, data, context) => [msg.split('').reverse().join(''), data, context],
  stream.logger()
)

// {"level":"debug","message":"fedcba"}
logger.debug('abcdef')

Or, if you wanted to filter log messages based on certain data flags, you could use withFilter with your own FilterFn input:

import log, { stream } from '@schopp/log'

const logger = log.withFilter(
  (_level, _msg, data) => !(data?.skip === true),
  stream.logger()
)

// {"level":"debug","message":"abc"}
logger.debug('abc')

// {"data":{"skip":false},"level":"debug","message":"def"}
logger.debug('def', { skip: false })

// (nothing)
logger.debug('ghi', { skip: true })

These are the two core functions that underpin the other with... convenience functions. It's simple to develop your own effects and filters for use with, or instead of, the other conveniences provided in this library.

Limitations

The following are stated limitations of this package and there are currently no plans to address them:

  • Messages must be strings. You cannot log data without a message, and supplying empty strings for that purpose is discouraged; logging should be an integrated and intentional practice. If you want to just 'dump' data, consider using console.debug() instead
  • Data and context objects may only be simple, flat data objects, as expressed by their Record<string, Scalar> types. This is a design choice to encourage considerate logging habits. There is nothing worse than running out of disk space due to bloated and indecipherable logs. If you must log particularly complex data, consider encoding it with JSON.stringify()