Skip to content

Commit

Permalink
Merge 87ffb2e into a1b37a3
Browse files Browse the repository at this point in the history
  • Loading branch information
ugolas committed Nov 6, 2018
2 parents a1b37a3 + 87ffb2e commit 86bc4a3
Show file tree
Hide file tree
Showing 8 changed files with 2,023 additions and 181 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
language: node_js

node_js:
- "4"
- "6"
- "8"
- "node"
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Middleware for logging request/responses in Express apps
- Exclude response body fields
- Exclude response specific headers
- Exclude specific URLs from logging
- Supported by Node v6 and above.

## Installation

Expand Down Expand Up @@ -118,6 +119,26 @@ Array of strings - pass the header names you wish to exclude from the audit (sen

Array of strings - pass the fields you wish to mask in the headers of the responses (senstitive data like authorization headers etc..).

##### levels

Map of statusCodes to log levels. By default the audit is logged with level 'info'. It is possible to override it by configuration according to the statusCode of the response:

- Key: status code, or status code group: '2xx', '401', etc.. First we try to match by exact match (for example 400), if no key found by exact match we fallback to match bu group (4xx).
- Value: log level, valid values: 'trace', 'debug', 'info', 'warn', 'error'.
- Configuration errors are ignored and the log is info by default.


Example:
```
levels: {
"2xx":"info", // All 2xx responses are info
"401":"warn", // 401 are warn
"4xx':info", // All 4xx except 401 are info
"503":"warn",
"5xx":"error" // All 5xx except 503 are errors, 503 is warn,
}
```


### Example

Expand Down
30 changes: 17 additions & 13 deletions lib/express-logger.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
'use strict';

var _ = require('lodash'),
logger = require('bunyan').createLogger({name: 'ExpressLogger'}),
logger = require('bunyan').createLogger({ name: 'ExpressLogger' }),
loggerHelper = require('./logger-helper'),
setupOptions,
flatten = require('flat');

var audit = function (req, res, next){
var audit = function (req, res, next) {
var oldWrite = res.write;
var oldEnd = res.end;
var chunks = [];

// Log start time of request processing
req.timestamp = new Date();

if (setupOptions.doubleAudit){
if (setupOptions.doubleAudit) {
loggerHelper.auditRequest(req, setupOptions);
}

res.write = function (chunk){
res.write = function (chunk) {
chunks.push(new Buffer(chunk));
oldWrite.apply(res, arguments);
};

// decorate response#end method from express
res.end = function (chunk){
res.end = function (chunk) {
res.timestamp = new Date();
if (chunk){
if (chunk) {
chunks.push(new Buffer(chunk));
}

Expand All @@ -41,7 +41,7 @@ var audit = function (req, res, next){
next();
};

module.exports = function(options){
module.exports = function (options) {
options = options || {};
var defaults = {
logger: logger,
Expand All @@ -61,7 +61,13 @@ module.exports = function(options){
excludeHeaders: []
},
doubleAudit: false,
excludeURLs: []
excludeURLs: [],
levels: {
'2xx': 'info',
'3xx': 'info',
'4xx': 'info',
'5xx': 'info'
}
};

_.defaultsDeep(options, defaults);
Expand All @@ -70,13 +76,11 @@ module.exports = function(options){
};

//convert all options fields that need to be array by default
function validateArrayFields(options, defaults)
{
function validateArrayFields(options, defaults) {
Object.keys(flatten(options)).forEach((key) => {
let optionValue = _.get(options, key);
let defaultValue =_.get(defaults, key)
if(_.isArray(defaultValue) && !_.isArray(optionValue))
{
let defaultValue = _.get(defaults, key)
if (_.isArray(defaultValue) && !_.isArray(optionValue)) {
_.set(options, key, [optionValue]);
}
});
Expand Down
62 changes: 33 additions & 29 deletions lib/logger-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ var utils = require('./utils');
var _ = require('lodash');
var ALL_FIELDS = '*';
const NA = 'N/A';
const DEFAULT_LOG_LEVEL = 'info';

var auditRequest = function(req, options){
var auditRequest = function (req, options) {
var shouldAudit = utils.shouldAuditURL(options.excludeURLs, req);

if (shouldAudit){
if (shouldAudit) {
var request;

if (options.setupFunc){
if (options.setupFunc) {
options.setupFunc(req, res);
}

if (options.request.audit){
if (options.request.audit) {
request = getRequestAudit(req, options);
}

Expand All @@ -24,29 +25,29 @@ var auditRequest = function(req, options){
};

// Add additional audit fields
if (req && req.additionalAudit){
if (req && req.additionalAudit) {
auditObject = Object.assign(auditObject, req.additionalAudit);
}

options.logger.info(auditObject, 'Inbound Transaction');
}
};

var auditResponse = function(req, res, options){
var auditResponse = function (req, res, options) {
var request;
var response;

var shouldAudit = utils.shouldAuditURL(options.excludeURLs, req);
if (shouldAudit){
if (options.setupFunc){
if (shouldAudit) {
if (options.setupFunc) {
options.setupFunc(req, res);
}

if (options.request.audit){
if (options.request.audit) {
request = getRequestAudit(req, options);
}

if (options.response.audit){
if (options.response.audit) {
response = getResponseAudit(req, res, options);
}

Expand All @@ -56,15 +57,21 @@ var auditResponse = function(req, res, options){
};

// Add additional audit fields
if (req && req.additionalAudit){
if (req && req.additionalAudit) {
auditObject = Object.assign(auditObject, req.additionalAudit);
}

options.logger.info(auditObject, 'Inbound Transaction');
let level = DEFAULT_LOG_LEVEL; // Default
if (res) {
// Make sure the resolved log level is supported by our logger:
let resolvedLogLevel = utils.getLogLevel(res.statusCode, options.levels);
level = options.logger[resolvedLogLevel] ? resolvedLogLevel : level;
}
options.logger[level](auditObject, 'Inbound Transaction');
}
};

function getRequestAudit(req, options){
function getRequestAudit(req, options) {
var headers = _.get(req, 'headers');
var requestFullURL = utils.getUrl(req);
var requestRoute = utils.getRoute(req);
Expand All @@ -73,7 +80,7 @@ function getRequestAudit(req, options){
var URLParams = req && req.params ? req.params : NA;
var timestamp = req && req.timestamp ? req.timestamp.toISOString() : NA;
var timestamp_ms = req && req.timestamp ? req.timestamp.valueOf() : NA;
var requestBody = _.get(req, 'body'); //handle body clone the original body
var requestBody = _.get(req, 'body'); //handle body clone the original body

requestBody = handleJson(requestBody, options.logger, options.request.excludeBody, options.request.maskBody);

Expand All @@ -87,37 +94,34 @@ function getRequestAudit(req, options){
url: requestFullURL,
url_route: requestRoute,
query: queryParams,
headers: _.isEmpty(headers) ? NA: headers,
headers: _.isEmpty(headers) ? NA : headers,
timestamp: timestamp,
timestamp_ms: timestamp_ms,
body: _.isEmpty(requestBody) ? NA: JSON.stringify(requestBody)
body: _.isEmpty(requestBody) ? NA : JSON.stringify(requestBody)
};

return auditObject;
}

function handleJson(obj, logger, excludeFields, maskFields) {

if(_.includes(excludeFields, ALL_FIELDS))
{
if (_.includes(excludeFields, ALL_FIELDS)) {
obj = undefined;
}
else if(obj)
{
else if (obj) {
try {
//in case obj is string and need to convert to json
if(typeof obj == 'string') {
if (typeof obj == 'string') {
obj = JSON.parse(obj);
}
else if(typeof obj != 'object')
{
else if (typeof obj != 'object') {
throw new Error('only json obj can be exclude/masked');
}

//order is important because body is clone first
obj = utils.maskJson(obj, maskFields);
obj = utils.cleanOmitKeys(obj, excludeFields);
} catch (err){
} catch (err) {
logger.warn('Error parsing json: ' + err);
obj = undefined;
}
Expand All @@ -129,17 +133,17 @@ function handleJson(obj, logger, excludeFields, maskFields) {



function getResponseAudit(req, res, options){
function getResponseAudit(req, res, options) {
var headers = _.get(res, '_headers');
var elapsed = req && res ? res.timestamp - req.timestamp : 0;
var timestamp = res && res.timestamp ? res.timestamp.toISOString() : NA;
var timestamp_ms = res && res.timestamp ? res.timestamp.valueOf() : NA;
var statusCode = res && res.statusCode ? res.statusCode : NA;
var responseBody = _.get(res, '_body'); //no need to clone because its not the original body
var responseBody = _.get(res, '_body'); //no need to clone because its not the original body

if (headers && headers['content-type'] && headers['content-type'].includes('application/json')) {
// Handle JSON only for json responses:
responseBody = handleJson(responseBody, options.logger , options.response.excludeBody, options.response.maskBody);
responseBody = handleJson(responseBody, options.logger, options.response.excludeBody, options.response.maskBody);
}

headers = handleJson(headers, options.logger, options.response.excludeHeaders, options.response.maskHeaders);
Expand All @@ -149,8 +153,8 @@ function getResponseAudit(req, res, options){
timestamp: timestamp,
timestamp_ms: timestamp_ms,
elapsed: elapsed,
headers: _.isEmpty(headers) ? NA: headers,
body: _.isEmpty(responseBody) ? NA: JSON.stringify(responseBody)
headers: _.isEmpty(headers) ? NA : headers,
body: _.isEmpty(responseBody) ? NA : JSON.stringify(responseBody)
};

return auditObject;
Expand Down
39 changes: 29 additions & 10 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,29 @@
var _ = require('lodash');
const MASK = 'XXXXX';
const NA = 'N/A';
const VALID_LEVELS = ['trace', 'debug', 'info', 'warn', 'error']
const DEFAULT_LEVEL = 'info';

var getUrl = function(req){
var getUrl = function (req) {
var url = req && req.url || NA;

return url;
};

var getRoute = function(req) {
var getRoute = function (req) {
var url = NA;

if (req){
if (req) {
var route = req.baseUrl;
if (req.route && route){
if (req.route && route) {
url = route + req.route.path;
}
}

return url;
};

function cleanOmitKeys (obj, omitKeys) {
function cleanOmitKeys(obj, omitKeys) {
if (obj && !_.isEmpty(omitKeys)) {
Object.keys(obj).forEach(function (key) {
if (_.some(omitKeys, omitKey => key === omitKey)) {
Expand All @@ -36,15 +38,15 @@ function cleanOmitKeys (obj, omitKeys) {
return obj;
};

var shouldAuditURL = function(excludeURLs, req){
return _.every(excludeURLs, function(path){
var shouldAuditURL = function (excludeURLs, req) {
return _.every(excludeURLs, function (path) {
var url = getUrl(req);
var route = getRoute(req);
return !(url.includes(path) || route.includes(path));
});
};

var maskJson = function(jsonObj, fieldsToMask){
var maskJson = function (jsonObj, fieldsToMask) {
let jsonObjCopy = _.cloneDeepWith(jsonObj, function (value, key) {
if (_.includes(fieldsToMask, key)) {
return MASK
Expand All @@ -53,11 +55,28 @@ var maskJson = function(jsonObj, fieldsToMask){
return jsonObjCopy;
};

var getLogLevel = function (statusCode, levelsMap) {
let level = DEFAULT_LEVEL; // Default

if (levelsMap) {
let status = statusCode.toString();

if (levelsMap[status]) {
level = VALID_LEVELS.includes(levelsMap[status]) ? levelsMap[status] : level;
} else {
let statusGroup = `${status.substring(0, 1)}xx`; // 5xx, 4xx, 2xx, etc..
level = VALID_LEVELS.includes(levelsMap[statusGroup]) ? levelsMap[statusGroup] : level;
}
}

return level;
}

module.exports = {
getRoute: getRoute,
getUrl: getUrl,
shouldAuditURL: shouldAuditURL,
maskJson:maskJson,
cleanOmitKeys:cleanOmitKeys
maskJson: maskJson,
cleanOmitKeys: cleanOmitKeys,
getLogLevel: getLogLevel
};
Loading

0 comments on commit 86bc4a3

Please sign in to comment.