Skip to content

Commit

Permalink
Merge fb34be7 into 031723a
Browse files Browse the repository at this point in the history
  • Loading branch information
superdav42 committed Mar 9, 2021
2 parents 031723a + fb34be7 commit 7ef7ea1
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 10 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,41 @@ The prefix to add to every stat collected. Usually used for grouping a set of st

A character or set of characters to replace the '/' (forward slash) characters in your URL path since forward slashes cannot be used in stat names. Defaults to `'_'`

### `defaultFilter`

Defines whether increment and timer stats are turned on by default. Defaults to `{ enableCounter: true, enableTimer: true }`.

### `filters`

An array of custom filters. A successful match requires one of these fields to be defined and match the route:
* `id`: The route id defined in the route's config
* `path`: The path defined in the route
* `method`: The HTTP method of the request/route
* `status`: The returned HTTP status code of the response

Parameters that are not included are considered wildcard and will match all values. Note that if none of these
parameters are included in the filter, then you will get a match on ALL route-response combinations.

In addition to matching, the field can contain the following configuration options:

* name: Defines a custom name for the stat to be reported
* enableTimer: Enable/disable the timer stat from being reported
* enableCounter: Enable/disable the count stat from being reported

Example configuration:
```js
defaultFilter: { // by default, enable timer and disable counter stats
enableCounter: false,
enableTimer: true,
}
filters: [
{ path: '/', enableCounter: true }, // enable counters (keep timers on as well) for this path
{ path: '/test/{param}', enableCounter: true }, // path with a parameter
{ path: '/rename', name: 'rename_stat' }, // rename the metric
{ id: 'match-my-id', enableCounter: true, enableTimer: true }, // match by route id
{ status: 407, name: 'match_on_status', enableCounter: true, enableTimer: true }, // match by status code
]
````

## Example

Expand Down
54 changes: 50 additions & 4 deletions lib/hapi-statsd.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ var StatsdClient = require('statsd-client'),
port: '8125',
prefix: 'hapi',
pathSeparator: '_',
template: '{path}.{method}.{statusCode}'
template: '{path}.{method}.{statusCode}',
defaultFilter: {
enableCounter: true,
enableTimer: true,
},
filters: [],
};

var register = function (server, options) {
Expand All @@ -19,6 +24,36 @@ var register = function (server, options) {
normalizePath = function(path) {
path = (path.indexOf('/') === 0) ? path.substr(1) : path;
return path.replace(/\//g, settings.pathSeparator);
},
filterCache = {}, //cache results so that we don't have to compute them every time
getFilter = function(statName, route, status) {
var cachedFilter = filterCache[statName];
if(cachedFilter) {
return cachedFilter;
}

var foundFilter = settings.filters.find(function(filter) {
if (filter.id && route.settings && route.settings.id !== filter.id) {
return false;
}

if (filter.path && route.path !== filter.path) {
return false;
}

if (filter.status && status !== filter.status) {
return false;
}

if (filter.method && route.method.toUpperCase() !== filter.method.toUpperCase()) {
return false;
}

// If none of the above checks failed, then we have a match
return true;
});

return filterCache[statName] = Hoek.applyToDefaults(settings.defaultFilter, foundFilter || {});
};

server.decorate('server', 'statsd', statsdClient);
Expand All @@ -36,7 +71,7 @@ var register = function (server, options) {
else if (specials.options && request._route === specials.options.route) {
path = '/{cors*}';
}
else if (request._route.path === '/' && request._route.method === 'options'){
else if (request._route.path === '/' && request._route.method === 'options') {
path = '/{cors*}';
}

Expand All @@ -46,8 +81,19 @@ var register = function (server, options) {
.replace('{statusCode}', statusCode);

statName = (statName.indexOf('.') === 0) ? statName.substr(1) : statName;
statsdClient.increment(statName);
statsdClient.timing(statName, startDate);

var filter = getFilter(statName, request._route, statusCode);
if (filter.name) {
statName = filter.name;
}

if (filter.enableCounter) {
statsdClient.increment(statName);
}

if (filter.enableTimer) {
statsdClient.timing(statName, startDate);
}

return h.continue;
});
Expand Down
85 changes: 79 additions & 6 deletions test/integration/hapi-statsd.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,57 @@ var assert = require('assert'),
Hapi = require('hapi');

beforeEach(async function() {
mockStatsdClient.incStat = '';
mockStatsdClient.timingStat = '';
mockStatsdClient.timingDate = '';
server = new Hapi.Server({
host: 'localhost',
port: 8085
});


var get = function (request, reply) {
var get = function (request, h) {
return 'Success!';
};

var err = function (request, reply) {
var err = function (request, h) {
return new Error();
};

server.route({ method: ['GET','OPTIONS'], path: '/', handler: get, config: {cors: true}});
var err407 = function (request, h) {
return h.response('error').code(407);
};

server.route({ method: ['GET','OPTIONS'], path: '/', handler: get, config: {cors: true} });
server.route({ method: 'GET', path: '/err', handler: err, config: {cors: true} });
server.route({ method: 'GET', path: '/test/{param}', handler: get, config: {cors: true}});
server.route({ method: 'GET', path: '/test/{param}', handler: get, config: {cors: true} });
server.route({ method: 'GET', path: '/default', handler: get, config: {cors: true} });
server.route({ method: 'GET', path: '/override', handler: get, config: {cors: true} });
server.route({ method: 'GET', path: '/rename', handler: get, config: {cors: true} });
server.route({ method: 'GET', path: '/match/id', handler: get, config: {id: 'match-my-id', cors: true} });
server.route({ method: 'POST', path: '/match/method', handler: get, config: { cors: true} });
server.route({ method: 'GET', path: '/match/status', handler: err407, config: { cors: true} });

try {
return await server.register({
plugin: plugin,
options: { statsdClient: mockStatsdClient }
options: {
statsdClient: mockStatsdClient,
defaultFilter: {
enableCounter: false,
enableTimer: true,
},
filters: [
{ path: '/', enableCounter: true },
{ path: '/err', enableCounter: true },
{ path: '/test/{param}', enableCounter: true },
{ path: '/override', enableCounter: true, enableTimer: false },
{ path: '/rename', name: 'rename_stat', enableCounter: true, enableTimer: true },
{ id: 'match-my-id', name: 'match_id_stat', enableCounter: true, enableTimer: true },
{ method: 'POST', name: 'match_on_post', enableCounter: true, enableTimer: true },
{ status: 407, name: 'match_on_status', enableCounter: true, enableTimer: true },
],
}
});
} catch (error){
return error
Expand All @@ -52,6 +81,50 @@ describe('hapi-statsd plugin tests', function() {
assert.equal(server.statsd, mockStatsdClient);
});

it('should honor default filter', async function() {
await server.inject('/default')
assert(mockStatsdClient.incStat == '');
assert(mockStatsdClient.timingStat == 'default.GET.200');
assert(mockStatsdClient.timingDate instanceof Date);
});

it('should honor filter override', async function() {
await server.inject('/override' );
assert(mockStatsdClient.incStat == 'override.GET.200');
assert(mockStatsdClient.timingStat == '');
});

it('should use cached value', async function() {
await server.inject('/override');
await server.inject('/override');
assert(mockStatsdClient.incStat == 'override.GET.200');
assert(mockStatsdClient.timingStat == '');
});

it('should rename stat', async function() {
await server.inject('/rename');
assert(mockStatsdClient.incStat == 'rename_stat');
assert(mockStatsdClient.timingStat == 'rename_stat');
});

it('should match on route id', async function() {
await server.inject('/match/id');
assert(mockStatsdClient.incStat == 'match_id_stat');
assert(mockStatsdClient.timingStat == 'match_id_stat');
});

it('should match on request method', async function() {
await server.inject({method: 'POST', url: '/match/method'} );
assert(mockStatsdClient.incStat == 'match_on_post');
assert(mockStatsdClient.timingStat == 'match_on_post');
});

it('should match on status code', async function() {
await server.inject('/match/status');
assert(mockStatsdClient.incStat == 'match_on_status');
assert(mockStatsdClient.timingStat == 'match_on_status');
});

it('should report stats with no path in stat name', async function() {
await server.inject('/');
assert(mockStatsdClient.incStat == 'GET.200');
Expand All @@ -68,7 +141,7 @@ describe('hapi-statsd plugin tests', function() {

it('should report stats with generic not found path', async function() {
await server.inject('/fnord')
assert(mockStatsdClient.incStat == '{notFound*}.GET.404');
assert(mockStatsdClient.incStat == '');
assert(mockStatsdClient.timingStat == '{notFound*}.GET.404');
assert(mockStatsdClient.timingDate instanceof Date);
});
Expand Down

0 comments on commit 7ef7ea1

Please sign in to comment.