Last active
May 10, 2024 03:20
-
-
Save terrisgit/4a531579c07a4980db9c5beeb6f07912 to your computer and use it in GitHub Desktop.
Express Gateway custom Policy for logging requests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const humanizeDuration = require('humanize-duration'); | |
const Loggers = require('@goodware/log'); | |
const { PassThrough } = require('stream'); | |
const { ulid } = require('ulidx'); | |
/** | |
* See https://github.com/expressjs/body-parser for more options | |
*/ | |
const jsonParser = require('express').json({ limit: '10mb' }); | |
const urlEncodedParser = require('express').urlencoded({ extended: true }); | |
// Logging-related constants | |
let log; | |
let shouldLog; | |
let shouldLogBody; | |
// Which headers to log | |
const headersToLog = [ | |
'referrer', 'date', 'content-type', 'content-length', 'host']; | |
/** | |
* Initialize @goodware/log, a Winston3 wrapper. | |
* See https://runkit.com/dev-guy/exploring-goodware-log for usage | |
*/ | |
function initLogger() { | |
const config = { | |
console: { data: true }, | |
categories: { | |
api: { | |
console: 'verbose', | |
}, | |
}, | |
}; | |
const loggers = new Loggers(config); | |
log = loggers.logger('api'); | |
shouldLog = log.isLevelEnabled('http'); | |
shouldLogBody = log.isLevelEnabled('verbose'); | |
} | |
initLogger(); | |
/** | |
* Policy definition | |
*/ | |
module.exports = { | |
name: 'request-logger', | |
policy: () => (req, res, next) => { | |
if (!shouldLog) return next(); | |
// ================================================= | |
// Declare constants across the request and response | |
const startTime = new Date(); | |
const correlationId = ulid(); | |
const { path, method, headers } = req; | |
/** | |
* This object is logged for requests and responses | |
*/ | |
const data = { | |
method, | |
path, | |
correlationId, | |
clientIp: headers['x-forwarded-for'] || req.connection.remoteAddress, | |
startTime: startTime.toISOString(), | |
}; | |
// ============================== | |
// Copy request headers to 'data' | |
{ | |
// eslint-disable-next-line no-restricted-syntax | |
for (const hdr of headersToLog) { | |
const value = headers[hdr]; | |
if (value) data[hdr] = value; | |
} | |
} | |
res.once('finish', () => { | |
// ================ | |
// Log the response | |
const endTime = new Date(); | |
const duration = endTime - startTime; | |
const { statusCode } = res; | |
Object.assign(data, { | |
endTime: endTime.toISOString(), | |
duration, | |
durationText: humanizeDuration(duration), | |
statusCode, | |
}); | |
const obj = { message: `${method} ${statusCode} ${path}`, data }; | |
// Log 5xx status codes as error; otherwise log as http | |
const level = statusCode >= 400 ? 'error' : 'http'; | |
if (level === 'error') obj.tags = ['http', 'noLogStack']; | |
log[level](['response'], obj); | |
}); | |
// =============== | |
// Log the request | |
const obj = { message: `${method} ${path}`, data }; | |
if (!shouldLogBody) { | |
log.http(['request'], obj); | |
} else { | |
// ====================================================== | |
// Log the request body | |
// https://www.express-gateway.io/exploit-request-stream/ | |
const stream = new PassThrough(); | |
req.egContext.requestStream = stream; | |
req.pipe(stream); | |
// eslint-disable-next-line no-undef | |
jsonParser(req, res, () => urlEncodedParser(req, res, () => { | |
let { body } = req; | |
delete req.body; | |
if (body instanceof Object && !(body instanceof Array)) { | |
if (body.password) { | |
body = { | |
...body, | |
}; | |
delete body.password; | |
} | |
// Set data.requestBody if body is not empty | |
// See https://stackoverflow.com/questions/679915/how-do-i-test-for-an-empty-javascript-object | |
// eslint-disable-next-line no-restricted-syntax, guard-for-in, no-unused-vars, no-unreachable-loop | |
for (const _ in body) { | |
data.requestBody = body; | |
break; | |
} | |
} else if (body !== null && body !== undefined) { | |
data.requestBody = body; | |
} | |
log.http(['request'], obj); | |
delete data.requestBody; | |
})); | |
// next() must be called outside the callback provided to jsonParser() | |
} | |
return next(); | |
}, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment