npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@simplyhexagonal/logger

v2.1.1

Published

Extensible debug logger with singleton capabilities, made to easily broadcast to multiple communication channels/transports

Downloads

2,030

Readme

Simply Hexagonal Logger

Tests Try logger on RunKit

Extensible asynchronous debug logger with singleton capabilities, developed to easily broadcast to multiple communication channels/transports.

import Logger from '@simplyhexagonal/logger';

const logger = new Logger({});

logger.debug('Trying to teach', 2, 'tooters', {to: 'toot'});

Open source notice

This project is open to updates by its users, I ensure that PRs are relevant to the community. In other words, if you find a bug or want a new feature, please help us by becoming one of the contributors ✌️ ! See the contributing section

Like this module? ❤

Please consider:

Features

  • Define different communication channels per log level (i.e. send debug messages to console and error messages to Slack)
  • Define multiple communication channels per log level
  • Specify channel names and then simply use the .channel() function to send messages to any specific channel
  • Extend your logging capabilities with officially supported transports for: Slack, Discord, Email, SMS, Socket
  • Easily make your own transports by implementing and extending the base LoggerTransport class type (and submit them via GitHub issue for adoption as an officially supported transport!)
  • Use the same logger instance throughout your app (singleton logger)
  • Use multiple logger instances throughout your app with de-duplicated communication channels/transports (singleton transports)
  • ANSI colors and easy to read formatting for CLI terminals
  • CSS colors and easy to read formatting for browser dev consoles

Usage

Install:

npm install @simplyhexagonal/logger

yarn add @simplyhexagonal/logger

pnpm i @simplyhexagonal/logger

There are three basic configuration elements you should established based on your app's needs and the environment you will deploy to:

  • log level
  • communication channels
  • error management strategy

Log level

There are 6 log levels:

  • debug
  • info
  • warn
  • error
  • fatal
  • all

And a bypass level that outputs the message with no date, level, nor colors:

  • raw

In your code you log messages to specific log levels:

logger.debug('hello');

try {
  throw new Error('you shall not pass');
} catch {
  logger.error('time to turn around');
}

// 2021-10-02T23:47:27.187Z DEBUG 🐞️:
//
//    hello
//

// 2021-10-02T23:47:27.191Z ERROR 🚨️:
//
//    time to turn around
//

Each log level is given a number value:

{
  debug: 0,
  info: 10,
  warn: 20,
  error: 30,
  fatal: 40,
  all: 100,
  raw: 110,
}

When Logger is instantiated, it will only setup communication channels for the configured log level and above (i.e. if you selected warn then the only logLevel >= 20 would be initialized)

You can set the log level when instancing the logger:

import Logger, { LogLevels } from '@simplyhexagonal/logger';

new Logger({
  logLevel: LogLevels.DEBUG,
  //...
});

It is highly recommended to set the log level based on a condition that determines the environment your app is running on:

const logLevel = (process.env.NODE_ENV === 'production') ? LogLevels.ERROR : LogLevels.DEBUG;

new Logger({
  logLevel,
  //...
});

Setting the log level using environment variables is only recommended as a way to override the log level configured during instantiation:

# .env
LOG_LEVEL=debug

(i.e. this is useful if you deem it necessary to turn on debug logging in production environments)

Communication channels

Let's say that you have a Discord server with a channel you want to receive only debug messages from your app, and another channel dedicated to receiving only errors.

The debug channel has the webhook path: /D3BU9/W3BH00K

The error channel has the webhook path: /3RR0R/W3BH00K

Using @simplyhexagonal/logger, you can add the official Discord transport as a dependency and import it:

import DiscordTransport from '@simplyhexagonal/logger-transport-discord';

Then, you can configure the transports for each of Logger's log levels:

