bs-logger

Bare simple logger for NodeJS

Usage no npm install needed!

<script type="module">
  import bsLogger from 'https://cdn.skypack.dev/bs-logger';
</script>

README

B.S. Logger Build Status Coverage Status Beerpay Beerpay

Opinionated bare simple logger for NodeJS (with TypeScript typings).

BSLogger has been created after being disapointed not finding a matching logger on the internet. Not that others aren't good, they just did not fit what I was looking for.

Here is what I was looking for (and tried to implemented in BSLogger):

  • light memory usage
  • easily extendable (see child method)
  • as few dependencies as possible
  • ability to define all targets in a string (so that ENV vars can be used)
  • when using file targets, not re-opening them
  • reasonable defautls:
    • logs warnings and above to stderr
    • logs JSON to files
  • no overhead if it's not going to log anywhere

TL,DR:

Install:

npm install --save bs-logger
# or
yarn add bs-logger

Use:

const { logger } = require('bs-logger');
//    or
// import logger from 'bs-logger';
//    or
// import { logger } from 'bs-logger';
//    as default exports the logger

logger('foo');
logger.debug('bar');
logger.warn({foo: 'bar'}, 'dummy', 'other'/*, ...*/);

More complex example:

// env MY_LOG_TARGETS="debug.log:trace,stderr:warn%json"
import { createLogger } from 'bs-logger';
const logger = createLogger({
  context: {namespace: 'http'},
  targets: process.env.MY_LOG_TARGETS,
  translate: (m) => {
    if (process.env.NODE_ENV === 'production') {
      m.context = { ...m.context, secret: null };
    }
    return m;
  },
});
// [...]
logger.debug({secret: 'xyz'}, 'trying to login')
// will log into debug.log `trying to login` with secret in the context except in prod

const login = logger.wrap(function login() {
  // your login code
})
// [...]
login();
// will log `calling login` with the arguments in context

Usage

Creating a logger

Root logger

BSLogger exports a global logger lazyly created on first use, but it is advised to create your own using the createLogger() helper:

  • If you are using it in a library wich is meant to be re-distributed:

    import { createLogger, LogContexts } 'bs-logger';
    const logger = createLogger({ [LogContexts.package]: 'my-pacakge' });
    
  • If you are using it in an application of your own:

    import { createLogger, LogContexts } 'bs-logger';
    const logger = createLogger({ [LogContexts.application]: 'my-app' });
    

Child logger

Child loggers extends the context, targets and message translators from their parent. You create a child logger using the child method:

const childLogger = logger.child({ [LogContexts.namespace]: 'http' })
// childLogger becomes a new logger

Logging

Any helper to log within BSLogger is a function which has the same signature as console.log(), and also accepts an optional first argument being the context. A context is any object, with some specific (but optional) properties which we'll see later.

logMethod(message: string, ...args: any[]): void
  // or
logMethod(context: LogContext, message: string, ...args: any[]): void

Directly

You can log using any logger as a function directly (if the logger or its possible parent(s) has not been created with any log level in its context, no level will be attached):

import { createLogger } from 'bs-logger'
const logger = createLogger()
// [...]
logger('my message');

Using level helpers

BSLogger is aware of 6 log levels (trace, debug, info, warn, error and fatal) but you can create your owns. A log level is basically a number. The higher it is, the more important will be the message. You can find log levels constants in LogLevels export:

import { LogLevels } from 'bs-logger';

const traceLevelValue = LogLevels.trace;
const debugLevelValue = LogLevels.debug;
// etc.

For each log level listed above, a logger will have a helper method to directly log using this level:

import { createLogger } from 'bs-logger'
const logger = createLogger()
// [...]
logger.trace('foo')
logger.debug('bar')
// etc.

Those helpers are the equivalent to

logger({ [LogContexts.logLevel]: level }, 'foo')

...except that they'll be replaced with an empty function on the first call if their level will not be handled by any target.

Wrapping functions

Each logger has a wrap method which you can use to wrap a function. If there is no matching log target, the wrap method will simply return your function, else it'll wrap it in another function of same signature. The wrapper will, before calling your function, log a message with received arguments in the context.

// With `F` being the type of your funciton:
logger.wrap(func: F): F
  // or
logger.wrap(message: string, func: F): F
  // or
logger.wrap(context: LogContext, messages: string, func: F): F

Defining target(s)

