Skip to content

Commit

Permalink
Add key-value pairs format
Browse files Browse the repository at this point in the history
  • Loading branch information
danivek committed Dec 16, 2017
1 parent 83486e9 commit d60e00f
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const contextlog = new Contextlog({
```

**Options:**
- **keyValue** (optional): key-value pairs format (except for `req.body` and `res.body` that will be stringify). Default = false;
- **skipUrls** (optional): Array of regex to skip/exclude specific URLs from logging.
- **application** (optional): *object* or *function*. Describes the application context.
- **user** (optional): *object* or *function*. Describes the user context.
Expand Down
61 changes: 48 additions & 13 deletions lib/Contextlog.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const os = require('os');
const useragent = require('useragent');


function getOsInfo() {
return {
hostname: os.hostname(),
Expand All @@ -25,14 +24,43 @@ function getProcessInfo() {
};
}

function flatten(obj) {
const newObj = {};
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
const temp = flatten(obj[key]);
Object.keys(temp).forEach((key2) => {
newObj[`${key}_${key2}`] = temp[key2];
});
} else {
newObj[key] = obj[key];
}
});

return newObj;
}

function removeEmpty(obj) {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
removeEmpty(obj[key]);
if (Object.keys(obj[key]).length === 0) delete obj[key];
}

if (obj[key] === undefined) delete obj[key];
});

return obj;
}

class Contextlog {
constructor(options) {
const opts = options || {};
this.options = {};
this.MASK_URL_PARAMS = ['x-api-key'];
this.MASK_HEADERS = ['authorization', 'x-api-key'];

// this.options.skip = opts.skip;
this.options.keyValue = opts.keyValue || false;
this.options.skipUrls = opts.skipUrls;
this.options.maskUrlParams = this.MASK_URL_PARAMS.concat(opts.maskUrlParams || []);
this.options.maskHeaders = this.MASK_HEADERS.concat(opts.maskHeaders || []);
Expand All @@ -43,7 +71,6 @@ class Contextlog {
this.options.parseUserAgent = opts.parseUserAgent || false;
this.options.reqBody = opts.reqBody || false;
this.options.resBody = opts.resBody || false;
this.options.body = opts.body;
}

mask(string) {
Expand Down Expand Up @@ -109,14 +136,22 @@ class Contextlog {
} else {
context.req.body = req.body;
}

if (this.options.keyValue) {
context.req.body = JSON.stringify(context.req.body);
}
}

if (this.options.reqBody) {
if (this.options.resBody) {
if (typeof this.options.resBody === 'function' && res.body) {
context.res.body = this.options.resBody(res.body);
} else {
context.res.body = res.body;
}

if (this.options.keyValue) {
context.res.body = JSON.stringify(context.res.body);
}
}

if (this.options.parseUserAgent) {
Expand All @@ -128,7 +163,7 @@ class Contextlog {
}

standard({ req, res }, context) {
const ctx = {
let ctx = {
date: new Date().toISOString(),
};

Expand All @@ -148,7 +183,8 @@ class Contextlog {
Object.assign(ctx, this.options.custom({ req, res }, context));
}

return ctx;
ctx = removeEmpty(ctx);
return this.options.keyValue ? flatten(ctx) : ctx;
}

request({ req, res }, context) {
Expand All @@ -159,25 +195,24 @@ class Contextlog {
if (skip) return undefined;
}

const meta = Object.assign({}, this.standard({ req, res }, context));
Object.assign(meta, this.getRequestResponseInfo(req, res));
const ctx = Object.assign({}, this.standard({ req, res }, context), this.getRequestResponseInfo(req, res));

return meta;
return this.options.keyValue ? flatten(ctx) : ctx;
}

error(error, { req, res }, context) {
const meta = Object.assign({}, this.standard({ req, res }, context));
const ctx = Object.assign({}, this.standard({ req, res }, context));
const errorMeta = {
error: true,
os: getOsInfo(),
process: getProcessInfo(),
message: error.message,
stack: error.stack,
};
Object.assign(meta, errorMeta);
Object.assign(meta, this.getRequestResponseInfo(req, res));

return meta;
Object.assign(ctx, errorMeta, this.getRequestResponseInfo(req, res));

return this.options.keyValue ? flatten(ctx) : ctx;
}
}

Expand Down
123 changes: 123 additions & 0 deletions test/unit/Contextlog.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,52 @@ describe('Contextlog', () => {
expect(context).to.have.property('custom').to.eql('custom');
done();
});

it('should clean empty standard context', (done) => {
const contextlog = new Contextlog({
application: { name: pkg.name, version: '1.0.0' },
user: ({ req }) => ({ id: req.user && req.user.id }),
resource: ({ req }) => ({ type: req.params.type, id: req.params.id }),
custom: ({ req }) => ({ custom: req.custom }),
});
const req = httpMocks.createRequest();
const res = httpMocks.createResponse();

const context = contextlog.standard({ req, res });

expect(context).to.have.property('date');
expect(context).to.have.property('application').to.eql({ name: 'contextlog', version: '1.0.0' });
expect(context).to.not.have.property('user');
expect(context).to.not.have.property('resource');
expect(context).to.not.have.property('custom');
done();
});

it('should return standard context with keyValue option', (done) => {
const contextlog = new Contextlog({
keyValue: true,
application: { name: pkg.name, version: '1.0.0' },
user: ({ req }) => ({ id: req.user.id }),
resource: ({ req }) => ({ type: req.params.type, id: req.params.id }),
custom: ({ req }) => ({ custom: req.custom }),
});
const req = httpMocks.createRequest();
const res = httpMocks.createResponse();

req.user = { id: 'john.doe' };
req.params = { type: 'blog', id: '1' };
req.custom = 'custom';

const context = contextlog.standard({ req, res });
expect(context).to.have.property('date');
expect(context).to.have.property('application_name').to.eql('contextlog');
expect(context).to.have.property('application_version').to.eql('1.0.0');
expect(context).to.have.property('user_id').to.eql('john.doe');
expect(context).to.have.property('resource_type').to.eql('blog');
expect(context).to.have.property('resource_id').to.eql('1');
expect(context).to.have.property('custom').to.eql('custom');
done();
});
});

describe('request', () => {
Expand Down Expand Up @@ -247,6 +293,50 @@ describe('Contextlog', () => {
expect(context.req.userAgent).to.have.property('family').to.eql('Chrome');
done();
});

it('should get request context with keyValue option', (done) => {
const contextlog = new Contextlog({
keyValue: true,
reqBody: true,
resBody: true,
});
const req = httpMocks.createRequest({
method: 'POST',
url: '/path/to/endpoint',
headers: {
host: 'localhost',
connection: 'keep-alive',
},
body: {
data: 'data',
password: 'password',
},
});
req.httpVersion = '1.1';
req.ip = '0.0.0.0';
const res = httpMocks.createResponse();
res.statusCode = '200';
res._headers = {
'content-type': 'application/json; charset=utf-8',
};
res.body = {
data: 'data',
};

const requestContext = contextlog.request({ req, res });
expect(requestContext).to.have.property('date');
expect(requestContext).to.have.property('req_body').to.eql('{"data":"data","password":"password"}');
expect(requestContext).to.have.property('req_headers_connection').to.eql('keep-alive');
expect(requestContext).to.have.property('req_headers_host').to.eql('localhost');
expect(requestContext).to.have.property('req_httpVersion').to.eql('1.1');
expect(requestContext).to.have.property('req_method').to.eql('POST');
expect(requestContext).to.have.property('req_remoteAddress').to.eql('0.0.0.0');
expect(requestContext).to.have.property('req_url').to.eql('/path/to/endpoint');
expect(requestContext).to.have.property('res_body').to.eql('{"data":"data"}');
expect(requestContext).to.have.property('res_headers_content-type').to.eql('application/json; charset=utf-8');
expect(requestContext).to.have.property('res_statusCode').to.eql('200');
done();
});
});

describe('error', () => {
Expand Down Expand Up @@ -285,5 +375,38 @@ describe('Contextlog', () => {

done();
});

it('should get error context with keyValue option', (done) => {
const contextlog = new Contextlog({
keyValue: true,
});
const req = httpMocks.createRequest();
const res = httpMocks.createResponse();

const err = new Error('boom');
const context = contextlog.error(err, { req, res });
expect(context).to.have.property('error').to.eql(true);
expect(context).to.have.property('message').to.eql('boom');
expect(context).to.have.property('stack');
// Os metadata
expect(context).to.have.property('os_hostname');
expect(context).to.have.property('os_plateform');
expect(context).to.have.property('os_loadavg_0');
expect(context).to.have.property('os_uptime');
expect(context).to.have.property('os_freemem');
// Process metadata
expect(context).to.have.property('process_pid');
expect(context).to.have.property('process_uid');
expect(context).to.have.property('process_gid');
expect(context).to.have.property('process_cwd');
expect(context).to.have.property('process_execPath');
expect(context).to.have.property('process_version');
expect(context).to.have.property('process_argv_0');
expect(context).to.have.property('process_memoryUsage_rss');
expect(context).to.have.property('process_memoryUsage_heapUsed');
expect(context).to.have.property('process_memoryUsage_external');

done();
});
});
});

0 comments on commit d60e00f

Please sign in to comment.