A middleware logger that implements the MDC logging pattern for use in AWS NodeJS Lambdas. It provides 4 log levels (DEBUG, INFO, WARN, ERROR), custom message attributes, and allows the creation of sub-loggers.
It is designed to make log messages easily readable by humans from Cloudwatch, as well as easily parsed by machines for ingestion into downstream systems like the ELK stack. When a log call is made the log level and message are placed first, followed by a JSON-stringified object that contains the message and custom attributes.
Since v3 lambda-logger-node only supports wrapping async function handlers.
// lambda.js
const { Logger } = require('lambda-logger-node')
let logger = Logger()
logger.setKey('custom', 'value')
exports.handler = logger.handler(handler)
async function handler (event, context) {
logger.info('test message') // ->
/*
INFO test message | ___$LAMBDA-LOG-TAG$___{
"traceId":"8sd3g32-42fg-43th45h-vsafd",
"date":"2018-12-17T23:37:24Z",
"appName":"your-app-name",
"apigTraceId":"897635-3534-33435-435",
"traceIndex":0,
"custom":"value",
"message":"test message",
"severity":"INFO",
"contextPath":""}___$LAMBDA-LOG-TAG$___ <-- one line when loged, whitespace for docs
*/
}npm install lambda-logger-node
Simple Lambda handler
const { Logger } = require('lambda-logger-node')
const logger = Logger()
exports.handler = logger.handler(handler)
async function handler (event, context) {
// your lambda handler
}Or, with more middleware
const { Logger } = require('lambda-logger-node')
const logger = Logger()
var moreMiddleware = require('more-middleware')
exports.handler = logger.handler(moreMiddleware(handler))
async function handler (event, context) {
// your lambda handler
}To simplify usage of the logger throughout your application configure a logger in its own module.
// logger.js
const config = require('./config')
const { Logger } = require('lambda-logger-node')
const logger = Logger({
minimumLogLevel = config.isProduction ? 'INFO' : null
})
module.exports = logger
// lambda.js
const logger = require('./logger')
exports.handler = logger(handler)
async function handler (event, context) {
// your lambda handler
}
// api.js
const logger = require('./logger')
module.exports { someFunc }
function someFunc() {
// app code
logger.info('custom log')
}In addition to this method allowing global use of your lambda, the logger is attached to the handler's context argument as context.logger. When the lambda handler is executed all of the request-specific values (like the traceId) are updated, even when the module is declared and required outside the handler like the one above.
The Logger module exports a constructor on Logger that takes the following options
function Logger ({
minimumLogLevel = null,
useGlobalErrorHandler = true,
redactors = [],
useBearerRedactor = true,
formatter = JsonFormatter
} = {})string: minimumLogLevel: one of DEBUG | INFO | WARN | ERROR. Supress messages that are below this level in severity.bool:useGlobalErrorHandler: default:true. Attach process-level handlers for uncaught exceptions and unhandled rejections to log messages with the logger. Attempting to construct two loggers with this setting will result in an error,[string|RegExp|func]:redactors: an array of redactors to process all log messages with. Astringwill be removed verbatim, aRegExpwill be removed if it matches. If a function is given it is passed the log message as a string, and MUST return a string (whether it replaced anything or not).bool: useBearerRedactor: default:true, add a bearer token redactor to the list of redactors.bool: testMode: Override environment checks and force "testMode" to betrueorfalse. Leaveundefinedto allow ENV to define test mode.func: formatter: format messages before they are written out. The default formatter is used if this option is left off. This is an advanced customization point, and a deep understanding of the logger will be necessary to implement a custom formatter (there are no docs other than source code right now).
The Logger constructor returns a logger instance with the following API
{
handler,
setMinimumLogLevel,
events,
setKey,
createSubLogger,
info,
warn,
error,
debug
}
handler: Takes a handler function as input and returns a wrapped handler function that configures per-request keys such astraceId.setMinimumLogLevel: Takes a log level (DEBUG | INFO | WARN | ERROR) and sets the sameminimumLogLevelthat the constructor took. Useful if this is not known until the handler has executed.events: anEventEmitter. Currently only supportsbeforeHandler.setKey: add a custom sttribute to all log messages. First argument is a string name for the attributes. The second argument is a value, or a value-returning function (function will be executed at log-time).debug|info|warn|error: Create a log for the matching severity.createSubLogger(string: subLoggerName): Creates a sub-logger that only has the log methods (debug|info|warn|error) andcreateSubLogger. Useful for providing loggers to sub-components like your Dynamo Client. Its messages are prefixed with the sub-loggers name; if there are multiple levels of sub-loggers each sub-logger is included in the prefix. e.g. for a sub-logger "SubTwo" that is a sub-logger of another sub-logger "SubOne" the message would beINFO SubOne.SubTwo message.
The logger also contains an event emitter that will emit a beforeHandler event just before the lambda handler function is called. This receives both the lambda event and context as arguments. For example, you could use this to generate a custom traceId key/value pair in your logs:
const nanoid = require('nanoid')
logger.events.on('beforeHandler', (lambdaEvent, context) => {
logger.setKey('traceId', nanoid())
})This will use a custom value generated by the nanoid library, rather than the default awsRequestId.