Each root logger (created using createLogger helper) is attached to 0 or more "target". A target is responsible of writing a log entry somewhere. It is an object with the following properties:

  • minLevel string: The minimum log level this target's strem writer will be called for
  • stream { write: (str: string) => void }: An object with a write function (like node's stream.Writable) which will be used to write log entries
  • format (msg: LogMessage) => string: A formatter which will be used to transform a log entry (message object) into a string

Using targets

When using the global logger, or if no targets specified when creating a logger, calling log methods will output to STDERR anything which has log level higher or equal to warn. This can be modified as follow by defineing the LOG_TARGETS environment variable or passing the targets option to createLogger. The targets can be an array of LogTarget (see above) or a string defining a list of one or more targets separated by comma (,). A string target is composed as follow:

  • The file path, absolute or relative to CWD. It can also be the specials stdout or stderr strings (case insensitive). When giving a path to a file, if it ends with the plus sign (+) the log data will be appended to the file instead of re-creating the file for each run.
  • An optional minimum log level after a colon (:). It should be a number or the log level name (ie trace, error, ...).
  • An optional formatter name after a percent sign (%). There are 2 included formatter: json (used for files by default) and simple (used for stdout and stderr by default). See below to define your own.

Examples:

  • debug.log%simple,stdout:fatal
    • Log everything to debug.log file in CWD dir (re-creates the file for each run). Uses the simple formatter.
    • Log only messages with level >= fatal to the standard out.
  • errors.log+:error,debug.log:15
    • Log only messages with level >= error to errors.log file (without re-creating the file at each run).
    • Log only messages with level >= 15 to debug.log file (re-creates the file for each run).

Custom formatters

A custom formatter is a function that takes a LogMessage object and returns a string. It can be registered giving it a name using the registerLogFormatter helper:

import { registerLogFormatter, createLogger } from 'bs-logger';
registerLogFormatter('foo', m => `${m.sequence} ${new Date(m.tim).toLocaleString()} ${m.message}`);
const logger = createLogger({
  targets: 'stdout%foo', // specifying out formatter
  });

Testing

The whole testing namespace has useful helpers for using BSLogger while unit testing your product.

In your tests you would usually prefer not having any logging to happen, or you would like to check what has been logged but without actually logging it to any target.

The testing namespace holds all testing utilities:

import { testing } from 'bs-logger'
  • If you use the root logger, here is how to disable its output:
testing.setup()

and the logger (or default) export will become a LoggerMock instance (see below).

  • If you create logger(s) using createLogger, when testing use the testing.createLoggerMock instead. It accepts the same first argument, with an extra second argument, optional, being the LogTargetMock to be used (see below).

LoggerMock

Loggers created using the testing namespace will have one and only one log target being a LogTargetMock, and that target will be set on the target extra property of the logger.

Here are the extra properties of LogTargetMock which you can then use for testing:

  • messages LogMessage[]: all log message objects which would have normally be logged
    • last LogMessage: the last one being logged
    • trace LogMessage[]: all log message objects with trace level
      • last LogMessage: last one with trace level
    • debug LogMessage[]: all log message objects with debug level
      • last LogMessage: last one with debug level
    • ...
  • lines string[]: all formatted log message lines which would have normally be logged
    • last string: the last one being logged
    • trace string[]: all formatted log message lines with trace level
      • last string: last one with trace level
    • debug string[]: all formatted log message lines with debug level
      • last string: last one with debug level
    • ...
  • clear () => void: method to clear all log message objects and formatted lines
  • filteredMessages (level: number | null, untilLevel?: number) => LogMessage[]: method to filter log message objects
  • filteredLins (level: number | null, untilLevel?: number) => string[]: method to filter formatted log message lines

Example

Let's say you have a logger.js file in which you create the logger for your app:

// file: logger.js
import { testing, createLogger, LogContexts } from 'bs-logger';

const factory = process.env.TEST ? testing.createLoggerMock : createLogger;

export default factory({ [LogContexts.application]: 'foo' });

In a test you could:

import logger from './logger';
// in `fetch(url)` you'd use the logger like `logger.debug({url}, 'GET')` when the request is actually made
import fetch from './http';

test('it should cache request', () => {
  logger.target.clear();
  fetch('http://foo.bar/dummy.json');
  expect(logger.target.messages.length).toBe(1);
  fetch('http://foo.bar/dummy.json');
  expect(logger.target.messages.length).toBe(1);
  // you can also expect on the message:
  expect(logger.target.messages.last.message).toBe('GET')
  expect(logger.target.messages.last.context.url).toBe('http://foo.bar/dummy.json')
  // or (mock target formater prefix the message with `[level:xxx] ` when there is a level)
  expect(logger.target.lines.last).toBe('[level:20] GET')
  // or filtering with level:
  expect(logger.target.lines.debug.last).toBe('[level:20] GET')
});

Installing

Add to your project with npm:

npm install --save bs-logger

or with yarn:

yarn add bs-logger

Running the tests

You need to get a copy of the repository to run the tests:

git clone https://github.com/huafu/bs-logger.git
cd bs-logger
npm run test

Built With

Contributing

Pull requests welcome!

Versioning

We use SemVer for versioning. For the versions available, see the tags on this repository.

Authors

  • Huafu Gandon - Initial work - huafu

See also the list of contributors who participated in this project.

License

This project is licensed under the MIT License - see the LICENSE file for details

Support on Beerpay

Hey dude! Help me out for a couple of :beers:!

Beerpay Beerpay