Skip to content

Instantly share code, notes, and snippets.

@terrisgit
Last active May 10, 2024 03:20
Show Gist options
  • Save terrisgit/4a531579c07a4980db9c5beeb6f07912 to your computer and use it in GitHub Desktop.
Save terrisgit/4a531579c07a4980db9c5beeb6f07912 to your computer and use it in GitHub Desktop.
Express Gateway custom Policy for logging requests
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