3.2.0 • Published 2 years ago

@nolawnchairs/logger v3.2.0

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

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

PropertyTypeDescriptionRequired
eolstringThe end-of-line character. Defaults to \n
serializationStrategyObjectSerializationStrategyHow non-scalar values will be printed. Defaults to INSPECT
inspectionDepthnumberThe depth of serialization when using the INSPECT strategy. Defaults to 3 See...
inspectionColorbooleanUsed in the INSPECT strategy, governs whether or not to color the inspected object
formatterFormatProviderDefines a custom formatter for each LogWriter attached this logger. Note that writers may override this with their own formatter
assertionsEnabledbooleanWhether or not failed assertion messages will be printed, Defaults to true, but will depend on the logging level to which they are bound
assertionLevelLogLevelThe 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.

PropertyTypeDescriptionRequired
enabled?booleanWhether this logger will produce data. Defaults to true
level?LogLevel \| numberThe 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
writersLogWriter[]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:

PropertyTypeDescription
dateDateThe date object referencing the time the log event occurred
pidstringThe process ID of the running process, in string format
levelLogLevelThe LogLevel enum value of the log event
levelTextstringThe text representation of the level, in CAPS. Note that INFO and WARN are left-padded with one empty space to accommodate symmetry
levelColorAnsiColorsThe AnsiColors enum value of the default color for the log event's level
messagestringThe formatted log message
contextstringOnly 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

PropertyTypeDescription
formatterFormatProviderThe function that builds the message from the LogEntry object provided
levelLogLevel \| numberThe 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:

PropertyTypeDescription
modenumberThe permissions for the file being written to in octal notation. Defaults to 0o644
encodingstring (BufferEncoding)The encoding
3.1.3

2 years ago

3.1.2

2 years ago

3.2.0

2 years ago

3.1.1

2 years ago

3.1.0

2 years ago

3.0.0-rc1

3 years ago

3.0.0

3 years ago

2.1.1

4 years ago

2.1.0

4 years ago

2.0.5

4 years ago

2.0.4

4 years ago

2.0.3

4 years ago

2.0.2

4 years ago

2.0.1

4 years ago

2.0.0

4 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.6

5 years ago

1.0.5

5 years ago

1.0.4

5 years ago

1.0.3

5 years ago

1.0.2

5 years ago

1.0.1

6 years ago

1.0.0

6 years ago