const optionsByLevel = {
  debug: [
    {
      transport: LoggerTransportName.DISCORD,
      options: {
        // debug channel webhook url
        destination: 'https://discord.com/api/webhooks/D3BU9/W3BH00K',
      },
    },
  ],
  info: [],
  warn: [],
  error: [
    {
      transport: LoggerTransportName.DISCORD,
      options: {
        // error channel webhook url
        destination: 'https://discord.com/api/webhooks/3RR0R/W3BH00K',
      },
    },
  ],
  fatal: [],
  all: [],
};

Then you would let Logger know which transport to use for LoggerTransportName.DISCORD:

const transports = {
  [LoggerTransportName.DISCORD]: DiscordTransport,
};

The final result would look something like this:

import {
  LogLevels,
  LoggerTransportName,
} from '@simplyhexagonal/logger';
import DiscordTransport from '@simplyhexagonal/logger-transport-discord';

const optionsByLevel = {
  debug: [
    {
      transport: LoggerTransportName.DISCORD,
      options: {
        // debug channel webhook url
        destination: 'https://discord.com/api/webhooks/D3BU9/W3BH00K',
      },
    },
  ],
  info: [],
  warn: [],
  error: [
    {
      transport: LoggerTransportName.DISCORD,
      options: {
        // error channel webhook url
        destination: 'https://discord.com/api/webhooks/3RR0R/W3BH00K',
      },
    },
  ],
  fatal: [],
  all: [],
  raw: [],
};

const transports = {
  [LoggerTransportName.DISCORD]: DiscordTransport,
};

const options = {
  logLevel: LogLevels.DEBUG,
  optionsByLevel,
  transports,
};

const logger = new Logger(options);

Error management strategy

In the previous example there's always a possibility for the Discord webhook to return an error.

When this happens Logger will default to throwing an error which can be handled using .catch():

logger.debug('hello discord').catch((e) => {
  logger.channel(LoggerTransportName.CONSOLE).error(e);
});

We understand that this adds unnecessary complexity, as such, you are encouraged to turn on error catching when instantiating Logger. When you do this, Logger will automagically catch transport errors and log them to console (with error log level):

const options = {
  optionsByLevel: optionsWithBadTransport,
  catchTransportErrors: true,
};

const logger = new Logger(options);

logger.debug('this will fail due to a bad transport');

//  2021-10-03T04:31:02.191Z ERROR 🚨️:
//  
//      {
//        "transportResult": {
//          "destination": "...",
//          "channelName": "...",
//          "error": {
//            "name": "Error",
//            "message": "LOGGER ERROR: ...",
//            "stack": "Error: LOGGER ERROR: ...",
//            // ...
//          },
//        },
//        //...
//      }
//

Furthermore, you could implement your own fallback transport:

import { LoggerTransport } from '@simplyhexagonal/logger/transports/base';

class MyTransport extends LoggerTransport {
  constructor(options: LoggerTransportOptions['options']) {
    const r = Math.random().toString(36).substring(7);
    super({...options, r});
  }

  async error([timestamp, ...message]: unknown[]) {
    console.log(timestamp, 'MY LOG:', ...message);

    return {
      destination: this.destination,
      channelName: this.channelName,
    };
  }
}

const options = {
  optionsByLevel: optionsWithBadTransport,
  catchTransportErrors: true,
  fallbackTransport: MyTransport,
};

const logger = new Logger(options);

logger.debug('this will fail due to a bad transport');

// 2021-10-03T04:31:02.201Z MY LOG: UndefinedTransportError: ...

And just as with LOG_LEVEL, we have implemented an environment variable for overriding purposes:

# .env
LOGGER_CATCH_TRANSPORT_ERRORS=true

IMPORTANT NOTE: we recommend always setting catchTransportErrors to true in production!

More options

import {
  LogLevels,
  LoggerTransportName,
} from '@simplyhexagonal/logger';
import DiscordTransport from '@simplyhexagonal/logger-transport-discord';

