UNPKG

koa-bunyan-logger

Version:

Koa middleware for logging requests using bunyan

259 lines (207 loc) 6.93 kB
const bunyan = require('bunyan'); const uuid = require('uuid'); const util = require('util'); const onFinished = require('on-finished'); const updateFields = (ctx, func, data, err) => { if (!func) return data; try { if (err) { return func.call(ctx, data, err) || data; } return func.call(ctx, data) || data; } catch (e) { ctx.log.error(e); return data; } }; /* * If logger is a bunyan logger instance, return it; * otherwise, create a new logger with some reasonable defaults. */ const createOrUseLogger = (logger) => { if (!logger || !logger.info || !logger.child) { const loggerOpts = logger || {}; loggerOpts.name = loggerOpts.name || 'koa'; loggerOpts.serializers = loggerOpts.serializers || bunyan.stdSerializers; logger = bunyan.createLogger(loggerOpts); } return logger; }; /* * Koa middleware that adds this.log property to the koa context * containing a bunyan logger instance. * * Parameters: * - loggerInstance: bunyan logger instance, or an object with properties * that will be passed to bunyan.createLogger. If not * specified, a default logger will be used. */ module.exports = (loggerInstance) => { loggerInstance = createOrUseLogger(loggerInstance); return (ctx, next) => { ctx.log = loggerInstance; return next(); }; }; /* * Koa middleware that gets a unique request id from a header or * generates a new one, and adds the requestId to all messages logged * using this.log in downstream middleware and handlers. * * Must use(koaBunyanLogger()) before using this middleware. * * Parameters: * - opts: object with optional properties: * - header: name of header to get request id from (default X-Request-Id) * - prop: property to store on 'this' context (default 'reqId') * - requestProp: property to store on 'this.request' (default 'reqId') * - field: log field name for bunyan (default 'req_id') */ module.exports.requestIdContext = (opts) => { opts = opts || {}; const header = opts.header || 'X-Request-Id'; const ctxProp = opts.prop || 'reqId'; const requestProp = opts.requestProp || 'reqId'; const logField = opts.field || 'req_id'; return (ctx, next) => { const reqId = ctx.request.get(header) || uuid.v4(); ctx[ctxProp] = reqId; ctx.request[requestProp] = reqId; const logFields = {}; logFields[logField] = reqId; if (!ctx.log) { throw new Error('must use(koaBunyanLogger()) before this middleware'); } ctx.log = ctx.log.child(logFields); return next(); }; }; /* * Logs requests and responses. * * Must use(koaBunyanLogger()) before using this middleware. * * Parameters: * - opts: object with optional properties * - durationField: name of duration field * - levelFn: function (status, err) * - updateLogFields: function (data) * - updateRequestLogFields: function (requestData) * - updateResponseLogFields: function (responseData) * - formatRequestMessage: function (requestData) * - formatResponseMessage: function (responseData) */ module.exports.requestLogger = (opts) => { opts = opts || {}; const levelFn = opts.levelFn || function (status) { if (status >= 500) { return 'error'; } if (status >= 400) { return 'warn'; } return 'info'; }; const durationField = opts.durationField || 'duration'; const formatRequestMessage = opts.formatRequestMessage || function () { return util.format(' <-- %s %s', this.request.method, this.request.originalUrl); }; const formatResponseMessage = opts.formatResponseMessage || function (data) { return util.format(' --> %s %s %d %sms', this.request.method, this.request.originalUrl, this.status, data[durationField]); }; return (ctx, next) => { if (Array.isArray(opts.ignorePath) && opts.ignorePath.includes(ctx.path)) { return next(); } let requestData = { req: ctx.req }; requestData = updateFields(ctx, opts.updateLogFields, requestData); requestData = updateFields(ctx, opts.updateRequestLogFields, requestData); ctx.log.info(requestData, formatRequestMessage.call(ctx, requestData)); const startTime = new Date().getTime(); let err; const onResponseFinished = () => { let responseData = { req: ctx.req, res: ctx.res }; if (err) { responseData.err = err; } responseData[durationField] = Date.now() - startTime; responseData = updateFields(ctx, opts.updateLogFields, responseData); responseData = updateFields(ctx, opts.updateResponseLogFields, responseData, err); const level = levelFn.call(ctx, ctx.status, err); ctx.log[level](responseData, formatResponseMessage.call(ctx, responseData)); // Remove log object to mitigate accidental leaks ctx.log = null; }; return next().catch((e) => { err = e; }).then(() => { // Emulate a finally // Handle response logging and cleanup when request is finished // This ensures that the default error handler is done onFinished(ctx.response.res, onResponseFinished.bind(ctx)); if (err) { throw err; // rethrow } }); }; }; /** * Middleware which adds methods this.time(label) and this.timeEnd(label) * to koa context. * * Parameters: * - opts: object with the following optional properties * - logLevel: name of log level to use; defaults to 'trace' * - updateLogFields: function which will be called with * arguments (fields) in koa context; can update fields or * return a new object. * * Must use(koaBunyanLogger()) before using this middleware. */ module.exports.timeContext = (opts) => { opts = opts || {}; const logLevel = opts.logLevel || 'trace'; const { updateLogFields } = opts; function time(label) { /* jshint validthis:true */ const startTimes = this._timeContextStartTimes; if (startTimes[label]) { this.log.warn('time() called for previously used label %s', label); } startTimes[label] = new Date().getTime(); } function timeEnd(label) { /* jshint validthis:true */ const startTimes = this._timeContextStartTimes; const startTime = startTimes[label]; if (!startTime) { // whoops! this.log.warn('timeEnd() called without time() for label %s', label); return; } const duration = new Date().getTime() - startTime; let fields = { label, duration, msg: `${label}: ${duration}ms` }; fields = updateFields(this, updateLogFields, fields); this.log[logLevel](fields); startTimes[label] = null; } return (ctx, next) => { ctx._timeContextStartTimes = {}; ctx.time = time; ctx.timeEnd = timeEnd; return next(); }; }; // Export our copy of bunyan module.exports.bunyan = bunyan;