Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,155 @@ connection with a server. For example, using the
});
});

## Tracing with .child()

This module allows the caller to create a "child" object of any of JsonClient,
StringClient or HttpClient. To create a `child` object one does:

childClient = client.child(options);

and uses `childClient` in place of `client`. The options supported are:

|Name | Type | Description |
| :--- | :----: | :---- |
|afterSync|Function|Function for tracing (run after every request, after the client has received the response)|
|beforeSync|Function|Function for tracing (run before every request)|

where these hook functions (if passed) will be called before and/or after
every client request with this child client. These are useful for integrating
with a tracing framework such as [OpenTracing](http://opentracing.io/), as they
allow passing in a closure such that all outgoing requests for a given trace can
be tagged with the appropriate headers.

If passed, the value of the `beforeSync` parameter should be a function with the
following prototype:

function beforeSync(opts) {

where `opts` is an object containing the options for the request. Some things
you might want to do in this beforeSync function include:

* writing a trace log message indicating that you're making a request
* modifying opts.headers to include additional headers in the outbound request

If passed, the value of the `afterSync` parameter should be a function with the
following prototype:

function afterSync(err, req, res) {

which takes an error object (if the request failed), and the
[req](http://restify.com/#request-api) and
[res](http://restify.com/#response-api) objects as defined in the restify
documentation. The `afterSync` caller is most useful for logging the fact that a
request completed and indicating errors or response details to a tracing
system.

A full example might be:

var clients = require('restify-clients');

var client = clients.createJsonClient({url: 'http://127.0.0.1:8080'});

var childClient1 = client.child({
beforeSync: function (opts) {
opts.headers['client-number'] = 1;
}, afterSync: function (err, req, res) {
console.error('code: %d', res.statusCode);
}
});

var childClient2 = client.child({
beforeSync: function (opts) {
opts.headers['client-number'] = 2;
}, afterSync: function (err, req, res) {
console.error('code: %d', res.statusCode);
}
});

// makes a `GET /hello` request with `client-number: 1` header
childClient1.get('/hello', function(err, req, res, obj) {
console.log('%j', obj);
});

// makes a `GET /hello` request with `client-number: 2` header
childClient2.get('/hello', function(err, req, res, obj) {
console.log('%j', obj);
});


where the two clients will have the same base parameters but will include
different 'client-number' headers in their requests and:

code: <HTTP status code>

will be written to stderr as each request completes.

The intention here is that you can use this to create a new client that wraps
some additional information. Especially when you'd like a client to always add
headers specific to that client. Another example using a restify server:

var clients = require('restify-clients');
var restify = require('restify');

var exampleClient = clients.createStringClient({url: 'http://0.0.0.0:8080'});
var server = restify.createServer({name: 'ExampleApp'});

server.use(function (req, res, next) {
// Add req.exampleClient to every req object so that handlers can use
// that as their client and not have to remember to add the request-id
// header themselves.
req.exampleClient = exampleClient.child({
beforeSync: function (opts) {
opts.headers['request-id'] = req.getId()
}
});
next();
});

server.get({
name: 'GetHello',
path: '/hello'
}, function (req, res, next) {
var client = req.exampleClient;

// This request will have the 'request-id: ...' header added which
// matches the *inbound* request we're handling, because the
// req.exampleClient was created here with a beforeSync() function that
// adds that to our request for us without modification to this client
// call.
client.get('/example', function (err, _req, _res, body) {
// ... handle results
});

next();
});

server.listen(function () {
console.log('listening at %s:%d', server.address().address,
server.address().port);
});

This shows a restify server where any call to `/hello` results in a call to
http://0.0.0.0:8080/example with a `request-id:` header added that matches the
original request this handler is part of.

### NOTES

* The beforeSync() and afterSync() functions are *sychronous*.

* It's not possible to call .child() on a client that's already a child.
Doing so will throw an assertion indicating that grandchildren are not
supported.

* Child connections will share the same agent and connection pool as the
parent object they were .child()ed from. This means only the parent should
call .close().


## Contributing

Add unit tests for any new or changed functionality. Ensure that lint and style


## Contributing

Expand Down
61 changes: 61 additions & 0 deletions lib/HttpClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var querystring = require('querystring');
var url = require('url');
var util = require('util');

var _ = require('lodash');
var assert = require('assert-plus');
var backoff = require('backoff');
var mime = require('mime');
Expand Down Expand Up @@ -400,6 +401,11 @@ function HttpClient(options) {

this.key = options.key;
this.name = options.name || 'HttpClient';

// These will be used when tracing is enabled.
this._afterSync = null;
this._beforeSync = null;

this.passphrase = options.passphrase;
this.pfx = options.pfx;

Expand Down Expand Up @@ -514,6 +520,31 @@ util.inherits(HttpClient, EventEmitter);
module.exports = HttpClient;


HttpClient.prototype.child = function child(options) {
assert.object(options, 'options');

var self = this;
var childClient;

assert.ok(!self._afterSync, 'grandchildren not supported');
assert.ok(!self._beforeSync, 'grandchildren not supported');

// We use _.clone() here to create a copy of the parent object so that we
// share all the open keepalive connections and the this.agent.
childClient = _.clone(self);

// add before and after hooks if they're passed
if (options.beforeSync) {
childClient._beforeSync = options.beforeSync;
}

if (options.afterSync) {
childClient._afterSync = options.afterSync;
}

return (childClient);
};

HttpClient.prototype.close = function close() {
var sockets = this.agent.sockets;
Object.keys((sockets || {})).forEach(function (k) {
Expand Down Expand Up @@ -610,15 +641,45 @@ HttpClient.prototype.basicAuth = function basicAuth(username, password) {
return (this);
};

function afterCaller(afterFn, err, req) {
if (err) {
afterFn(err);
return;
}

req.on('result', function _onResult(res_err, res) {
res.on('end', function () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind naming this function?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at all. Sorry I missed that one.

return afterFn(res_err, req, res);
});
});
}

HttpClient.prototype.request = function request(opts, cb) {
assert.object(opts, 'options');
assert.func(cb, 'callback');

var self = this;

function _callbackCaller(callback) {
return (function _callCallback(err, req) {
afterCaller(self._afterSync, err, req);

// call the original callback
return callback.apply(self, arguments);
});
}

/* eslint-disable no-param-reassign */
if (self._afterSync) {
cb = _callbackCaller(cb);
}
cb = once(cb);
/* eslint-enable no-param-reassign */

if (self._beforeSync) {
self._beforeSync(opts);
}

opts.audit = this.audit;

if (opts.retry === false) {
Expand Down
129 changes: 129 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ function sendJsonNull(req, res, next) {
next();
}

// sends back a body with the contents of the restify-clients-test-header received
function sendTestHeader(req, res, next) {
res.header('content-type', 'json');
res.send(200, req.header('restify-clients-test-header', 'missing'));
next();
}

function getLog(name, stream, level) {
return (bunyan.createLogger({
level: (process.env.LOG_LEVEL || level || 'fatal'),
Expand Down Expand Up @@ -1271,4 +1278,126 @@ describe('restify-client tests', function () {
});
});
});

// Shows that the .before and .after functions are called for each client type.
describe('before/after hooks for .child()', function () {
var _clients = {
HttpClient: {
creator: clients.createHttpClient
}, JsonClient: {
creator: clients.createJsonClient
}, StringClient: {
creator: clients.createStringClient
}
};
var server = restify.createServer({});

function _getBody(clientType, req, data, cb) {
if (clientType === 'HttpClient') {
req.on('result', function (err, res) {
var body = '';

assert.ifError(err);

res.setEncoding('utf8');
res.on('data', function (chunk) {
body += chunk;
});

res.on('end', function () {
cb(body.replace(/"/g, ''));
});
});
} else {
cb(data.replace(/"/g, ''));
}
}

before(function (callback) {
var url = 'http://127.0.0.1';

server.get('/testheader', sendTestHeader);
server.listen();
url = url + ':' + server.address().port;

// create the clients
Object.keys(_clients).forEach(function (clientName) {
var _client = _clients[clientName];
_client.handle = _client.creator({url: url});
});

callback();
});

Object.keys(_clients).forEach(function (clientName) {
var _client = _clients[clientName];

// .child() of {Http,Json,String}Client should be able to use .before and .after
it('request from ' + clientName + '.child()', function (done) {
var afterRan = false;
var beforeRan = false;
var childClient;

childClient = _client.handle.child({
beforeSync: function (opts) {
beforeRan = true;
opts.headers['restify-clients-test-header'] = clientName;
}, afterSync: function (err, req, res) {
assert.ifError(err);
afterRan = true;
}
});

childClient.get('/testheader', function (err, req, res, data) {
assert.ifError(err);
_getBody(clientName, req, data, function (body) {
assert.equal(body, clientName);
assert.equal(beforeRan, true);
assert.ok(afterRan, true);

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

// non-child call should not have been modified
it('request from ' + clientName, function (done) {
_client.handle.get('/testheader', function (err, req, res, data) {
assert.ifError(err);
_getBody(clientName, req, data, function (body) {
assert.equal(body, 'missing');
done();
});
});
});
});

it('ensure grandchild protection enabled', function (done) {
var child;

child = _clients.JsonClient.handle.child({
beforeSync: function (opts) {
console.log('hello world');
}
});

assert.throws(function createGrandchild() {
child.child({
beforeSync: function (opts) {
console.log('hello hello world');
}
});
}, /grandchildren not supported/);

done();
});

after(function (callback) {
Object.keys(_clients).forEach(function (clientName) {
_clients[clientName].handle.close();
});
server.close();
callback();
});
});
});