Standardize the way you write logs with loglayer
using your existing logging library
(bunyan
/ winston
/ pino
/ roarr
/ etc).
Spend less time from having to define your logs and spend more writing them.
- Switch between different logging libraries if you do not like the one you use without changing your entire codebase.
- Starting off with
console
and want to switch tobunyan
later? You can with little effort!
- Starting off with
- Intuitive API with no dependencies.
- Written in typescript.
- Installation instructions for each logging library.
- Unit tested with various logging libraries.
Without loglayer
, how does one define a log entry?
// is it like this?
winston.info('my message', { some: 'data' })
// or like this?
bunyan.info({ some: 'data' }, 'my message')
How does one work with errors?
// is it like this? Is err the field for errors?
roarr.error({ err: new Error('test') })
// Do I need to serialize it first?
roarr.error({ err: serialize(new Error('test')) })
With loglayer
, stop worrying about details, and write logs!
logLayer
.withMetadata({ some: 'data'})
.withError(new Error('test'))
.info('my message')
loglayer
is a wrapper around logging libraries to provide a consistent way to specify context, metadata, and errors.
$ npm i loglayer
import { LoggerType, LogLayer } from 'loglayer'
const log = new LogLayer({
logger: {
instance: console,
type: LoggerType.CONSOLE,
},
})
import pino, { P } from 'pino'
import { LogLayer, LoggerType } from 'loglayer'
const p = pino({
level: 'trace'
})
const log = new LogLayer<P.Logger>({
logger: {
instance: p,
type: LoggerType.PINO,
},
})
bunyan
requires an error serializer to be defined to handle errors.
import bunyan from 'bunyan'
import { LogLayer, LoggerType } from 'loglayer'
const b = bunyan.createLogger({
name: 'test-logger',
// Show all log levels
level: 'trace',
// We've defined that bunyan will transform Error types
// under the `err` field
serializers: { err: bunyan.stdSerializers.err },
})
const log = new LogLayer({
logger: {
instance: b,
type: LoggerType.BUNYAN,
},
error: {
// Make sure that loglayer is sending errors under the err field to bunyan
fieldName: 'err'
}
})
import winston from 'winston'
import { LogLayer, LoggerType } from 'loglayer'
import { serializeError } from 'serialize-error'
const w = winston.createLogger({})
const log = new LogLayer<winston.Logger>({
logger: {
instance: w as unknown as LoggerLibrary,
type: LoggerType.WINSTON,
},
error: {
serializer: serializeError,
},
})
roarr
requires an error serializer as it does not serialize errors on its own.- By default,
roarr
logging is disabled, and must be enabled via theseroarr
instructions.
import { LogLayer, LoggerType } from 'loglayer'
import { Roarr as r, Logger } from 'roarr'
import { serializeError } from 'serialize-error'
const log = new LogLayer<Logger>({
logger: {
instance: r.Roarr,
type: LoggerType.ROARR,
},
error: {
serializer: serializeError,
},
})
Using express
and pino
:
import express from 'express'
import pino from 'pino'
import { LogLayer, LoggerType } from 'loglayer'
// We only need to create the logging library instance once
const p = pino({
level: 'trace'
})
const app = express()
const port = 3000
// Define logging middleware
app.use((req, res, next) => {
req.log = new LogLayer({
logger: {
instance: p,
type: LoggerType.PINO
}
// Add a request id for each new request
}).withContext({
// generate a random id
reqId: Math.floor(Math.random() * 100000).toString(10),
// let's also add in some additional details about the server
env: 'prod'
})
next();
})
app.get('/', (req, res) => {
// Log the message
req.log.info('sending hello world response')
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
new LogLayer<LoggerInstanceType = LoggerLibrary, ErrorType = any>(config: LogLayerConfig)
Generics (all are optional):
LoggerInstanceType
: A definition that implements loginfo
/warn
/error
/trace
/debug
methods.- Used for returning the proper type in the
getLoggerInstance()
method.
- Used for returning the proper type in the
ErrorType
: A type that represents theError
type. Used with the serializer and error methods. Defaults toany
.
interface LogLayerConfig {
logger: {
/**
* The instance of the logging library to send log data and messages to
*/
instance: ExternalLogger
/**
* The instance type of the logging library being used
*/
type: LoggerType
}
error?: {
/**
* A function that takes in an incoming Error type and transforms it into an object.
* Used in the event that the logging library does not natively support serialization of errors.
*/
serializer?: ErrorSerializerType
/**
* Logging libraries may require a specific field name for errors so it knows
* how to parse them.
*
* Default is 'err'.
*/
fieldName?: string
/**
* If true, always copy error.message if available as a log message along
* with providing the error data to the logging library.
*
* Can be overridden individually by setting `copyMsg: false` in the `onlyError()`
* call.
*
* Default is false.
*/
copyMsgOnOnlyError?: boolean
}
context?: {
/**
* If specified, will set the context object to a specific field
* instead of flattening the data alongside the error and message.
*
* Default is context data will be flattened.
*/
fieldName?: string
}
metadata?: {
/**
* If specified, will set the metadata data to a specific field
* instead of flattening the data alongside the error and message.
*
* Default is metadata will be flattened.
*/
fieldName?: string
}
}
Config option: logger.type
Use the other
value for log libraries not supported here. loglayer
may or may not
work with it.
enum LoggerType {
OTHER = 'other',
WINSTON = 'winston',
ROARR = 'roarr',
PINO = 'pino',
BUNYAN = 'bunyan',
CONSOLE = 'console',
}
Config option: error.serializer
By default, loglayer
will pass error objects directly to the logging library as-is.
Some logging libraries do not have support for serializing errors, and as a result, the error may not be displayed in a log.
If you use such a library, you can define a function that transforms an error, which is in the format of:
type ErrorSerializerType = (err) => Record<string, any> | string
For example:
import { LoggerType, LogLayer } from 'loglayer'
const log = new LogLayer({
logger: {
instance: console,
type: LoggerType.CONSOLE,
},
error: {
serializer: (err) => {
// Can be an object or string
return JSON.stringify(err)
}
}
})
By default, loglayer
will flatten context and metadata into a single object
before sending it to the logging library.
For example:
log.withContext({
reqId: '1234'
})
log.withMetadata({
hasRole: true,
hasPermission: false
}).info('checking permissions')
Will result in a log entry in most logging libraries:
{
"level": 30,
"time": 1638138422796,
"hostname": "local",
"msg": "checking permissions",
"hasRole": true,
"hasPermission": false,
"reqId": 1234
}
Some developers prefer a separation of their context and metadata into dedicated fields.
You can do this via the config options, context.fieldName
and metadata.fieldName
:
const log = new LogLayer({
...,
metadata: {
// we'll put our metadata into a field called metadata
fieldName: 'metadata'
},
context: {
// we'll put our context into a field called context
fieldName: 'context'
}
})
The same log commands would now be formatted as:
{
"level": 30,
"time": 1638138422796,
"hostname": "local",
"msg": "checking permissions",
"metadata": {
"hasRole": true,
"hasPermission": false
},
"context": {
"reqId": 1234
}
}
LogLayer#info(...messages: MessageDataType[]): void
LogLayer#warn(...messages: MessageDataType[]): void
LogLayer#error(...messages: MessageDataType[]): void
LogLayer#debug(...messages: MessageDataType[]): void
LogLayer#trace(...messages: MessageDataType[]): void
type MessageDataType = string | number | null | undefined
Your logging library may or may not support passing multiple parameters. See your logging library's documentation for more details.
// Can be a single message
log.info('this is a message')
// Or passed through multiple parameters to be interepreted by your logging library.
// For example, in roarr, the subsequent parameters after the first are for sprintf interpretation only.
// Other libraries do nothing with additional parameters.
log.info('request id: %s', id)
LogLayer#withContext(data: Record<string, any>): LogLayer
- This adds or replaces context data to be included with each log entry.
- Can be chained with other methods.
log.withContext({
requestId: 1234
})
// Your logging library will now include the context data
// as part of its logging output
log.info('this is a request')
Output from pino
:
{
"level": 30,
"time": 1638146872750,
"pid": 38300,
"hostname": "local",
"requestId": 1234,
"msg": "this is a request"
}
LogLayer#withMetadata(data: Record<string, any>): ILogBuilder
Use this if you want to log data that is specific to the message only.
- This method must be chained with a log message method.
- This method can be chained with
withError()
to include an error with the metadata.
log.withMetadata({ some: 'data' }).info('this is a message that includes metadata')
LogLayer#metadataOnly(data: Record<string, any>, logLevel: LogLevel = 'info'): void
Use this if you want to only log metadata without including a message.
// Default log level is 'info'
log.metadataOnly({ some: 'data' })
// Adjust log level
log.metadataOnly({ some: 'data' }, LogLevel.warn)
- If the
error.serializer
config is not used, then it will be the job of the logging library to handle serialization.- If you are not seeing errors logged:
- Make sure the logging library's log level is configured to print an
error
log level. - The logging library may not serialize errors out of the box and must be configured, or a serializer must
be defined with
loglayer
so that it can serialize it before sending it to the logging library.
- Make sure the logging library's log level is configured to print an
- If you are not seeing errors logged:
- The
error.fieldName
config is used to determine the field name to attach the error to when sending to the logging library.- The default field name used is
err
.
- The default field name used is
LogLayer#withError(error: Error): ILogBuilder
Use this to include an error object with your message.
- This method must be chained with a log message method.
- This method can be chained with
withMetadata()
to include metadata alongside the error.
// You can use any log level you want
log.withError(new Error('error')).error('this is a message that includes an error')
LogLayer#errorOnly(error: Error, opts?: OnlyErrorOpts): void
Options:
interface OnlyErrorOpts {
/**
* Sets the log level of the error
*/
logLevel?: LogLevel
/**
* If `true`, copies the `error.message` if available to the logger library's
* message property.
*
* If the config option `error.copyMsgOnOnlyError` is enabled, this property
* can be set to `false` to disable the behavior for this specific log entry.
*/
copyMsg?: boolean
}
Use this if you want to only log metadata without including a message.
// Default log level is 'error'
log.errorOnly(new Error('test'))
// Adjust log level
log.errorOnly(new Error('test'), { level: LogLevel.warn })
// Include the error message as part of the logging library's message field
// This may be redundant as the error message value will still be included
// as part of the message itself
log.errorOnly(new Error('test'), { copyMsg: true })
// If the loglayer instance has `error.copyMsgOnOnlyError = true` and you
// want to disable copying the message for a single line, explicitly
// define copyMessage with false
log.errorOnly(new Error('test'), { copyMsg: false })
LogLayer#getLoggerInstance()
Returns back the backing logger used in the event you need to call methods specific to that logging library.
Rather than having to define your own mocks for loglayer
, we have a mock class you can use for your tests:
import { MockLogLayer } from 'loglayer'
// You can use the MockLogLayer in place of LogLayer
// so nothing will log
$ npm run test:ci