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

@nolawnchairs/logger

v3.2.0

Published

Multi-stream formatted logger for NodeJS applications

Downloads

5

Readme

Logger

npm version

Version 3.x is incompatible with 2.x and 1.x. Note that this project is more or less an "in-house" tool, so no guarantees.

For v2 docs, go here

Install

npm i @nolawnchairs/logger
yarn add @nolawnchairs/logger

Setting Logging Up

As opposed to v2, there is more boilerplate to configure, since we now provide multiple logging environments that can each write to multiple output streams.

Set up your logging environments using the init method. The providers.globalLoggers object must contain at least one object that defines the function providing the configuration for this logger:

Log.init({
  providers: {
    globalLoggers: {
      development: () => ({
        enabled: process.env.NODE_ENV === 'development',
        level: LogLevel.DEBUG,
        serializationStrategy: ObjectSerializationStrategy.INSPECT,
        writers: [
          LogWriter.stdout(),
          LogWriter.file(`${LOG_DIR}/my-application.log`, { 
            formatter: Formatters.jsonFormatter 
          })
        ]
      })
    }
  }
})

In the above example, we define a single globalLogger. The name we give this logger is development, and it returns a LoggerInstanceConfig object. Here, the enabled key is set based on whether or not we're in development mode, we set the level to DEBUG, set a serialization strategy (which dictates how non-scalar values are treated when printing), and an array of LogWriter instances that define where the logging output goes.

Global vs Feature Loggers

Global loggers are available throughout your project and are logged to using the standard Log interface as such:

Log.info('This is a test message')

Feature Loggers are only available within the class or module in which they are defined, and accept a string value that will identify where the message came from. This is useful in larger projects, since the message will give you context as to where it was printed.

Crate a Feature Logger:

const logger = Log.forFeature('FeatureClass')
logger.info('Test info message from the FeatureClass')

Feature Loggers require configuration just like Global Loggers do. You can pass configuration to each one you create:

const logger = Log.forFeature('FeatureClass', {
  enabled: true,
  level: LogLevel.ERROR,
  writers: [
    LogWriter.stdout(),
    LogWriter.file(`${LOG_DIR}/logs/feature-class.log`, { 
      formatter: Formatters.monochromeFormatter 
    }),
  ]
})

logger.info('Test info message from the FeatureClass')

Or you can create a default configuration for all Feature Loggers that don't provide configuration themselves, which is easier, and will format all Feature Loggers the same:

const IS_DEV = process.env.NODE_ENV === 'development'

Log.init({
  providers: {
    globalLoggers: {...},
    featureLogger: context => {
      const logName = snakeCase(context)
      return {
        enabled: true,
        level: IS_DEV ? LogLevel.DEBUG : LogLevel.ERROR,
        serializationStrategy: ObjectSerializationStrategy.INSPECT,
        writers: [
          LogWriter.stdout(),
          // Create a new log file for each feature
          LogWriter.file(`${LOG_DIR}/logs/feature.${logName}.log`, { 
            formatter: Formatters.jsonFormatter 
          })
          // Or use the same file for all logging.
          LogWriter.file(`${LOG_DIR}/my-application.log`, { 
            formatter: Formatters.jsonFormatter 
          })
        ]
      }
    } 
  }
})

You can of course add custom configuration to any Feature Logger you create, which will override the default configuration.


enum LogLevel

The LogLevel enum contains the following values:

  • DEBUG
  • INFO
  • WARN
  • ERROR
  • FATAL

Each logger instance contains identical methods for each: debug, info, warn, error and fatal

Each log level method takes one or more arguments. The first argument will always be printed, any additional arguments will only be printed if the first argument is a string with sprintf-type tokens that will be printed in order.

If the first argument is not a string, only the first argument will be printed, and the serialization strategy will apply. This example uses the default INSPECT strategy.

Log.info('Test') // Test
Log.info('Test #%d', 1) // Test #1
Log.info('Test', '123') // Test
Log.info([1, 2, 3]) // [1, 2, 3]
Log.info({ value: 42 }) // { value: 42 }
Log.info('The thing with id "%s" was not found', 42) // The thing with id "42" was not found
Log.info('%d + %d = %d', 10, 10, 10 + 10) // 10 + 10 = 20