const options = {
  logLevel: LogLevels.DEBUG, // default
  optionsByLevel: {
    debug: [
      // ***
      // This console config is the default if a log level options array is left empty
      // (like `info` in this example)
      {
        transport: LoggerTransportName.CONSOLE,
        options: {
          destination: LoggerTransportName.CONSOLE,
          channelName: LoggerTransportName.CONSOLE,
        },
      },
      // if you do this you would have only one instance of this transport since all
      // transports are singleton (as in pre-filtered and de-duplicated)
      // ***
      {
        transport: LoggerTransportName.DISCORD,
        options: {
          destination: 'https://discord.com/api/webhooks/D3BU9/W3BH00K',
          channelName: 'discord-debug',
        },
      },
    ],
    info: [], // in this case `loggger.info()` will default to logging to the console
    warn: [],
    error: [
      {
        transport: LoggerTransportName.DISCORD,
        options: {
          destination: 'https://discord.com/api/webhooks/3RR0R/W3BH00K',
        },
      },
    ],
    fatal: [],
    all: [],
    raw: [],
  },
  transports: {
    [`${LoggerTransportName.DISCORD}`]: DiscordTransport,
  },
  singleton: true, // default
  catchTransportErrors: false, // default
  fallbackTransport: MyTransport,
};

const logger = new Logger(options);

The all and raw log levels

An important thing to note is that transports defined for the all and raw log levels will always be instantiated.

In the same way, calls to logger.all() or logger.raw() will always log.

For this reason we suggest only ever using logger.all() when an app starts and when an app is manually stopped, and only use logger.raw() when outputting messages you absolutely need to be unformatted.

Channels

It can be extremely useful to setup multiple channels for specific purposes on a log level:

const logger = new Logger({
  logLevel: LogLevels.DEBUG,
  optionsByLevel: {
    warn: [],
    info: [],
    debug: [],
    error: [
      {
        transport: LoggerTransportName.SLACK,
        options: {
          destination: 'https://hooks.slack.com/services/T123/B456/M0N90',
          channelName: 'mongo',
        },
      },
      {
        transport: LoggerTransportName.SLACK,
        options: {
          destination: 'https://hooks.slack.com/services/T123/B456/M55QL',
          channelName: 'mssql',
        },
      },
    ],
    fatal: [],
    all: [],
  },
});

and then send messages to a specific channel depending on the event that's triggering the log:

const server = async () => {
  await mongoose.connect('mongodb://mymongo.cluster:27017/myapp').catch((e) => {
    logger.channel('mongo').error(e);
  });

  await sql.connect('Server=mymssql.cluster,1433;Database=myapp;').catch((e) => {
    logger.channel('mssql').error(e);
  });
}

server();

Transports

We have the following officially supported transports:

Time functions to measure performance

We have implemented functions similar to console.time() and console.timeEnd() to measure performance:

const logger = new Logger({});

logger.time('my-timed-operation');

// do things

const result: number = await logger.timeEnd('my-timed-operation');

// transport output:
//
//   my-timed-operation: 123.456 ms
//

console.log(result);

// 123.456

Three important differences from the console functions to note:

  • logger.timeEnd is async as it logs to the transports through the .raw() method
  • logger.timeEnd will return a number representing the elapsed time in milliseconds, which is useful for being able to store the value for usage in measuring performance through code
  • we used performance.timeOrigin + performance.now() instead of Date.now() to get the current time in high resolution milliseconds, due to this fact the time measurements are more accurate than some internal time-based functions (e.g. from experience we've seen code like setTimeout(() => {}, 100) might be measured to take 99.83 ms which can break a test if you expect something like await logger.timeEnd('...') > 100)

Contributing

Yes, thank you! This plugin is community-driven, most of its features are from different authors. Please update the docs and tests and add your name to the package.json file.

Contributors ✨

Thanks goes to these wonderful people (emoji key):

License

Copyright (c) 2021-Present Logger Contributors. Licensed under the Apache License 2.0.