Skip to content

Commit

Permalink
support fetchr stats collection and monitoring
Browse files Browse the repository at this point in the history
  • Loading branch information
lingyan committed Jun 28, 2016
1 parent c64b9aa commit 067d92b
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 35 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,50 @@ var fetcher = new Fetcher({
});
```

## Stats Monitoring & Analysis

To collect fetcher service's success/failure/latency stats, you can configure `statsCollector` for `Fetchr`. The `statsCollector` function will be invoked with one argumment: `stats`. The `stats` object will contain the following fields:

* **resource:** The name of the resource for the request
* **operation:** The name of the operation, `create|read|update|delete`
* **params:** The params object for the resource
* **statusCode:** The status code of the response
* **err:** The error object of failed request; null if request was successful
* **time:** The time spent for this request, in milliseconds

### Client

```js
var Fetcher = require('fetchr');
var fetcher = new Fetcher({
xhrPath: '/myCustomAPIEndpoint',
xhrTimeout: 4000,
statsCollector: function (stats) {
// just console logging as a naive example. there is a lot more you can do here,
// like aggregating stats or filtering out stats you don't want to monitor
console.log('Request for resource', stats.resource,
'with', stats.operation,
'returned statusCode:', stats.statusCode,
' within', stats.time, 'ms');
}
});
```

### Server

```js
app.use('/myCustomAPIEndpoint', Fetcher.middleware({
statsCollector: function (stats) {
// just console logging as a naive example. there is a lot more you can do here,
// like aggregating stats or filtering out stats you don't want to monitor
console.log('Request for resource', stats.resource,
'with', stats.operation,
'returned statusCode:', stats.statusCode,
' within', stats.time, 'ms');
}
}));
```

## API

- [Fetchr](https://github.com/yahoo/fetchr/blob/master/docs/fetchr.md)
Expand Down
58 changes: 45 additions & 13 deletions libs/fetcher.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,13 @@ function Request (operation, resource, options) {
corsPath: options.corsPath,
context: options.context || {},
contextPicker: options.contextPicker || {},
statsCollector: options.statsCollector,
_serviceMeta: options._serviceMeta || []
};
this._params = {};
this._body = null;
this._clientConfig = {};
this._startTime = 0;
}

/**
Expand Down Expand Up @@ -123,6 +125,33 @@ Request.prototype.clientConfig = function (config) {
return this;
};

/**
* capture meta data; capture stats for this request and pass stats data
* to options.statsCollector
* @method _captureMetaAndStats
* @param {Object} err The error response for failed request
* @param {Object} result The response data for successful request
*/
Request.prototype._captureMetaAndStats = function (err, result) {
var self = this;
var meta = (err && err.meta) || (result && result.meta);
if (meta) {
self.options._serviceMeta.push(meta);
}
var statsCollector = self.options.statsCollector;
if (typeof statsCollector === 'function') {
var stats = {
resource: self.resource,
operation: self.operation,
params: self._params,
statusCode: err ? err.statusCode : 200,
err: err,
time: Date.now() - self._startTime
};
statsCollector(stats);
}
};

/**
* Execute this fetcher request and call callback.
* @method end
Expand All @@ -132,28 +161,27 @@ Request.prototype.clientConfig = function (config) {
*/
Request.prototype.end = function (callback) {
var self = this;
function captureMeta (data) {
if (data.meta) {
self.options._serviceMeta.push(data.meta);
}
return data;
}
self._startTime = Date.now();

if (callback) {
return executeRequest(self, function (result) {
captureMeta(result);
return executeRequest(self, function requestSucceeded(result) {
self._captureMetaAndStats(null, result);
setImmediate(callback, null, result.data, result.meta);
}, function (err) {
captureMeta(err);
}, function requestFailed(err) {
self._captureMetaAndStats(err);
setImmediate(callback, err);
});
} else {
var promise = new Promise(function (resolve, reject) {
var promise = new Promise(function requestExecutor(resolve, reject) {
debug('Executing request %s.%s with params %o and body %o', self.resource, self.operation, self._params, self._body);
setImmediate(executeRequest, self, resolve, reject);
});
promise = promise.then(captureMeta, function (err) {
throw captureMeta(err);
promise = promise.then(function requestSucceeded(result) {
self._captureMetaAndStats(null, result);
return result;
}, function requestFailed(err) {
self._captureMetaAndStats(err);
throw err;
});
return promise;
}
Expand Down Expand Up @@ -284,6 +312,9 @@ Request.prototype._constructGroupUri = function (uri) {
* lodash pick predicate function with three arguments (value, key, object)
* @param {Function|String|String[]} [options.contextPicker.GET] GET context picker
* @param {Function|String|String[]} [options.contextPicker.POST] POST context picker
* @param {Function} [options.statsCollector] The function will be invoked with 1 argument:
* the stats object, which contains resource, operation, params (request params),
* statusCode, err, and time (elapsed time)
*/

function Fetcher (options) {
Expand All @@ -294,6 +325,7 @@ function Fetcher (options) {
corsPath: options.corsPath,
context: options.context,
contextPicker: options.contextPicker,
statsCollector: options.statsCollector,
_serviceMeta: this._serviceMeta
};
}
Expand Down
60 changes: 46 additions & 14 deletions libs/fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ function getErrorResponse(err) {
* @param {Object} options configuration options for Request
* @param {Object} [options.req] The request object from express/connect. It can contain per-request/context data.
* @param {Array} [options.serviceMeta] Array to hold per-request/session metadata from all service calls.
* @param {Function} [options.statsCollector] The function will be invoked with 1 argument:
* the stats object, which contains resource, operation, params (request params),
* statusCode, err, and time (elapsed time)
* @constructor
*/
function Request (operation, resource, options) {
Expand All @@ -92,6 +95,8 @@ function Request (operation, resource, options) {
this._params = {};
this._body = null;
this._clientConfig = {};
this._startTime = 0;
this._statsCollector = options.statsCollector;
}

/**
Expand Down Expand Up @@ -129,6 +134,34 @@ Request.prototype.clientConfig = function (config) {
return this;
};

/**
* capture meta data; capture stats for this request and pass stats data
* to options.statsCollector
* @method _captureMetaAndStats
* @param {Object} errData The error response for failed request
* @param {Object} result The response data for successful request
*/
Request.prototype._captureMetaAndStats = function (errData, result) {
var self = this;
var meta = (errData && errData.meta) || (result && result.meta);
if (meta) {
self.serviceMeta.push(meta);
}
var statsCollector = self._statsCollector;
if (typeof statsCollector === 'function') {
var err = errData && errData.err;
var stats = {
resource: self.resource,
operation: self.operation,
params: self._params,
statusCode: err ? err.statusCode : (result && result.meta && result.meta.statusCode || 200),
err: err,
time: Date.now() - self._startTime
};
statsCollector(stats);
}
};

/**
* Execute this fetcher request and call callback.
* @method end
Expand All @@ -137,26 +170,24 @@ Request.prototype.clientConfig = function (config) {
*/
Request.prototype.end = function (callback) {
var self = this;
var promise = new Promise(function (resolve, reject) {
self._startTime = Date.now();

var promise = new Promise(function requestExecutor(resolve, reject) {
setImmediate(executeRequest, self, resolve, reject);
});

promise = promise.then(function (result) {
if (result.meta) {
self.serviceMeta.push(result.meta);
}
promise = promise.then(function requestSucceeded(result) {
self._captureMetaAndStats(null, result);
return result;
}, function(errData) {
if (errData.meta) {
self.serviceMeta.push(errData.meta);
}
}, function requestFailed(errData) {
self._captureMetaAndStats(errData);
throw errData.err;
});

if (callback) {
promise.then(function (result) {
promise.then(function requestSucceeded(result) {
setImmediate(callback, null, result.data, result.meta);
}, function (err) {
}, function requestFailed(err) {
setImmediate(callback, err);
});
} else {
Expand Down Expand Up @@ -213,7 +244,6 @@ function Fetcher (options) {
this.options = options || {};
this.req = this.options.req || {};
this._serviceMeta = [];

}

Fetcher.services = {};
Expand Down Expand Up @@ -330,7 +360,8 @@ Fetcher.middleware = function (options) {
serviceMeta = [];
request = new Request(OP_READ, resource, {
req: req,
serviceMeta: serviceMeta
serviceMeta: serviceMeta,
statsCollector: options.statsCollector
});
request
.params(parseParamValues(qs.parse(path.join('&'))))
Expand Down Expand Up @@ -385,7 +416,8 @@ Fetcher.middleware = function (options) {
serviceMeta = [];
request = new Request(singleRequest.operation, singleRequest.resource, {
req: req,
serviceMeta: serviceMeta
serviceMeta: serviceMeta,
statsCollector: options.statsCollector
});
request
.params(singleRequest.params)
Expand Down
30 changes: 26 additions & 4 deletions tests/unit/libs/fetcher.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ function handleFakeXhr (request) {
request.respond(res.status, JSON.stringify(res.headers), res.text);
});
}

var context = {_csrf: 'stuff'};
var resource = defaultOptions.resource;
var params = defaultOptions.params;
Expand All @@ -75,12 +76,30 @@ var callback = defaultOptions.callback;
var resolve = defaultOptions.resolve;
var reject = defaultOptions.reject;

describe('Client Fetcher', function () {
var stats = null;

function statsCollector(s) {
stats = s;
}

var callbackWithStats = function (operation, done) {
return function (err, data, meta) {
expect(stats.resource).to.eql(resource);
expect(stats.operation).to.eql(operation);
expect(stats.time).to.be.at.least(0);
expect(stats.err).to.eql(err);
expect(stats.statusCode).to.eql((err && err.statusCode) || 200);
expect(stats.params).to.eql(params);
callback(operation, done)(err, data, meta);
};
};

describe('Client Fetcher', function () {
describe('DEFAULT', function () {
before(function () {
this.fetcher = new Fetcher({
context: context
context: context,
statsCollector: statsCollector
});
validateXhr = function (req) {
if (req.method === 'GET') {
Expand All @@ -92,7 +111,10 @@ describe('Client Fetcher', function () {
}
};
});
testCrud(params, body, config, callback, resolve, reject);
beforeEach(function () {
stats = null;
});
testCrud(params, body, config, callbackWithStats, resolve, reject);
after(function () {
validateXhr = null;
});
Expand Down Expand Up @@ -133,7 +155,7 @@ describe('Client Fetcher', function () {
});
after(function () {
validateXhr = null;
})
});
});

describe('xhr', function () {
Expand Down

0 comments on commit 067d92b

Please sign in to comment.