More detailed documentation on sprintf tokens can be found here

Assertions

Along with the standard log functions, there is also an assert method which you can call to print an "Assertion Failed" message if a given condition resolves to a false-like value.

const [a, b] = [1, 0]
Log.assert(a === b, '%d should equal %d', a, b)
// Will print:
// Assertion Failed: 1 should equal 0

Assertions are bound to a certain logging level, LogLevel.DEBUG by default, but this can be changed with the assertionLevel property in the global configuration. You can completely disable assertions for all levels by setting assertionsEnabled to false in the global configuration.


enum ObjectSerializationStrategy

This setting will govern how non-scalar (objects, classes and arrays) are printed. The ObjectSerializationStrategy enum contains the following values:

  • OMIT - the value is ignored
  • JSON - the value is serialized into JSON
  • INSPECT - the value is expanded to a readable format. This is the method used my the default console.log implementation

Configuration

Each logger we create requires its configuration to be set. We define these in the init method of the main Log interface, or individually for Feature Loggers should they require custom configuration than the default provided in init

Configuration is set in the Log.init() method, and has the following structure:

{
  global: LoggerGlobalConfig
  providers: {
    globalLoggers: {
      yourLoggerName: () => LoggerInstanceConfig
      anotherLogger: () => LoggerInstanceConfig
    }
    featureLogger: () => LoggerInstanceConfig
  }
}

interface LoggerGlobalConfig

The following values can be set to the global object, and will provide default values to all your other loggers

| Property | Type | Description | Required | | ----------- | ----------- | -------- | :--------: | | eol | string | The end-of-line character. Defaults to \n || | serializationStrategy | ObjectSerializationStrategy | How non-scalar values will be printed. Defaults to INSPECT || | inspectionDepth | number | The depth of serialization when using the INSPECT strategy. Defaults to 3 See... || | inspectionColor | boolean | Used in the INSPECT strategy, governs whether or not to color the inspected object || | formatter | FormatProvider | Defines a custom formatter for each LogWriter attached this logger. Note that writers may override this with their own formatter || | assertionsEnabled | boolean | Whether or not failed assertion messages will be printed, Defaults to true, but will depend on the logging level to which they are bound | | | assertionLevel | LogLevel | The log level to which failed assertion messages will be printed. Defaults to LogLevel.DEBUG. Note that if choosing a log level that's is not enabled, assertions will not print ||

interface LoggerInstanceConfig

Each individual logger you define must be configured with the following properties.

| Property | Type | Description | Required | | ----------- | ----------- | -------- | :--------: | | enabled? | boolean | Whether this logger will produce data. Defaults to true | ✅ | | level? | LogLevel \| number | The level to which this logger will adhere. Use a LogLevel enum value or an or'ed bitmask of multiple levels to use in unison1. The value set here will apply the same constraints to child writers. If specifying custom levels for writers, leave this option empty, as it defaults to LEVELS_ALL | ✅ | | writers | LogWriter[] | An array of LogWriter instances this logger will use | ✅ |

In addition to the above properties, the LoggerInstanceConfig will accept any of the properties defined in LoggerGlobalConfig, which will override any default values you set in global.

Notes

1 Specifying a single level will print anything from the defined level, UP. Meaning that a WARN level will print WARN, ERROR and FATAL, but not INFO or DEBUG. You can, however, define log level in a non-contiguous manner by masking two levels together:

{
  level: LogLevel.INFO | LogLevel.ERROR
}

The above configuration will ONLY print INFO or ERROR messages, and nothing else


Formatters

Formatters define how the logging messages are structured in output. There are three formatters included by default:

Formatters.defaultFormatter

defaultFormatter produces colored formatting, ideal for console streams:

// Global
2021-04-25T18:48:35.409Z 45532  INFO | Testing 123

// Feature
2021-04-25T18:48:35.409Z 45532  INFO | FeatureName | Testing 123

Formatters.monochromeFormatter

monochromeFormatter produces the same output as the defaultFormatter, but without the coloring

Formatters.jsonFormatter

