@nolawnchairs/logger
v3.2.0
Published
Multi-stream formatted logger for NodeJS applications
Downloads
5
Readme
Logger
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 ignoredJSON
- the value is serialized into JSONINSPECT
- the value is expanded to a readable format. This is the method used my the defaultconsole.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 LogWriter
s 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 |