jsonFormatter produces a JSON object for each line printed. Useful for file logging where extra processing or analysis may be required

# Global
{"date":"2021-04-26T14:21:55.392Z","pid":"1031654","level":"INFO","message":"Testing 123"}

# Feature
{"date":"2021-04-26T14:21:55.392Z","pid":"1031654","level":"INFO","context":"FeatureName","message":"Testing 123"}

Formatters.colorize(color: AnsiColors, text: string)

returns string

This is a convenience function to colorize text. It takes an AnsiColors enum value and the text to apply the color to. The standard AnsiColors.RESET is applied at the end of the string

Custom Formatters

You can create a custom formatter by defining a function that accepts a LogEntry object and returns a string. You can include the formatter inside the LoggerInstanceConfig.

type FormatterProvider

function (e: LogEntry) => string

{
  enabled: true,
  level: LogLevel.INFO,
  formatter: (e: LogEntry) => {
    return `${e.date.toIsoString()} - ${e.levelText} - ${e.message}`
  },
  writers: [...]
}

interface LogEntry

The LogEntry object is passed to every formatter function and includes the following data required to print logs:

| Property | Type | Description | |---|---|---| |date | Date | The date object referencing the time the log event occurred | |pid| string | The process ID of the running process, in string format | |level|LogLevel | The LogLevel enum value of the log event | |levelText | string | The text representation of the level, in CAPS. Note that INFO and WARN are left-padded with one empty space to accommodate symmetry | |levelColor | AnsiColors | The AnsiColors enum value of the default color for the log event's level | |message | string | The formatted log message | |context | string | Only provided for Feature Loggers if set, the string value passed as the context argument |

Log Writers

Each logger you create can include one or more LogWriters that produce the logs to an output stream.

LogWriter.stdout(options?: WriterOptions)

Prints to stdout

LogWriter.stderr(options?: WriterOptions)

Prints to stderr

LogWriter.file(file: string, options?: FileWriterOptions)

Prints to a file. The file argument is required and must be the path to the writable log file. If this file does not exist, it will be automatically created for you.

File writers will attempt to re-use existing opened write streams, which are indexed by the resolved file path parameter.

When defining Feature Loggers, either inline or in the global configuration, you can set the log file name within the definition. It is recommended that you use consistent absolute paths to avoid multiple write streams to the same files. All paths passed to the LogWriter.file function are normalized using the path.resolve function provided by NodeJS to ensure only a single stream to a given file exists.

  featureLogger: name => ({
    enabled: !IS_DEV,
    level: LogLevel.ERROR,
    formatter: Formatters.monochromeFormatter,
    writers: [
      LogWriter.file(`${ROOT}/logs/feature.${name}.log`),
    ]
  })

You can narrow down the log levels each writer will print by specifying its own log level. This is useful if you only want error or fatal messages to be logged to disk, but still want warning levels printing to stdout.

  featureLogger: name => ({
    enabled: !IS_DEV,
    formatter: Formatters.monochromeFormatter,
    writers: [
      LogWriter.file(`${ROOT}/logs/feature.${name}-error.log`, {
        level: LogLevel.ERROR
      }),
    ]
  })

Note that when specifying levels to writers, they will be constrained by the level set in the parent logger. For example, if you set the logger's level to LogLevel.ERROR only, and supply LogLevel.INFO | LogLevel.ERROR to a writer, only the error level messages will be printed because the parent logger does not allow info level messages to print. When Supplying a level constraint to writers, it's best to leave either the logger's level option empty to relegate the message filtering to the writers.


Writer Options

Writers can accept an options object which is optional, and all properties provided are also optional

object WriterOptions

| Property | Type | Description | |---|---|---| | formatter | FormatProvider | The function that builds the message from the LogEntry object provided | | level | LogLevel \| number | The log level (or bitmask) to apply to this writer. Note that the level provided here is contingent on it being enabled in the parent logger.

object FileWriterOptions

The file writer takes an options object with two additional optional properties:

| Property | Type | Description | |---|---|---| | mode | number | The permissions for the file being written to in octal notation. Defaults to 0o644 | | encoding | string (BufferEncoding) | The encoding |